009 · Maximum Subarray

algorithm
Published

April 27, 2026

My First Thoughts

初次看到这个题,很自然会把它想成:

用一个不定长的框,去框住 nums 里的某个连续区间,让这个区间的和尽量大。

因为题目要求的是连续子数组,所以第一反应往“区间”上靠,其实是很正常的。

而且它会让人想到前面的 Best Time to Buy and Sell Stock。那题里我们也是一边往右走,一边维护“到当前位置为止的最好状态”。

于是这里很容易先想到:

  • 右边界 right 一定要有,因为我们总得知道当前看到哪里了
  • 如果只记右边界,那每到一个新位置,都要重新计算前面很多区间和,显然太慢
  • 所以是不是还需要一个 left,表示当前认为比较好的左边界?

顺着这个想法,脑子里很容易出现类似这样的思路:

left = 0
best = nums[0]

for right in range(len(nums)):
    total = sum(nums[left:right + 1])

    if total >= best:
        best = total

        next_total = sum(nums[left + 1:right + 1])
        if next_total >= best:
            left += 1
            best = next_total

这个方向的直觉并不离谱。它其实已经抓到了两个点:

  • 这是一个连续区间问题
  • 不能每次都把所有组合从头算一遍

Why That Is Not Enough

问题出在:这题虽然看起来像“区间”题,但它不是一个适合标准滑动窗口的题。

原因是数组里有负数。

一旦有负数,窗口的和就不具备那种“边扩边缩、单调调整”的性质。也就是说,左边界不是像普通窗口题那样,一点一点往右挪就能稳定逼近答案。

更具体地说,前面的这一类想法有两个问题。

第一,反复计算区间和会很慢。

像这样:

sum(nums[left:right + 1])

如果每一轮都重新算一次,总复杂度最坏会接近 \(O(n^2)\)

第二,左边界不是“慢慢前进一步”就够了。

有时候真正正确的决定不是:

  • 把左边界往右挪一格

而是:

  • 直接把前面整段全部丢掉
  • 从当前位置重新开始

比如:

[-100, 4, -1, 2]

当看到后面的 4 时,前面的 -100 已经不是“需要微调的左边界”,而是应该被彻底舍弃的拖累。

所以这题真正关键的不是:

当前最优区间的左边界到底在哪?

而是:

以当前位置结尾的最大连续子数组和,是多少?

这个视角一换,题目就顺了。


Final Idea

定义一个状态 current,表示:

以当前位置结尾的最大连续子数组和

当我们走到 nums[i] 时,只有两种选择:

  1. nums[i] 自己单独开始一个新的子数组
  2. nums[i] 接到前面的连续子数组后面

所以状态转移就是:

current = max(nums[i], current + nums[i])

然后再用一个 best,记录遍历过程中出现过的最大值:

best = max(best, current)

这里其实已经把“左边界怎么处理”隐含进去了。

如果:

nums[i] > current + nums[i]

说明前面的累计和已经变成负担了。那就不要接着前面走,直接从当前值重新开始。

这等价于:左边界直接跳到当前位置。

所以这题虽然表面上像区间题,但真正高效的写法并不需要手动维护 left


Why It Works

current 保存的是“必须以当前位置结尾”的最佳答案。

那么到 nums[i] 时:

  • 如果前面的 current 是有帮助的,就让它加上 nums[i]
  • 如果前面的 current 是拖累,就直接丢掉,从 nums[i] 重新开始

因为任何一个以 i 结尾的连续子数组,都只可能来自这两种情况,所以这个转移是完整的。

best 记录的是所有位置上 current 的最大值,也就是整个数组中的最大连续子数组和。


Code

def max_sub_array(nums: list[int]) -> int:
    current = nums[0]
    best = nums[0]

    for i in range(1, len(nums)):
        current = max(nums[i], current + nums[i])
        best = max(best, current)

    return best

Complexity

Time \(O(n)\) — 从左到右遍历一次数组
Space \(O(1)\) — 只维护了 currentbest 两个变量

Takeaway

这题最容易卡住的地方,是一直盯着“区间左边界怎么移动”。但它真正适合维护的不是边界,而是“以当前位置结尾的最大和”。一旦前面的累计和变成负担,就直接从当前位置重新开始。这也是很多动态规划题里很常见的一种状态设计方式。


Quiz