一、完整代码(精简注释)
class Solution {
public:
int maxProduct(vector<int>& nums) {
// 初始化当前最大、最小乘积及全局最大值
long long maxfn = nums[0];
long long minfn = nums[0];
long long ans = nums[0];
for (int i = 1; i < nums.size(); i++) {
// 保存上一轮的最大、最小乘积
long long mx = maxfn, mn = minfn;
// 三种情况取最大乘积
maxfn = max((long long)nums[i],
max(mx * nums[i], mn * nums[i]));
// 三种情况取最小乘积
minfn = min((long long)nums[i],
min(mx * nums[i], mn * nums[i]));
// 更新全局最大乘积
ans = max(maxfn, ans);
}
return (int)ans;
}
};
二、动态规划五步总结
1. 确定 DP 状态
这道题使用动态规划的空间优化形式,只需要三个变量:
-
maxfn:以当前元素结尾的连续子数组的最大乘积。 -
minfn:以当前元素结尾的连续子数组的最小乘积。 -
ans:遍历过程中出现的全局最大乘积。
为什么需要维护最小乘积?
因为负数乘负数可能得到最大的正数,所以之前的最小乘积也可能产生当前的最大乘积。
2. 确定递推公式
对于每个 nums[i],考虑三种情况:
-
当前元素自己作为一个子数组:
nums[i] -
前一个最大乘积乘以当前元素:
mx * nums[i] -
前一个最小乘积乘以当前元素:
mn * nums[i]
最大乘积:
maxfn = max((long long)nums[i],
max(mx * nums[i], mn * nums[i]));
最小乘积:
minfn = min((long long)nums[i],
min(mx * nums[i], mn * nums[i]));
注意: mx 和 mn 保存的是上一轮的状态,避免更新 maxfn 后影响 minfn 的计算。
3. 初始化
long long maxfn = nums[0];
long long minfn = nums[0];
long long ans = nums[0];
只有第一个元素时,最大乘积、最小乘积和全局最大乘积都等于 nums[0]。
4. 确定遍历顺序
for(int i = 1; i < nums.size(); i++)
从左往右遍历,因为当前位置的最大、最小乘积依赖前一个位置的状态。
5. 举例推导
假设:
nums = [-2, 3, -4]
| i | numsi | maxfn | minfn | ans |
|---|---|---|---|---|
| 0 | -2 | -2 | -2 | -2 |
| 1 | 3 | 3 | -6 | 3 |
| 2 | -4 | 24 | -12 | 24 |
当 i = 2 时:
mx = 3;
mn = -6;
nums[i] = -4;
计算:
maxfn = max(-4, max(-12, 24)); // 24
minfn = min(-4, min(-12, 24)); // -12
ans = max(24, 3); // 24
最终返回 24。
三、复杂度分析
-
时间复杂度: \(O(n)\),只需要遍历一次数组。
-
空间复杂度: \(O(1)\),只使用固定数量的变量,不需要额外的 DP 数组。
四、核心易错点
- 为什么需要同时维护最大值和最小值?
负数会改变乘积的大小关系,之前的最小乘积乘以负数可能成为最大乘积。
- 为什么需要 mx 和 mn?
因为更新当前最大、最小乘积时,都必须使用上一轮的状态,不能使用已经更新的 maxfn。
- 为什么 ans 不能直接等于 maxfn?
因为最大乘积子数组不一定以最后一个元素结尾,所以需要记录遍历过程中的最大值:
ans = max(ans, maxfn);
- 为什么使用 long long?
为了扩大中间乘积的表示范围,降低整数溢出的风险。但 long long 也有范围限制,并不能保证任意输入都不溢出。
最终记忆:
这道题的核心就是 维护两个状态(最大乘积、最小乘积),每次从三种情况中更新当前状态,再通过 ans 保存全局最大值。
普通 DP 需要 \(O(n)\) 空间,而这里每次只依赖上一轮状态,因此可以优化到 \(O(1)\) 空间。