题目描述
思路解析
一句话答案:LeetCode 53 最大子数组和的最优解是动态规划,也就是经典的 Kadane 算法:定义「必须以第 i 个数结尾」的最大和 cur,每到一个新数只做一次抉择——前面那段的和为正就接上、为负就丢掉另起炉灶,同时用 best 记录全程出现过的最大值。一遍扫描搞定,时间 O(n)、空间 O(1)。
最大子数组和这道题在问什么
给一个整数数组 nums(有正有负),找一个连续的子数组,至少包含一个元素,使它的元素和最大,返回这个和。「连续」是全部难度的来源:如果允许任选元素,把正数全挑出来就完了;正因为必须连成一片,才会出现「要不要为了后面的大正数,忍着带上中间几个负数」这种权衡。
为什么暴力枚举是 O(n²),慢在哪
暴力做法是枚举子数组的起点和终点,配合前缀和可以把每段的和算到 O(1),但起终点组合仍有约 n²/2 种,整体 O(n²)。慢的根源是重复权衡:以位置 i 结尾的各段和,与以 i+1 结尾的各段和之间只差一个元素,暴力却把它们当成不相干的问题从头算。如果「以 i 结尾的最优」能一步递推出「以 i+1 结尾的最优」,整个问题就只剩一趟线性扫描。
状态为什么要定义成「必须以第 i 个数结尾」
直接定义「前 i 个数里的最大子数组和」是递推不动的:那个最优段可能结束在前面任何位置,跟第 i+1 个数接不接得上说不清,新元素来了无从更新。加一个限制就通了——定义 dp[i] 为「所有以第 i 个数结尾的子数组」里的最大和。锚死了结尾,第 i+1 个数的处境立刻清晰:它要么接在前一段屁股后面,要么自己另起一段,只有这两种可能。
这个定义不会漏掉答案:任何子数组总得在某个位置结尾,把每个位置结尾的最优都算出来,全局最大值就藏在其中。所以最终答案是所有 dp[i] 里的最大值,而不是最后一个 dp——最优段完全可能在数组中间就结束了,示例 [-2,1,-3,4,-1,2,1,-5,4] 的答案 6 对应中段的 [4,-1,2,1],就不在末尾。
前面的和为负就丢掉,为什么是对的
转移写出来是 dp[i] = max(nums[i], dp[i-1] + nums[i]),白话就是「接上前段」与「另起炉灶」二选一。它的正确性一句话可以点透:前段能提供的全部价值就是它的和 dp[i-1]——这个和是正数,接上等于白赚一笔垫底分;是负数,接上只会把自己拖低,还不如从 nums[i] 从零开始。注意被丢弃的判据是「前段的和为负」,不是「前面出现过负数」:示例里 4 后面跟着 -1,因为当时累计和 4 - 1 = 3 仍是正的,继续带着走才吃到了后面的 2 和 1。
由于 dp[i] 只依赖 dp[i-1],连数组都不用开,两个变量就够:cur 滚动记「以当前数结尾的最大和」,best 记全程最大值。这就是 Kadane 算法的全部。
复杂度与最容易踩的边界
时间 O(n):数组只扫一遍,每步一次比较一次加法。空间 O(1):只有 cur、best 两个变量。
最经典的坑是 best 的初值:必须设成 nums[0],不能设 0。数组全是负数时,正确答案是其中最大的那个负数,初值 0 会让你错误地返回 0——而空子数组是不允许的,题目要求至少含一个元素。另一个坑前面已提:答案是扫描全程的最大值,别返回最后一步的 cur。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
关键直觉:如果前面那段的和已经是负的,接上它只会拖累自己,不如丢掉、从这个数重新开始。
上行是原数组 nums(固定),下行 dp 待填——dp[i] 是“到第 i 个数为止、必须以它结尾”的最大和。从最左开始。
只有第 0 个数,子数组只能是它自己:dp[0]=nums[0]=-2。
算 dp[1]:另起一段=nums[1]=1;接上前面=dp[0]+nums[1]=-2+1=-1。前面 dp[0]=-2 是负的,接上反而更小。
取较大的 1 填进 dp[1]。丢掉前面、从这个数另起更划算。
算 dp[2]:另起一段=nums[2]=-3;接上前面=dp[1]+nums[2]=1+-3=-2。前面 dp[1]=1 是正的,接上能加分。
取较大的 -2 填进 dp[2]。接上前面更划算。
算 dp[3]:另起一段=nums[3]=4;接上前面=dp[2]+nums[3]=-2+4=2。前面 dp[2]=-2 是负的,接上反而更小。
取较大的 4 填进 dp[3]。丢掉前面、从这个数另起更划算。
算 dp[4]:另起一段=nums[4]=-1;接上前面=dp[3]+nums[4]=4+-1=3。前面 dp[3]=4 是正的,接上能加分。
取较大的 3 填进 dp[4]。接上前面更划算。
算 dp[5]:另起一段=nums[5]=2;接上前面=dp[4]+nums[5]=3+2=5。前面 dp[4]=3 是正的,接上能加分。
取较大的 5 填进 dp[5]。接上前面更划算。
算 dp[6]:另起一段=nums[6]=1;接上前面=dp[5]+nums[6]=5+1=6。前面 dp[5]=5 是正的,接上能加分。
取较大的 6 填进 dp[6]。接上前面更划算。
算 dp[7]:另起一段=nums[7]=-5;接上前面=dp[6]+nums[7]=6+-5=1。前面 dp[6]=6 是正的,接上能加分。
取较大的 1 填进 dp[7]。接上前面更划算。
算 dp[8]:另起一段=nums[8]=4;接上前面=dp[7]+nums[8]=1+4=5。前面 dp[7]=1 是正的,接上能加分。
取较大的 5 填进 dp[8]。接上前面更划算。
dp 全部填好。答案不是最后一格——要在整行 dp 里横扫一遍找最大的那个,因为最优子数组可以在任何位置结尾。
整行扫下来,最大的是 dp[6]=6——这就是最大子数组和(对应子数组 [4,-1,2,1])。
边界先想清,尤其全负数的情况。
两个高频追问。
参考代码
def maxSubArray(nums): cur = best = nums[0] for x in nums[1:]: cur = max(x, cur + x) # 另起 or 接上 best = max(best, cur) return best复杂度
- 时间:O(n),一遍线性扫描
- 空间:O(1),两个滚动变量 cur、best
易错点
面试追问把动画讲成自己的话
追问如何同时返回这段子数组的起止下标?
追问为什么不用前缀和 + 暴力?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
跳跃游戏
LeetCode 55 · 中等 · 沿着 贪心 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题