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

LC152动态规划线性 DP · 双轨乘积状态表账本

乘积最大子数组

nums=[2,3,-2,4],求乘积最大子数组。

题目是什么

nums=[2,3,-2,4],求乘积最大子数组。

解决什么问题

当前格依赖哪些已知状态,为什么这些状态已经计算完成?

核心结论

每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。

01交互算法精讲

先说结论:这道题到底解决什么

怎样从“nums=[2,3,-2,4],求乘积最大子数组。”推导出 线性 DP · 双轨乘积,并证明每次状态变化都不会漏掉答案?

中心结论:每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。

读完必须能回答
  1. 1.暴力方案在哪里重复计算,为什么仍然是正确基线?
  2. 2.当前格依赖哪些已知状态,为什么这些状态已经计算完成?
  3. 3.不变量“curMax/curMin 是以 i 结尾的局部状态;ans 是全局答案,只增不减。”为什么能保证算法安全前进?
02交互算法精讲

完整题目与题意拆解

给定一个整数数组 nums ,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)。

在本站主例中,nums=[2,3,-2,4],求乘积最大子数组。

算法最终需要得到或观察:最大乘积 6,最优子数组 [2,3]。

把题目翻译成状态
  • 输入:nums=[2,3,-2,4],求乘积最大子数组。
  • 机器需要维护:curMax(以 i 结尾最大)、curMin(以 i 结尾最小)、ans(全局最大乘积)。
  • 最终可观察结果:最大乘积 6,最优子数组 [2,3]。
动画 1 · 题意扫描

先看清算法到底要维护什么

先建立输入、目标、输出和第一批状态,不急着进入模板。

Step 1/20%
题目与输入建立输入、目标与算法心智
三候选人竞争 · 双轨乘积 DP

Scene 1 · 题意:连续子数组的最大乘积

ans · 全局最大
2
先理解题意,不急着写 DP
curMax · 以 i 结尾最大
curMin · 以 i 结尾最小
+2
i
+3
-2
+4
max 段 min 段
状态面板
i
0
x
+2
prevMax
prevMin
curMax · 以 i 结尾最大2
curMin · 以 i 结尾最小2
ans · 全局最大2
完整执行轨迹 · nums = [2, 3, -2, 4] · 4
ixprevMaxprevMincandidatescurMaxcurMinans
02222
1322{3, 6, 6}636
2-263{-2, -12, -6}-2-126
34-2-12{4, -8, -48}4-486
CodeTrace · Go · 双轨 max/min
1func maxProduct(nums []int) int {
2 curMax := nums[0]
3 curMin := nums[0]
4 ans := nums[0]
5
6 for i := 1; i < len(nums); i++ {
7 x := nums[i]
8 prevMax, prevMin := curMax, curMin
9 curMax = max3(x, prevMax*x, prevMin*x)
10 curMin = min3(x, prevMax*x, prevMin*x)
11 if curMax > ans {
12 ans = curMax
13 }
14 }
15
16 return ans
17}
03交互算法精讲

第一层方案:暴力做法

直接递归会反复求同一个子问题;动态规划把答案写入状态表并按依赖顺序复用。

暴力方案的价值是确认题意并提供正确性基线。它通常会覆盖所有候选, 但没有保存已经确认的信息,因此同一状态会被重新计算。

动画 2 · 暴力重复

重复工作究竟发生在哪里

把重复读取或重复搜索的区域明确标出,再决定优化必须保存什么。

Step 1/20%
先做对:建立暴力基线枚举所有候选并完整验证
三候选人竞争 · 双轨乘积 DP

Scene 1 · 题意:连续子数组的最大乘积

ans · 全局最大
2
先理解题意,不急着写 DP
curMax · 以 i 结尾最大
curMin · 以 i 结尾最小
+2
i
+3
-2
+4
max 段 min 段
状态面板
i
0
x
+2
prevMax
prevMin
curMax · 以 i 结尾最大2
curMin · 以 i 结尾最小2
ans · 全局最大2
完整执行轨迹 · nums = [2, 3, -2, 4] · 4
ixprevMaxprevMincandidatescurMaxcurMinans
02222
1322{3, 6, 6}636
2-263{-2, -12, -6}-2-126
34-2-12{4, -8, -48}4-486
CodeTrace · Go · 双轨 max/min
1func maxProduct(nums []int) int {
2 curMax := nums[0]
3 curMin := nums[0]
4 ans := nums[0]
5
6 for i := 1; i < len(nums); i++ {
7 x := nums[i]
8 prevMax, prevMin := curMax, curMin
9 curMax = max3(x, prevMax*x, prevMin*x)
10 curMin = min3(x, prevMax*x, prevMin*x)
11 if curMax > ans {
12 ans = curMax
13 }
14 }
15
16 return ans
17}
优化方向:每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
04交互算法精讲

整体地图:先做什么,再做什么

  1. 1建模把输入翻译成“状态表账本”,明确答案需要观察什么。
  2. 2状态只维护 curMax(以 i 结尾最大)、curMin(以 i 结尾最小)、ans(全局最大乘积)。
  3. 3转移每一步按照 每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
  4. 4收尾读取 最大乘积 6,最优子数组 [2,3]。,并复核边界与复杂度。
05交互算法精讲

状态表账本:核心概念

先写清 dp 状态含义,再推导转移、初值和遍历顺序。

这道题不是为了记住一个组件名称,而是为了让状态具有可解释的语义:curMax(以 i 结尾最大)、curMin(以 i 结尾最小)、ans(全局最大乘积)。

核心不变量
  • curMax/curMin 是以 i 结尾的局部状态;ans 是全局答案,只增不减。
  • 必须先保存 prevMax/prevMin,再更新 curMax/curMin,顺序不能乱。
动画 3 · 核心概念

建立“状态表账本”心智模型

用主例建立核心状态,先预测下一步,再公开正确分支和理由。

Step 1/40%
题目与输入建立输入、目标与算法心智
三候选人竞争 · 双轨乘积 DP

Scene 1 · 题意:连续子数组的最大乘积

ans · 全局最大
2
先理解题意,不急着写 DP
curMax · 以 i 结尾最大
curMin · 以 i 结尾最小
+2
i
+3
-2
+4
max 段 min 段
状态面板
i
0
x
+2
prevMax
prevMin
curMax · 以 i 结尾最大2
curMin · 以 i 结尾最小2
ans · 全局最大2
完整执行轨迹 · nums = [2, 3, -2, 4] · 4
ixprevMaxprevMincandidatescurMaxcurMinans
02222
1322{3, 6, 6}636
2-263{-2, -12, -6}-2-126
34-2-12{4, -8, -48}4-486
CodeTrace · Go · 双轨 max/min
1func maxProduct(nums []int) int {
2 curMax := nums[0]
3 curMin := nums[0]
4 ans := nums[0]
5
6 for i := 1; i < len(nums); i++ {
7 x := nums[i]
8 prevMax, prevMin := curMax, curMin
9 curMax = max3(x, prevMax*x, prevMin*x)
10 curMin = min3(x, prevMax*x, prevMin*x)
11 if curMax > ans {
12 ans = curMax
13 }
14 }
15
16 return ans
17}
06交互算法精讲

核心机制:状态如何一步步变化

给出一个数组,要求找出这个数组中连续元素乘积最大的值。 这一题是 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。每一次更新都必须保持核心不变量,而不是只让样例碰巧得到正确答案。

动画 4 · 机制构建

一次状态转移为什么成立

集中观察一次选择、计算和状态写回,让变量变化与原因同时出现。

Step 1/50%
Scene 6 · i=1 · x=+3ans 刷新!用 curMax 更新全局最优(不是把 curMax 当最终答案)。
三候选人竞争 · 双轨乘积 DP

Scene 6 · i=1 · x=+3

ans · 全局最大
6
ans 刷新
curMax · 以 i 结尾最大
curMin · 以 i 结尾最小
+2
+3
i
-2
+4
6
3
max 段 [2, 3]min 段 [2, 3]
状态面板
i
1
x
+3
prevMax
2
prevMin
2
curMax · 以 i 结尾最大6
curMin · 以 i 结尾最小3
ans · 全局最大6
当前最优段 → [2, 3]
三候选人竞争 · i=1 · x=+3
x
3
→ curMin
prevMax × x
6
→ curMax
prevMin × x
6
→ curMax
curMax = max(3, 6, 6) = 6 · curMin = min(3, 6, 6) = 3

讲解 · ans 刷新!用 curMax 更新全局最优(不是把 curMax 当最终答案)。

算法 · ans = max(2, 6) = 6。

🏆 ans 刷新:ans = max(旧 ans, curMax) = 6(curMax 是局部,ans 才是全局答案)。

curMax

以当前位置 i 结尾的最大乘积

curMin

以当前位置 i 结尾的最小乘积

ans

遍历过程中见过的全局最大乘积(不是 curMax!)

递推公式
curMax = max(x, prevMax × x, prevMin × x)
curMin = min(x, prevMax × x, prevMin × x)
ans = max(ans, curMax)

关键:先保存 prevMax/prevMin,再更新 curMax/curMin——否则 prevMax 被污染。

完整执行轨迹 · nums = [2, 3, -2, 4] · 2
ixprevMaxprevMincandidatescurMaxcurMinans
02222
1322{3, 6, 6}636
CodeTrace · Go · 双轨 max/min
1func maxProduct(nums []int) int {
2 curMax := nums[0]
3 curMin := nums[0]
4 ans := nums[0]
5
6 for i := 1; i < len(nums); i++ {
7 x := nums[i]
8 prevMax, prevMin := curMax, curMin
9 curMax = max3(x, prevMax*x, prevMin*x)
10 curMin = min3(x, prevMax*x, prevMin*x)
11 if curMax > ans {
12 ans = curMax
13 }
14 }
15
16 return ans
17}
07交互算法精讲

正确性证明:为什么不会漏答案

初始化

初始化:算法开始时,全部合法候选仍在状态表示范围内;curMax/curMin 是以 i 结尾的局部状态;ans 是全局答案,只增不减。

保持

保持:执行“每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。”时,只删除已经能证明不可能的候选,并把新信息写回 curMax(以 i 结尾最大)、curMin(以 i 结尾最小)、ans(全局最大乘积)。

终止

终止:没有待处理状态或达到命中条件时,当前可观察结果就是“最大乘积 6,最优子数组 [2,3]。”。

正确性抓手不是“样例跑通”,而是每一帧结束后仍能复述:curMax/curMin 是以 i 结尾的局部状态;ans 是全局答案,只增不减。;必须先保存 prevMax/prevMin,再更新 curMax/curMin,顺序不能乱。
08交互算法精讲

完整执行过程

  1. 1题目与输入nums=[2,3,-2,4],求乘积最大子数组。 因为:每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
  2. 2Scene 2 · 反例:只维护 curMax 会失败负×负反杀:历史最小乘积乘负数可跃升为最大。 因为:若跳过 curMin,24 永远无法进入 ans。
  3. 3Scene 3 · 核心状态:curMax / curMin / ans混淆 curMax 与 ans 是最常见的面试失误之一。 因为:ans 用 curMax 更新,但 curMax 本身每步重算。
  4. 4Scene 5 · 初始化首元素初始化三条滚动状态。 因为:ans 初值必须是 nums[0],不能是 0。
  5. 5Scene 6 · i=1 · x=+3ans 刷新!用 curMax 更新全局最优(不是把 curMax 当最终答案)。 因为:ans 只增不减,记录历史最优的「以 i 结尾」最大乘积。
  6. 6预测下一步先不要看下一帧——根据当前不变量,预测算法接下来会怎么动。 因为:主动预测会暴露你对不变量的真实理解,比被动看动画有效得多。
  7. 7Scene 8 · i=3 · x=+4三候选人竞争完毕,curMax/curMin 沿各自轨道延伸。 因为:每步 O(1) 更新,扫描一遍数组。
  8. 8收尾与复杂度最大乘积 6,最优子数组 [2,3]。 因为:时间 O(n) · 空间 O(1)。每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
动画 5 · 完整执行

从输入完整走到可观察结果

从主例第一帧运行到答案,时间轴始终显示当前状态、因果解释和下一步。

Step 1/80%
题目与输入建立输入、目标与算法心智
三候选人竞争 · 双轨乘积 DP

Scene 1 · 题意:连续子数组的最大乘积

ans · 全局最大
2
先理解题意,不急着写 DP
curMax · 以 i 结尾最大
curMin · 以 i 结尾最小
+2
i
+3
-2
+4
max 段 min 段
状态面板
i
0
x
+2
prevMax
prevMin
curMax · 以 i 结尾最大2
curMin · 以 i 结尾最小2
ans · 全局最大2
完整执行轨迹 · nums = [2, 3, -2, 4] · 4
ixprevMaxprevMincandidatescurMaxcurMinans
02222
1322{3, 6, 6}636
2-263{-2, -12, -6}-2-126
34-2-12{4, -8, -48}4-486
CodeTrace · Go · 双轨 max/min
1func maxProduct(nums []int) int {
2 curMax := nums[0]
3 curMin := nums[0]
4 ans := nums[0]
5
6 for i := 1; i < len(nums); i++ {
7 x := nums[i]
8 prevMax, prevMin := curMax, curMin
9 curMax = max3(x, prevMax*x, prevMin*x)
10 curMin = min3(x, prevMax*x, prevMin*x)
11 if curMax > ans {
12 ans = curMax
13 }
14 }
15
16 return ans
17}
09交互算法精讲

把动画和 Go 代码逐行对应

代码窗口不会按容易漂移的固定数字行号硬绑动画,而是根据当前语义阶段, 在完整 Go 实现中定位选择、计算、写回或返回分支。当前变量与高亮行一起变化。

动画 6 · 代码映射

让每个动作都落到 Go 分支

重新执行关键分支,只显示当前代码附近窗口,并说明这行为什么在此刻运行。

Step 1/60%
Scene 5 · 初始化第一格只能自成一段,三个状态相同。ans 不能初值为 0(全负数会错)。
三候选人竞争 · 双轨乘积 DP

Scene 5 · 初始化

ans · 全局最大
2
curMax · 以 i 结尾最大
curMin · 以 i 结尾最小
+2
i
+3
-2
+4
2
2
max 段 [2]min 段 [2]
状态面板
i
0
x
+2
prevMax
prevMin
curMax · 以 i 结尾最大2
curMin · 以 i 结尾最小2
ans · 全局最大2
当前最优段 → [2]
curMax

以当前位置 i 结尾的最大乘积

curMin

以当前位置 i 结尾的最小乘积

ans

遍历过程中见过的全局最大乘积(不是 curMax!)

递推公式
curMax = max(x, prevMax × x, prevMin × x)
curMin = min(x, prevMax × x, prevMin × x)
ans = max(ans, curMax)

关键:先保存 prevMax/prevMin,再更新 curMax/curMin——否则 prevMax 被污染。

完整执行轨迹 · nums = [2, 3, -2, 4] · 1
ixprevMaxprevMincandidatescurMaxcurMinans
02222
CodeTrace · Go · 双轨 max/min
1func maxProduct(nums []int) int {
2 curMax := nums[0]
3 curMin := nums[0]
4 ans := nums[0]
5
6 for i := 1; i < len(nums); i++ {
7 x := nums[i]
8 prevMax, prevMin := curMax, curMin
9 curMax = max3(x, prevMax*x, prevMin*x)
10 curMin = min3(x, prevMax*x, prevMin*x)
11 if curMax > ans {
12 ans = curMax
13 }
14 }
15
16 return ans
17}
10交互算法精讲

完整 Go 提交代码与最小测试

完整 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 许可代码整理,并按本站教学结构补充解释与动画映射。

11交互算法精讲

正确性与复杂度

时间复杂度 O(n)

执行过程中只保留仍可能影响答案的状态。每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。

空间复杂度 O(1)

额外状态主要用于维护:curMax(以 i 结尾最大)、curMin(以 i 结尾最小)、ans(全局最大乘积)。

终局不变量
  • curMax/curMin 是以 i 结尾的局部状态;ans 是全局答案,只增不减。
  • 必须先保存 prevMax/prevMin,再更新 curMax/curMin,顺序不能乱。
12交互算法精讲

最容易写错的地方

错误 1

只维护 curMax:负×负时 prevMin×x 可反杀成最大,会漏掉最优解(如 [-2,3,-4] 漏 24)。

错误 2

忘记 curMin:无法记录历史最小乘积,负数翻转时 curMax 来源丢失。

错误 3

把 curMax 当成全局答案:curMax 每步重算,必须用 ans = max(ans, curMax) 记录全局最优。

错误 4

没处理 0:0 切断乘积链,三候选皆 0,必须从当前格重启(如 [-2,0,-1] 答案是 0)。

错误 5

更新顺序错误:先算 curMax 再算 curMin 会污染 prevMax,必须 oldMax, oldMin := curMax, curMin 先保存。

边界复查

必须额外检查空输入、单元素、未命中或不可达情况,以及恰好落在边界的输入。

13交互算法精讲

最后复盘:带走逻辑链

  1. 1题意nums=[2,3,-2,4],求乘积最大子数组。
  2. 2重复直接递归会反复求同一个子问题;动态规划把答案写入状态表并按依赖顺序复用。
  3. 3优化每步从 x、prevMax×x、prevMin×x 三候选更新 curMax/curMin,再用 curMax 刷新全局 ans。
  4. 4证明curMax/curMin 是以 i 结尾的局部状态;ans 是全局答案,只增不减。;必须先保存 prevMax/prevMin,再更新 curMax/curMin,顺序不能乱。
  5. 5复杂度时间 O(n),空间 O(1)
面试表达:这题和最大子数组和不同,乘积遇到负数会发生符号翻转,所以当前位置的最大乘积可能来自之前的最小乘积。遍历时维护以 i 结尾的最大乘积 curMax 和最小乘积 curMin,每一步从 x、prevMax×x、prevMin×x 三者中更新,再用 curMax 更新全局答案 ans。时间 O(n),空间 O(1)。
迁移练习
  • LC53 最大和
  • LC713 乘积小于 K