LEETCODE 053Medium
最大子数组和
把状态定义成“以 i 结尾的最大和”,转移就只剩一个选择:前面的和是正资产就带上,是负资产就抛弃重开。
问题拆解
找出数组中和最大的连续子数组,返回它的和。暴力枚举所有子数组是 O(n²),即便用前缀和优化掉求和的开销,枚举本身还是平方级。
直接定义 dp[i] 为“前 i 个元素的最大子数组和”会发现推不动:dp[i-1] 对应的子数组不知道结束在哪里,没法判断 nums[i] 能不能接上去。换一个定义就通了——dp[i] 表示以 nums[i] 结尾的最大子数组和。结尾固定后,选择只剩一个:nums[i] 要么接在以 nums[i-1] 结尾的子数组后面,要么另起炉灶自己单干。
dp[i] = max(dp[i-1] + nums[i], nums[i])
两者取谁,就看 dp[i-1] 是不是正的:前面攒下的是正资产就带上,是负资产就丢掉。最终答案是所有 dp[i] 里的最大值——最大子数组总得以某个位置结尾。
当“前 i 个的最优解”推不出转移时,试试收紧成“以 i 结尾的最优解”:加强的约束反而让转移变得唯一。
Kadane:滚动成一个变量
dp[i] 只依赖 dp[i-1],整张表可以压成一个变量 cur,同时用 best 记录历史最大值。
public int maxSubArray(int[] nums) {
int cur = nums[0]; // 以当前位置结尾的最大和
int best = nums[0]; // 全局最大
for (int i = 1; i < nums.length; i++) {
cur = Math.max(cur + nums[i], nums[i]); // 接上前段 or 另起炉灶
best = Math.max(best, cur);
}
return best;
}
def maxSubArray(nums: list[int]) -> int:
cur = best = nums[0] # cur:以当前位置结尾的最大和
for x in nums[1:]:
cur = max(cur + x, x) # 接上前段 or 另起炉灶
best = max(best, cur)
return best
func maxSubArray(nums []int) int {
cur, best := nums[0], nums[0] // cur:以当前位置结尾的最大和
for _, x := range nums[1:] {
cur = max(cur+x, x) // 接上前段 or 另起炉灶
best = max(best, cur)
}
return best
}
pub fn max_sub_array(nums: Vec<i32>) -> i32 {
let mut cur = nums[0]; // 以当前位置结尾的最大和
let mut best = nums[0];
for &x in &nums[1..] {
cur = (cur + x).max(x); // 接上前段 or 另起炉灶
best = best.max(cur);
}
best
}
两个变量的初值都设成 nums[0] 而不是 0——这是本题最常见的错误来源。数组可能全是负数(如 [-3, -1, -2],答案是 -1),如果 best 初始化为 0,会错误地返回 0,对应了一个题目不允许的“空子数组”。同理,cur = max(cur + x, x) 中的第二个选项是 x 本身而非 0:另起炉灶也必须把当前元素收进来,子数组不能为空。
复杂度
| 指标 | 复杂度 | 原因 |
|---|---|---|
| 时间 | O(n) |
单次遍历,每个元素做一次 max |
| 空间 | O(1) |
dp 数组滚动成两个变量 |
可以迁移的模式
- “最大子数组/子串”类问题的状态定义首选“以 i 结尾”,乘积最大子数组(152)、最长递增子序列(300)同源;
- 初值和边界要用“全负数组”来自检,凡是不允许空区间的题,0 都不是安全的初值;
- 转移只依赖前一项的 DP,一律可以滚动成常数空间。
Kadane 算法五行写完,但“换个状态定义让转移成立”这个动作,比代码本身值钱得多。