乘积最大子数组:同时记下最小,负负才能得正
只记最大不够。乘上一个负数,旧的最小会变成新的最大。每个位置在「旧最大×x、旧最小×x、x 自己」里取两端。
维护以 i 结尾的最大乘积 curMax 和最小乘积 curMin。令 a=curMax·x、b=curMin·x,新的 curMax、curMin 分别是 {a,b,x} 的最大和最小。best 记录 curMax 的历史峰值。时间 O(n),空间 O(1)。
给你一个整数数组,找出一段连续、非空的子数组,使里面的数乘起来最大,返回这个乘积。数组里可以有正数、负数和 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 翻出来。
Go:双变量滚动
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 * xb := curMin * xcurMax = 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,下一个数从自己重启,不必单独分支。