LeetCode 53|最大子数组和:从分治到动态规划的完整推导

64 阅读4分钟

最大子数组和是一道非常经典的题。
经典到什么程度?
它几乎是:

  • 分治思想的标准示例
  • 动态规划入门必刷题
  • 后续连续区间问题的“母题”

这篇笔记不只给答案,而是完整走一遍思路升级的过程


一、题目要求

给定一个整数数组 nums,找到一个连续子数组(至少包含一个元素),使其元素和最大,返回这个最大和。

示例:

输入:[-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:[4,-1,2,1] 的和最大

关键约束:

  • 子数组必须连续
  • 数组中可能全部是负数

二、从最直观的思考开始

如果不考虑效率,最直接的想法是:

  • 枚举所有子数组
  • 计算它们的和
  • 取最大值

时间复杂度是 O(n²),显然不可取。

那有没有一种方式,在结构上拆解这个问题

答案是:有,分治。


三、分治法:区间三分的经典思想

核心思想

对于任意一个区间 [left, right],最大子数组只可能来自三种情况:

  1. 完全在左半区间
  2. 完全在右半区间
  3. 横跨中点 mid

这就是分治的基础。


分治函数定义

getMax(nums, left, right)

含义:

返回 nums[left..right] 区间内的最大子数组和


递归终止条件

if (left == right) {
    return nums[left];
}

区间里只有一个数,答案只能是它本身。


分治完整代码

class Solution {
    public int maxSubArray(int[] nums) {
        return getMax(nums, 0, nums.length - 1);
    }

    public int getMax(int[] nums, int left, int right) {
        if (left == right) {
            return nums[left];
        }

        int mid = left + (right - left) / 2;

        int leftMax = getMax(nums, left, mid);
        int rightMax = getMax(nums, mid + 1, right);
        int crossMax = getCrossMax(nums, left, right);

        return Math.max(leftMax, Math.max(rightMax, crossMax));
    }

    public int getCrossMax(int[] nums, int left, int right) {
        int mid = left + (right - left) / 2;

        int leftSum = nums[mid];
        int leftMax = leftSum;
        for (int i = mid - 1; i >= left; i--) {
            leftSum += nums[i];
            leftMax = Math.max(leftSum, leftMax);
        }

        int rightSum = nums[mid + 1];
        int rightMax = rightSum;
        for (int i = mid + 2; i <= right; i++) {
            rightSum += nums[i];
            rightMax = Math.max(rightSum, rightMax);
        }

        return leftMax + rightMax;
    }
}

分治法在做什么?

  • 左递归:左区间的最大子数组
  • 右递归:右区间的最大子数组
  • 中间扫描:以 mid 为分界的最大跨区间子数组

这是一个逻辑非常严谨、思想非常漂亮的解法。


分治法的代价

时间复杂度:

T(n) = 2T(n/2) + O(n) = O(n log n)

这已经不错了,但还能更好。


四、动态规划:从“区间”转向“当前位置”

换一个角度思考:

如果我已经走到位置 i,
那“以 i 结尾的最大子数组和”是多少?

这就是动态规划的切入点。


状态定义

dp[i] 表示:以 nums[i] 结尾的最大子数组和

注意:
必须 以 i 结尾,这是关键。


状态转移方程

来到第 i 个元素时,只有两种选择:

  1. 把 nums[i] 接在前一个子数组后面
  2. 从 nums[i] 自己重新开始

公式就是:

dp[i] = max(dp[i - 1] + nums[i], nums[i])

五、动态规划标准写法(使用 dp 数组)

完整代码

class Solution {
    public int maxSubArray(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = nums[0];

        int res = nums[0];

        for (int i = 1; i < nums.length; i++) {
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
            res = Math.max(res, dp[i]);
        }

        return res;
    }
}

逐行理解

  • dp[i - 1] + nums[i]
    表示“之前的子数组是正收益,继续扩展”
  • nums[i]
    表示“之前是负担,直接丢掉,从当前重新开始”

六、动态规划的空间优化

观察转移方程可以发现:

dp[i] 只依赖 dp[i - 1]

所以数组是可以压缩的。


空间优化版代码

class Solution {
    public int maxSubArray(int[] nums) {
        int preMax = nums[0];
        int res = preMax;

        for (int i = 1; i < nums.length; i++) {
            int curMax = Math.max(nums[i], preMax + nums[i]);
            res = Math.max(res, curMax);
            preMax = curMax;
        }

        return res;
    }
}

这一步优化的本质

  • preMax 相当于 dp[i - 1]
  • curMax 相当于 dp[i]
  • 状态没有变,只是存储方式变了

七、三种思路的对比

解法时间复杂度空间复杂度作用
分治法O(n log n)O(log n)理解区间思想
DP 数组O(n)O(n)推导状态转移
DP 优化O(n)O(1)实战首选

八、总结

这道题非常适合用来建立一个清晰认知:

  • 分治告诉你:
    答案可以从区间结构中拆出来
  • 动态规划告诉你:
    当前位置的选择只和上一步有关
  • 空间优化告诉你:
    不是所有状态都值得被保存

如果你能把这三种解法串起来理解,
后面再遇到连续子数组、区间最值、股票类问题,
你会发现它们本质上都在做同一件事。