LEETCODE 152Medium
乘积最大子数组
负数一乘会把最大翻成最小、最小翻成最大,所以要同时维护以 i 结尾的最大积与最小积。
问题拆解
在数组里找一个连续子数组,让它的乘积最大。看起来和“最大子数组和”是一道题换了个运算符,照搬那套做法——维护“以 i 结尾的最大和”——却会在负数上翻车:一个很小的负积,乘上一个负数,可能一跃成为最大积。
比如 [-2, 3, -4]。走到 -4 时,以它结尾的最大积是 (-2) * 3 * (-4) = 24,它的来源恰恰是前一步的最小积 -6。也就是说,只记最大值会把未来的答案丢掉。
关键观察:以 i 结尾的最大积,只可能来自三个候选——nums[i] 自己、前一步最大积乘 nums[i]、前一步最小积乘 nums[i]。最小积同理。所以把两条状态一起往前滚就够了。
乘法没有单调性:正数保序,负数反序。凡是运算会“翻转大小关系”的 DP,就要把最大和最小两个极端一起带着走。
同时滚动最大积与最小积
一个小技巧:当 nums[i] 是负数时,先把最大积和最小积互换,之后的转移就和正数完全一样,不用写三元比较。
public int maxProduct(int[] nums) {
int ans = nums[0], maxProd = nums[0], minProd = nums[0];
for (int i = 1; i < nums.length; i++) {
int x = nums[i];
if (x < 0) { // 负数把最大最小对调
int t = maxProd;
maxProd = minProd;
minProd = t;
}
maxProd = Math.max(x, maxProd * x); // 要么接上前缀,要么另起炉灶
minProd = Math.min(x, minProd * x);
ans = Math.max(ans, maxProd);
}
return ans;
}
def maxProduct(nums: List[int]) -> int:
ans = max_prod = min_prod = nums[0]
for x in nums[1:]:
if x < 0: # 负数把最大最小对调
max_prod, min_prod = min_prod, max_prod
max_prod = max(x, max_prod * x) # 要么接上前缀,要么另起炉灶
min_prod = min(x, min_prod * x)
ans = max(ans, max_prod)
return ans
func maxProduct(nums []int) int {
ans, maxProd, minProd := nums[0], nums[0], nums[0]
for _, x := range nums[1:] {
if x < 0 { // 负数把最大最小对调
maxProd, minProd = minProd, maxProd
}
maxProd = max(x, maxProd*x) // 要么接上前缀,要么另起炉灶
minProd = min(x, minProd*x)
ans = max(ans, maxProd)
}
return ans
}
pub fn max_product(nums: Vec<i32>) -> i32 {
let mut ans = nums[0];
let (mut max_prod, mut min_prod) = (nums[0], nums[0]);
for &x in &nums[1..] {
if x < 0 {
// 负数把最大最小对调
std::mem::swap(&mut max_prod, &mut min_prod);
}
max_prod = x.max(max_prod * x); // 要么接上前缀,要么另起炉灶
min_prod = x.min(min_prod * x);
ans = ans.max(max_prod);
}
ans
}
每一步转移里的 max(x, ...) 不能省:它对应“不要前面的乘积,从自己重新开始”,专门处理前缀里出现 0(乘积被清零,接着乘只会是 0)的情况。另一个易错点是答案要在循环里逐步取最大——最大积以哪个下标结尾是不知道的,不能只看最后一个状态。
复杂度
| 指标 | 复杂度 | 原因 |
|---|---|---|
| 时间 | O(n) |
一次遍历,每个元素常数次乘法比较 |
| 空间 | O(1) |
只滚动最大积、最小积、答案三个变量 |
可以迁移的模式
- 转移运算会翻转大小关系时(乘负数、取相反数),最优解可能藏在“最劣状态”里,两个极端要一起维护;
- “以 i 结尾”的状态定义配上“要么延续、要么重开”的转移,是子数组类 DP 的标准骨架;
- 答案沿途收集,而不是只看最后一个状态。
把“最大子数组和”里的一条状态拆成两条,这道题就解完了——差别全在负数身上。