题目描述
思路解析
一句话答案:LeetCode 152 乘积最大子数组的标准解是双状态动态规划:同时维护「以当前元素结尾」的最大乘积 imax 和最小乘积 imin。因为负负得正,眼下最小(最负)的乘积乘一个负数就可能翻身成最大,所以遇到负数先交换 imax 与 imin 再转移。一遍扫描即可,时间 O(n)、空间 O(1)。
乘积最大子数组难在哪
在数组 nums 里找一段连续子数组,使所有元素的乘积最大,返回这个乘积。数组里混着正数和负数(还可能有 0),这正是难点:加法世界里「越加越大」的直觉,在乘法世界被负号打碎——一个负数把大变小、把小变大,两个负数相乘又翻回正。比如 nums = [2,3,-2,4,-1,2,1,-5,4,3] 的答案是 480,靠的恰恰是负数之间的配合。
为什么照搬最大子数组和的套路会漏解
这题长得很像 LeetCode 53 最大子数组和,那题只维护一个状态:以当前元素结尾的最大和。照搬到乘法上——只维护「以当前元素结尾的最大乘积」——会立刻漏解:一段乘积是 -48 的前缀在「最大」的眼里一文不值,早被丢弃;可它后面若跟着一个 -1,乘起来就是 48,反而是全场希望最大的种子。
关键观察由此而来:乘法里「当前最小」和「当前最大」同样有前途,一个负数就能让两者互换身份。所以状态必须成对维护——最大乘积 imax 和最小乘积 imin 缺一不可,让最负的那条线也活着,等负数来接它翻身。
imax 和 imin 两个状态怎么定义
定义 imax 为「以当前元素结尾的连续子数组的最大乘积」,imin 为同一口径下的最小乘积,二者都强制「以当前元素结尾」——这保证子数组连续,也让下一个元素能直接接在后面。全局答案 ans 则是历史上所有 imax 中的最大者,因为最优子数组总得以某个位置收尾,逐位收集就不会错过。三个变量都用 nums[0] 初始化,从第二个元素起逐个转移。
遇到负数为什么要先交换再转移
对每个新元素 x,以 x 结尾的子数组只有两种形态:x 自己另起一段,或接在前一段后面。所以 imax = max(x, imax * x)、imin = min(x, imin * x)。但若 x 是负数,乘法会把大小关系整体翻转——原来最大的乘完变最小,原来最小的乘完变最大。先把 imax 与 imin 交换再套同一组转移式,等价于让 x 去乘「乘完它之后会变大的那一条」,两条候选线都不丢。交换必须发生在转移之前,写反了两个状态全错。
「和 x 本身比」这一半也不可省:遇到 0 或前段乘积方向不利时,max(x, imax * x) 允许从 x 重新起段,把拖后腿的前缀干净地甩掉,0 自然成为分段点。
复杂度多少,最容易错在哪
每个元素只做常数次比较和乘法,一遍扫描,时间 O(n);全程只有 imax、imin、ans 三个变量,空间 O(1)。
三个高频翻车点复盘:只维护 imax 不维护 imin,负负得正的翻身仗直接打不了;转移时忘了和 x 本身比,被 0 或反号的前缀拖死;交换写在转移之后,负数一来两个状态同时污染。记住一句话就够了——加法只用记最大,乘法要最大最小成对记,遇负先换位。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
核心一句:同时维护 imax(最大)和 imin(最小)两个状态,缺一不可。
第 0 格只有它自己:以 nums[0] 结尾的最大、最小乘积都是 2。表三行:nums 固定,imax / imin 待填。
nums[1]=3 是正数(非负),不交换:最大还接最大,最小还接最小。
落子:imax=6、imin=3。两条路里——「自己单独成段」或「接上前一段」——接上前面那段(2×3)更大。答案更新到 6。
遇到负数 -2:负 × 负 = 正,所以先把 imax 和 imin 交换——原来最小的(最负的)乘上这个负数,最有希望变成新的最大。
落子:imax=-2、imin=-12。两条路里——「自己单独成段」或「接上前一段」——当前数 -2 自己另起一段更大。答案更新到 6。
nums[3]=4 是正数(非负),不交换:最大还接最大,最小还接最小。
落子:imax=4、imin=-48。两条路里——「自己单独成段」或「接上前一段」——当前数 4 自己另起一段更大。答案更新到 6。
遇到负数 -1:负 × 负 = 正,所以先把 imax 和 imin 交换——原来最小的(最负的)乘上这个负数,最有希望变成新的最大。
落子:imax=48、imin=-4。两条路里——「自己单独成段」或「接上前一段」——接上前面那段(-48×-1)更大。答案更新到 48。
nums[5]=2 是正数(非负),不交换:最大还接最大,最小还接最小。
落子:imax=96、imin=-8。两条路里——「自己单独成段」或「接上前一段」——接上前面那段(48×2)更大。答案更新到 96。
nums[6]=1 是正数(非负),不交换:最大还接最大,最小还接最小。
落子:imax=96、imin=-8。两条路里——「自己单独成段」或「接上前一段」——接上前面那段(96×1)更大。答案更新到 96。
遇到负数 -5:负 × 负 = 正,所以先把 imax 和 imin 交换——原来最小的(最负的)乘上这个负数,最有希望变成新的最大。
落子:imax=40、imin=-480。两条路里——「自己单独成段」或「接上前一段」——接上前面那段(-8×-5)更大。答案更新到 96。
nums[8]=4 是正数(非负),不交换:最大还接最大,最小还接最小。
落子:imax=160、imin=-1920。两条路里——「自己单独成段」或「接上前一段」——接上前面那段(40×4)更大。答案更新到 160。
nums[9]=3 是正数(非负),不交换:最大还接最大,最小还接最小。
落子:imax=480、imin=-5760。两条路里——「自己单独成段」或「接上前一段」——接上前面那段(160×3)更大。答案更新到 480。
扫完整张表,所有 imax 里最大的就是答案 480(出现在第 9 格)。负数被 imin 接住、再翻身,正是这题的精髓。
边界先想清:负数、0、全负。
两个高频追问。
参考代码
def maxProduct(nums): imax = imin = ans = nums[0] for x in nums[1:]: if x < 0: imax, imin = imin, imax imax = max(x, imax * x) imin = min(x, imin * x) ans = max(ans, imax) return ans复杂度
- 时间:O(n),一遍线性扫描
- 空间:O(1),只用 imax/imin/ans 三个变量
易错点
面试追问把动画讲成自己的话
追问为什么 max/min 都要和 x 本身比?
追问和「最大子数组和」(LC53) 区别?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
单词拆分
LeetCode 139 · 中等 · 沿着 一维动态规划 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题