乘积最大子数组
nums=[2,3,-2,4],求乘积最大子数组。
nums=[2,3,-2,4],求乘积最大子数组。
当前格依赖哪些已知状态,为什么这些状态已经计算完成?
每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
先说结论:这道题到底解决什么
怎样从“nums=[2,3,-2,4],求乘积最大子数组。”推导出 线性 DP · 双轨乘积,并证明每次状态变化都不会漏掉答案?
中心结论:每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
- 1.暴力方案在哪里重复计算,为什么仍然是正确基线?
- 2.当前格依赖哪些已知状态,为什么这些状态已经计算完成?
- 3.不变量“curMax/curMin 是以 i 结尾的局部状态;ans 是全局答案,只增不减。”为什么能保证算法安全前进?
完整题目与题意拆解
给定一个整数数组 nums ,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)。
在本站主例中,nums=[2,3,-2,4],求乘积最大子数组。
算法最终需要得到或观察:最大乘积 6,最优子数组 [2,3]。
- • 输入:nums=[2,3,-2,4],求乘积最大子数组。
- • 机器需要维护:curMax(以 i 结尾最大)、curMin(以 i 结尾最小)、ans(全局最大乘积)。
- • 最终可观察结果:最大乘积 6,最优子数组 [2,3]。
先看清算法到底要维护什么
先建立输入、目标、输出和第一批状态,不急着进入模板。
Scene 1 · 题意:连续子数组的最大乘积
完整执行轨迹 · nums = [2, 3, -2, 4] · 4 步
| i | x | prevMax | prevMin | candidates | curMax | curMin | ans |
|---|---|---|---|---|---|---|---|
| 0 | 2 | — | — | — | 2 | 2 | 2 |
| 1 | 3 | 2 | 2 | {3, 6, 6} | 6 | 3 | 6 |
| 2 | -2 | 6 | 3 | {-2, -12, -6} | -2 | -12 | 6 |
| 3 | 4 | -2 | -12 | {4, -8, -48} | 4 | -48 | 6 |
CodeTrace · Go · 双轨 max/min
1func maxProduct(nums []int) int {2curMax := nums[0]3curMin := nums[0]4ans := nums[0]56for i := 1; i < len(nums); i++ {7x := nums[i]8prevMax, prevMin := curMax, curMin9curMax = max3(x, prevMax*x, prevMin*x)10curMin = min3(x, prevMax*x, prevMin*x)11if curMax > ans {12ans = curMax13}14}1516return ans17}
第一层方案:暴力做法
直接递归会反复求同一个子问题;动态规划把答案写入状态表并按依赖顺序复用。
暴力方案的价值是确认题意并提供正确性基线。它通常会覆盖所有候选, 但没有保存已经确认的信息,因此同一状态会被重新计算。
重复工作究竟发生在哪里
把重复读取或重复搜索的区域明确标出,再决定优化必须保存什么。
Scene 1 · 题意:连续子数组的最大乘积
完整执行轨迹 · nums = [2, 3, -2, 4] · 4 步
| i | x | prevMax | prevMin | candidates | curMax | curMin | ans |
|---|---|---|---|---|---|---|---|
| 0 | 2 | — | — | — | 2 | 2 | 2 |
| 1 | 3 | 2 | 2 | {3, 6, 6} | 6 | 3 | 6 |
| 2 | -2 | 6 | 3 | {-2, -12, -6} | -2 | -12 | 6 |
| 3 | 4 | -2 | -12 | {4, -8, -48} | 4 | -48 | 6 |
CodeTrace · Go · 双轨 max/min
1func maxProduct(nums []int) int {2curMax := nums[0]3curMin := nums[0]4ans := nums[0]56for i := 1; i < len(nums); i++ {7x := nums[i]8prevMax, prevMin := curMax, curMin9curMax = max3(x, prevMax*x, prevMin*x)10curMin = min3(x, prevMax*x, prevMin*x)11if curMax > ans {12ans = curMax13}14}1516return ans17}
整体地图:先做什么,再做什么
- 1建模把输入翻译成“状态表账本”,明确答案需要观察什么。
- 2状态只维护 curMax(以 i 结尾最大)、curMin(以 i 结尾最小)、ans(全局最大乘积)。
- 3转移每一步按照 每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
- 4收尾读取 最大乘积 6,最优子数组 [2,3]。,并复核边界与复杂度。
状态表账本:核心概念
先写清 dp 状态含义,再推导转移、初值和遍历顺序。
这道题不是为了记住一个组件名称,而是为了让状态具有可解释的语义:curMax(以 i 结尾最大)、curMin(以 i 结尾最小)、ans(全局最大乘积)。
- • curMax/curMin 是以 i 结尾的局部状态;ans 是全局答案,只增不减。
- • 必须先保存 prevMax/prevMin,再更新 curMax/curMin,顺序不能乱。
建立“状态表账本”心智模型
用主例建立核心状态,先预测下一步,再公开正确分支和理由。
Scene 1 · 题意:连续子数组的最大乘积
完整执行轨迹 · nums = [2, 3, -2, 4] · 4 步
| i | x | prevMax | prevMin | candidates | curMax | curMin | ans |
|---|---|---|---|---|---|---|---|
| 0 | 2 | — | — | — | 2 | 2 | 2 |
| 1 | 3 | 2 | 2 | {3, 6, 6} | 6 | 3 | 6 |
| 2 | -2 | 6 | 3 | {-2, -12, -6} | -2 | -12 | 6 |
| 3 | 4 | -2 | -12 | {4, -8, -48} | 4 | -48 | 6 |
CodeTrace · Go · 双轨 max/min
1func maxProduct(nums []int) int {2curMax := nums[0]3curMin := nums[0]4ans := nums[0]56for i := 1; i < len(nums); i++ {7x := nums[i]8prevMax, prevMin := curMax, curMin9curMax = max3(x, prevMax*x, prevMin*x)10curMin = min3(x, prevMax*x, prevMin*x)11if curMax > ans {12ans = curMax13}14}1516return ans17}
核心机制:状态如何一步步变化
给出一个数组,要求找出这个数组中连续元素乘积最大的值。 这一题是 DP 的题,状态转移方程是:最大值是 `Max(f(n)) = Max( Max(f(n-1)) n, Min(f(n-1)) n)`;最小值是 `Min(f(n)) = Min( Max(f(n-1)) n, Min(f(n-1)) n)`。只要动态维护这两个值,如果最后一个数是负数,最大值就在负数 最小值中产生,如果最后一个数是正数,最大值就在正数 最大值中产生。
每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
执行过程中持续维护:curMax(以 i 结尾最大)、curMin(以 i 结尾最小)、ans(全局最大乘积)。
正确性依赖以下不变量:curMax/curMin 是以 i 结尾的局部状态;ans 是全局答案,只增不减。;必须先保存 prevMax/prevMin,再更新 curMax/curMin,顺序不能乱。
面试时可以压缩为:这题和最大子数组和不同,乘积遇到负数会发生符号翻转,所以当前位置的最大乘积可能来自之前的最小乘积。遍历时维护以 i 结尾的最大乘积 curMax 和最小乘积 curMin,每一步从 x、prevMax×x、prevMin×x 三者中更新,再用 curMax 更新全局答案 ans。时间 O(n),空间 O(1)。
落到当前题,执行机制可以压缩为:每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。每一次更新都必须保持核心不变量,而不是只让样例碰巧得到正确答案。
一次状态转移为什么成立
集中观察一次选择、计算和状态写回,让变量变化与原因同时出现。
Scene 6 · i=1 · x=+3
讲解 · ans 刷新!用 curMax 更新全局最优(不是把 curMax 当最终答案)。
算法 · ans = max(2, 6) = 6。
🏆 ans 刷新:ans = max(旧 ans, curMax) = 6(curMax 是局部,ans 才是全局答案)。
以当前位置 i 结尾的最大乘积
以当前位置 i 结尾的最小乘积
遍历过程中见过的全局最大乘积(不是 curMax!)
关键:先保存 prevMax/prevMin,再更新 curMax/curMin——否则 prevMax 被污染。
完整执行轨迹 · nums = [2, 3, -2, 4] · 2 步
| i | x | prevMax | prevMin | candidates | curMax | curMin | ans |
|---|---|---|---|---|---|---|---|
| 0 | 2 | — | — | — | 2 | 2 | 2 |
| 1 | 3 | 2 | 2 | {3, 6, 6} | 6 | 3 | 6 |
CodeTrace · Go · 双轨 max/min
1func maxProduct(nums []int) int {2curMax := nums[0]3curMin := nums[0]4ans := nums[0]56for i := 1; i < len(nums); i++ {7x := nums[i]8prevMax, prevMin := curMax, curMin9curMax = max3(x, prevMax*x, prevMin*x)10curMin = min3(x, prevMax*x, prevMin*x)11if curMax > ans {12ans = curMax13}14}1516return ans17}
正确性证明:为什么不会漏答案
初始化:算法开始时,全部合法候选仍在状态表示范围内;curMax/curMin 是以 i 结尾的局部状态;ans 是全局答案,只增不减。
保持:执行“每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。”时,只删除已经能证明不可能的候选,并把新信息写回 curMax(以 i 结尾最大)、curMin(以 i 结尾最小)、ans(全局最大乘积)。
终止:没有待处理状态或达到命中条件时,当前可观察结果就是“最大乘积 6,最优子数组 [2,3]。”。
完整执行过程
- 1题目与输入nums=[2,3,-2,4],求乘积最大子数组。 因为:每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
- 2Scene 2 · 反例:只维护 curMax 会失败负×负反杀:历史最小乘积乘负数可跃升为最大。 因为:若跳过 curMin,24 永远无法进入 ans。
- 3Scene 3 · 核心状态:curMax / curMin / ans混淆 curMax 与 ans 是最常见的面试失误之一。 因为:ans 用 curMax 更新,但 curMax 本身每步重算。
- 4Scene 5 · 初始化首元素初始化三条滚动状态。 因为:ans 初值必须是 nums[0],不能是 0。
- 5Scene 6 · i=1 · x=+3ans 刷新!用 curMax 更新全局最优(不是把 curMax 当最终答案)。 因为:ans 只增不减,记录历史最优的「以 i 结尾」最大乘积。
- 6预测下一步先不要看下一帧——根据当前不变量,预测算法接下来会怎么动。 因为:主动预测会暴露你对不变量的真实理解,比被动看动画有效得多。
- 7Scene 8 · i=3 · x=+4三候选人竞争完毕,curMax/curMin 沿各自轨道延伸。 因为:每步 O(1) 更新,扫描一遍数组。
- 8收尾与复杂度最大乘积 6,最优子数组 [2,3]。 因为:时间 O(n) · 空间 O(1)。每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
从输入完整走到可观察结果
从主例第一帧运行到答案,时间轴始终显示当前状态、因果解释和下一步。
Scene 1 · 题意:连续子数组的最大乘积
完整执行轨迹 · nums = [2, 3, -2, 4] · 4 步
| i | x | prevMax | prevMin | candidates | curMax | curMin | ans |
|---|---|---|---|---|---|---|---|
| 0 | 2 | — | — | — | 2 | 2 | 2 |
| 1 | 3 | 2 | 2 | {3, 6, 6} | 6 | 3 | 6 |
| 2 | -2 | 6 | 3 | {-2, -12, -6} | -2 | -12 | 6 |
| 3 | 4 | -2 | -12 | {4, -8, -48} | 4 | -48 | 6 |
CodeTrace · Go · 双轨 max/min
1func maxProduct(nums []int) int {2curMax := nums[0]3curMin := nums[0]4ans := nums[0]56for i := 1; i < len(nums); i++ {7x := nums[i]8prevMax, prevMin := curMax, curMin9curMax = max3(x, prevMax*x, prevMin*x)10curMin = min3(x, prevMax*x, prevMin*x)11if curMax > ans {12ans = curMax13}14}1516return ans17}
把动画和 Go 代码逐行对应
代码窗口不会按容易漂移的固定数字行号硬绑动画,而是根据当前语义阶段, 在完整 Go 实现中定位选择、计算、写回或返回分支。当前变量与高亮行一起变化。
让每个动作都落到 Go 分支
重新执行关键分支,只显示当前代码附近窗口,并说明这行为什么在此刻运行。
Scene 5 · 初始化
以当前位置 i 结尾的最大乘积
以当前位置 i 结尾的最小乘积
遍历过程中见过的全局最大乘积(不是 curMax!)
关键:先保存 prevMax/prevMin,再更新 curMax/curMin——否则 prevMax 被污染。
完整执行轨迹 · nums = [2, 3, -2, 4] · 1 步
| i | x | prevMax | prevMin | candidates | curMax | curMin | ans |
|---|---|---|---|---|---|---|---|
| 0 | 2 | — | — | — | 2 | 2 | 2 |
CodeTrace · Go · 双轨 max/min
1func maxProduct(nums []int) int {2curMax := nums[0]3curMin := nums[0]4ans := nums[0]56for i := 1; i < len(nums); i++ {7x := nums[i]8prevMax, prevMin := curMax, curMin9curMax = max3(x, prevMax*x, prevMin*x)10curMin = min3(x, prevMax*x, prevMin*x)11if curMax > ans {12ans = curMax13}14}1516return ans17}
完整 Go 提交代码与最小测试
func maxProduct(nums []int) int {
minimum, maximum, res := nums[0], nums[0], nums[0]
for i := 1; i < len(nums); i++ {
if nums[i] < 0 {
maximum, minimum = minimum, maximum
}
maximum = max(nums[i], maximum*nums[i])
minimum = min(nums[i], minimum*nums[i])
res = max(res, maximum)
}
return res
}
func max(a int, b int) int {
if a > b {
return a
}
return b
}
func min(a int, b int) int {
if a > b {
return b
}
return a
}func main() {
// 1. 主例
// 输入:mode="max-product-subarray", nums=[2,3,-2,4]
// 期望:最大乘积 6,最优子数组 [2,3]。
//
// 2. 失败 / 未命中
// 检查:只维护 curMax:负×负时 prevMin×x 可反杀成最大,会漏掉最优解(如 [-2,3,-4] 漏 24)。
//
// 3. 边界
// 空输入、单元素、最小合法规模,以及答案恰好落在边界的情况。
//
// 4. 迁移
// LC53 最大和;LC713 乘积小于 K
}Go 参考实现基于 halfrost/LeetCode-Go 的 MIT 许可代码整理,并按本站教学结构补充解释与动画映射。
正确性与复杂度
执行过程中只保留仍可能影响答案的状态。每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
额外状态主要用于维护:curMax(以 i 结尾最大)、curMin(以 i 结尾最小)、ans(全局最大乘积)。
- • curMax/curMin 是以 i 结尾的局部状态;ans 是全局答案,只增不减。
- • 必须先保存 prevMax/prevMin,再更新 curMax/curMin,顺序不能乱。
最容易写错的地方
只维护 curMax:负×负时 prevMin×x 可反杀成最大,会漏掉最优解(如 [-2,3,-4] 漏 24)。
忘记 curMin:无法记录历史最小乘积,负数翻转时 curMax 来源丢失。
把 curMax 当成全局答案:curMax 每步重算,必须用 ans = max(ans, curMax) 记录全局最优。
没处理 0:0 切断乘积链,三候选皆 0,必须从当前格重启(如 [-2,0,-1] 答案是 0)。
更新顺序错误:先算 curMax 再算 curMin 会污染 prevMax,必须 oldMax, oldMin := curMax, curMin 先保存。
必须额外检查空输入、单元素、未命中或不可达情况,以及恰好落在边界的输入。
最后复盘:带走逻辑链
- 1题意nums=[2,3,-2,4],求乘积最大子数组。
- 2重复直接递归会反复求同一个子问题;动态规划把答案写入状态表并按依赖顺序复用。
- 3优化每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
- 4证明curMax/curMin 是以 i 结尾的局部状态;ans 是全局答案,只增不减。;必须先保存 prevMax/prevMin,再更新 curMax/curMin,顺序不能乱。
- 5复杂度时间 O(n),空间 O(1)
- • LC53 最大和
- • LC713 乘积小于 K