当前:LC152 · 乘积最大子数组 · 首次出现于 Day 38 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC152 · Maximum Product Subarray · 动态规划

乘积最大子数组:同时记下最小,负负才能得正

只记最大不够。乘上一个负数,旧的最小会变成新的最大。每个位置在「旧最大×x、旧最小×x、x 自己」里取两端。

维护以 i 结尾的最大乘积 curMax 和最小乘积 curMin。令 a=curMax·x、b=curMin·x,新的 curMax、curMin 分别是 {a,b,x} 的最大和最小。best 记录 curMax 的历史峰值。时间 O(n),空间 O(1)。

时间 O(n)空间 O(1)结论先行 · 全文约 6 节
导读

给你一个整数数组,找出一段连续、非空的子数组,使里面的数乘起来最大,返回这个乘积。数组里可以有正数、负数和 0。子数组必须挨在一起。

主例 nums = [2, 3, -2, 4]。[2,3] 乘积 6,[4] 是 4,[-2,4] 是 -8,整段乘起来是 -48。答案是 6。后面那个 4 没能和前面的 6 接上,因为中间隔着 -2,6×(-2)×4=-48。

LC53 只问和,前面是负数就丢掉。乘积不行:前面很负的一段,再乘一个负数会翻成很大的正数。本文要回答:为什么必须同时记最小,以及主例里那次对调发生在 -2 上之后,best 为什么仍停在 6。

最大最小双变量

子数组必须连续,走到下标 i 时,以 nums[i] 结尾的乘积只有三条来路:接上「以 i−1 结尾的最大」再乘 x,接上「以 i−1 结尾的最小」再乘 x,或者从 x 自己重新开张。缺的不是第三条——LC53 也有「自己开张」——而是第二条:最小必须留着,因为 x 若是负数,最小乘 x 才是最大。

于是每个位置保留两个数。curMax 是以当前数结尾的最大乘积,curMin 是以当前数结尾的最小乘积。先把旧的 curMax·x、curMin·x 算出来,再和 x 一起比:三个数里最大的留给 curMax,最小的留给 curMin。best 只盯 curMax,专记历史冠军。必须先算出两个乘积再覆盖 curMax、curMin,否则第二个式子会用到已经改过的值。

0 会把两条线都拉成 0。下一个数和 0 相乘还是 0,三个候选里通常是它自己更大或更小,等于从自己重启。这和「切断」是一回事,不必单独写 if x==0。

用手走主例。i=0,只有 2,curMax=curMin=best=2。i=1 读到 3:2×3=6,三个候选是 6、6、3,curMax=6,curMin=3,best=6,对应 [2,3]。i=2 读到 -2:6×(-2)=-12,3×(-2)=-6,再加上 -2 自己。最大是 -2,最小是 -12。角色对调:旧的最大变成了很负的最小。best 仍是 6,因为当前结尾的最好只有 -2。

i=3 读到 4:-2×4=-8,-12×4=-48,还有 4 自己。curMax=4,curMin=-48,best 仍是 6。-12 没能在这一步翻正,4 选择自己开张。演示场景停在 max=4、min=-48、best=6,对应答案 [2,3]。若数组在 4 后面再跟一个 -1,-48×(-1)=48 会立刻超过 6——min 就是为这一类后手准备的。对照 [-2, 3, -4]:走到 -4 时旧 min=-6,-6×(-4)=24,best=24,没有 min 只能得到 3。

全是负数时,答案可能是「最靠近 0 的那个负数」,也可能是偶数个负数连乘。双变量都会覆盖:每次自己开张和接上旧段都在三个候选里。不能把 best 初始化成 0,题目允许答案为负。用 nums[0] 同时垫上 curMax、curMin、best。

为什么需要 min负数把大小关系翻转。旧的最小(很负)乘上新的负数,就是当前最大。主例的 -12 没等到下一次翻转;[-2,3,-4] 等到了,24 完全靠 min 翻出来。
nums = [2,3,-2,4]
2i=0
[0]
3
[1]
-2
[2]
4
[3]
最大 2最小 2best 2
i=0 · 2

Go:双变量滚动

solution.goGo
func maxProduct(nums []int) int {
curMax, curMin := nums[0], nums[0]
best := nums[0]
for i := 1; i < len(nums); i++ {
x := nums[i]
a := curMax * x
b := curMin * x
curMax = max3(a, b, x)
curMin = min3(a, b, x)
if curMax > best { best = curMax }
}
return best
}
func max3(a, b, c int) int {
if a > b { a, b = b, a } // 简单实现略
if c > a { a = c }
return a
}

1用 nums[0] 同时垫 curMax、curMin、best。全负时答案可以是负数,不能垫 0。

2a、b 必须在覆盖 curMax、curMin 之前算出来。主例 -2 那一步 a=-12、b=-6。

3三个候选里取最大和最小。主例最后一步 curMax 变成 4,best 仍锁着前面的 6。

总结

以 i 结尾同时记最大和最小,负号对调。主例 best 停在 [2,3] 的 6。

  • 只记最大会在「负负得正」上漏掉答案,对照 [-2,3,-4]=24。
  • 先算旧 max·x 和旧 min·x,再覆盖,避免用到半新半旧的值。
  • 0 把两条线拉成 0,下一个数从自己重启,不必单独分支。
同族题目
LC53最大子数组和LC1567乘积为正的最长子数组LC628三个数的最大乘积