标签:子序列 解法 序列 bar 区分 ref swa public 负数
给定一个整数数组 nums
,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)。
示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
https://leetcode-cn.com/problems/maximum-product-subarray/
其实这道题最直接的方法就是用 DP 来做,而且要用两个 dp 数组,
其中 f[i] 表示子数组 [0, i] 范围内并且一定包含 nums[i] 数字的最大子数组乘积,
g[i] 表示子数组 [0, i] 范围内并且一定包含 nums[i] 数字的最小子数组乘积,
初始化时 f[0] 和 g[0] 都初始化为 nums[0],其余都初始化为0。
那么从数组的第二个数字开始遍历,那么此时的最大值和最小值只会在这三个数字之间产生,
即 f[i-1] * nums[i],g[i-1] * nums[i],和 nums[i]。
所以用三者中的最大值来更新 f[i],用最小值来更新 g[i],然后用 f[i] 来更新结果 res 即可,由于最终的结果不一定会包括 nums[n-1] 这个数字,所以 f[n-1] 不一定是最终解,不断更新的结果 res 才是,参见代码如下:
class Solution {
public:
int maxProduct(vector<int>& nums) {
int res = nums[0], n = nums.size();
vector<int> f(n, 0), g(n, 0);
f[0] = nums[0];
g[0] = nums[0];
for (int i = 1; i < n; ++i) {
f[i] = max(max(f[i - 1] * nums[i], g[i - 1] * nums[i]), nums[i]);
g[i] = min(min(f[i - 1] * nums[i], g[i - 1] * nums[i]), nums[i]);
res = max(res, f[i]);
}
return res;
}
};
class Solution:
def maxProduct(self, nums: List[int]) -> int:
n = len(nums)
f = [1] * (n + 1)
fg = [1] * (n + 1)
res = float('-inf')
for i in range(1, n + 1):
f[i] = max(f[i - 1] * nums[i - 1], g[i - 1] * nums[i - 1], nums[i - 1])
g[i] = min(f[i - 1] * nums[i - 1], g[i - 1] * nums[i - 1], nums[i - 1])
res = max(res, f[i])
return res
我们可以对上面的解法进行空间上的优化
class Solution {
public:
int maxProduct(vector<int>& nums) {
if (nums.empty()) return 0;
int res = nums[0], mn = nums[0], mx = nums[0];
for (int i = 1; i < nums.size(); ++i) {
int tmax = mx, tmin = mn;
mx = max(max(nums[i], tmax * nums[i]), tmin * nums[i]);
mn = min(min(nums[i], tmax * nums[i]), tmin * nums[i]);
res = max(res, mx);
}
return res;
}
};
下面这种方法也是用两个变量来表示当前最大值和最小值的,但是没有无脑比较三个数,而是对于当前的 nums[i] 值进行了正负情况的讨论
参见代码如下:
class Solution {
public:
int maxProduct(vector<int>& nums) {
int res = nums[0], mx = res, mn = res;
for (int i = 1; i < nums.size(); ++i) {
//当遍历到一个正数时,此时的最大值等于之前的最大值乘以这个正数和当前正数中的较大值,
//此时的最小值等于之前的最小值乘以这个正数和当前正数中的较小值。
if (nums[i] > 0) {
mx = max(mx * nums[i], nums[i]);
mn = min(mn * nums[i], nums[i]);
}
//当遍历到一个负数时,先用一个变量t保存之前的最大值 mx,
//此时的最大值等于之前最小值乘以这个负数和当前负数中的较大值,
//此时的最小值等于之前保存的最大值t乘以这个负数和当前负数中的较小值。
else {
int t = mx;
mx = max(mn * nums[i], nums[i]);
mn = min(t * nums[i], nums[i]);
}
res = max(res, mx); //在每遍历完一个数时,都要更新最终的最大值。
}
return res;
}
};
而下面这种方法的巧妙处在于先判断一个当前数字是否是负数,是的话就交换最大值和最小值。
那么此时的 mx 就是之前的 mn,所以 mx 的更新还是跟上面的方法是统一的,
而在在更新 mn 的时候,之前的 mx 已经保存到 mn 中了,而且并没有改变,所以可以直接拿来用,
参见代码如下:
class Solution {
public:
int maxProduct(vector<int>& nums) {
int res = nums[0], mx = res, mn = res;
for (int i = 1; i < nums.size(); ++i) {
if (nums[i] < 0) swap(mx, mn);
mx = max(nums[i], mx * nums[i]);
mn = min(nums[i], mn * nums[i]);
res = max(res, mx);
}
return res;
}
};
这种解法遍历了两次,一次是正向遍历,一次是反向遍历,
相当于正向建立一个累加积数组,每次用出现的最大值更新结果 res,
然后再反响建立一个累加积数组,再用出现的最大值更新结果 res,注意当遇到0的时候,prod 要重置为1。
至于为啥正反两次遍历就可以得到正确的结果了呢?主要还是由于负数个数的关系,因为负数可能会把最大值和最小值翻转,那么当有奇数个负数时,如果只是正向遍历的话,可能会出错,比如 [-1, -2, -3],累加积会得到 -1,2,-6,看起来最大值只能为2,其实不对,而如果我们再反向来一遍,累加积为 -3,6,-6,就可以得到6了。
所以当负数个数为奇数时,首次出现和末尾出现的负数就很重要,有可能会是最大积的组成数字,所以遍历两次就不会漏掉组成最大值的机会,参见代码如下:
class Solution {
public:
int maxProduct(vector<int>& nums) {
int res = nums[0], prod = 1, n = nums.size();
for (int i = 0; i < n; ++i) {
res = max(res, prod *= nums[i]);
if (nums[i] == 0) prod = 1;
}
prod = 1;
for (int i = n - 1; i >= 0; --i) {
res = max(res, prod *= nums[i]);
if (nums[i] == 0) prod = 1;
}
return res;
}
};
标签:子序列 解法 序列 bar 区分 ref swa public 负数
原文地址:https://www.cnblogs.com/wwj99/p/12425867.html