【LeetCode Hot100 刷题日记 (13/100)】53. 最大子数组和 —— 动态规划、数组、 分治法📌

54 阅读7分钟

📌 题目链接:leetcode.cn/problems/ma…
🔍 难度:中等 | 🏷️ 标签:数组、动态规划、分治
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)(动态规划解法)


题目分析

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

这是一个经典的「最优化」问题,要求在所有可能的连续子数组中找到和最大的那个。由于子数组必须是连续的,因此不能跳着选元素,也不能重新排序。

示例回顾:

  • 输入:[-2,1,-3,4,-1,2,1,-5,4] → 输出:6(对应子数组 [4,-1,2,1]
  • 输入:[1] → 输出:1
  • 输入:[5,4,-1,7,8] → 输出:23

💡 关键点:负数会“拖累”总和,但有时为了连接后面的正数,不得不暂时接受负数。如何权衡?——这就是动态规划要解决的问题。


核心算法及代码讲解

本题有两种主流解法:动态规划(推荐)分治法(进阶)。面试中最常考察的是动态规划,因其简洁高效;而分治法则体现了结构化思维,适用于扩展场景(如区间查询、支持修改等)。


✅ 方法一:动态规划(Kadane 算法)

这是由 Jay Kadane 提出的经典算法,专用于解决最大子数组和问题。

🧠 核心思想:

定义状态 f(i) 表示以第 i 个元素结尾的连续子数组的最大和

那么状态转移方程为:

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

解释:

  • 如果前面的子数组和 f(i-1) 是正的,加上当前元素 nums[i] 会让总和更大;
  • 如果 f(i-1) 是负的,不如从 nums[i] 重新开始一段新子数组。

最终答案就是所有 f(i) 中的最大值。

🚀 优化空间:

由于 f(i) 只依赖 f(i-1),我们不需要开数组,只需用一个变量 pre 记录前一个状态即可,实现 O(1) 空间

🔍 代码详解(含行注释):

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int pre = 0;                // pre 表示 f(i-1),即以上一个元素结尾的最大子数组和
        int maxAns = nums[0];       // 初始化答案为第一个元素(防止全负)
        for (const auto &x : nums) {
            pre = max(pre + x, x);      // 状态转移:要么接上前面,要么从 x 重新开始
            maxAns = max(maxAns, pre);  // 实时更新全局最大值
        }
        return maxAns;
    }
};

为什么初始化 maxAns = nums[0]
因为题目保证数组非空,且若全为负数(如 [-3,-1,-2]),最大子数组就是最大的那个负数,不能初始化为 0!


🌲 方法二:分治法(线段树思想雏形)

虽然动态规划更优,但分治法展示了如何将问题分解并合并结果,是理解线段树区间查询类问题的基础。

🧠 核心思想:

对区间 [l, r],递归处理左右两半,然后合并信息。我们需要维护四个值:

字段含义
iSum区间总和
lSum以左端点开头的最大子数组和
rSum以右端点结尾的最大子数组和
mSum区间内任意位置的最大子数组和(即所求)

🔁 合并规则(pushUp):

假设左子区间为 L,右子区间为 R

  • iSum = L.iSum + R.iSum
  • lSum = max(L.lSum, L.iSum + R.lSum)
  • rSum = max(R.rSum, R.iSum + L.rSum)
  • mSum = max(L.mSum, R.mSum, L.rSum + R.lSum)

💡 跨越中点的情况:最大子数组可能横跨左右,此时和为 L.rSum + R.lSum

🔍 代码详解(含行注释):

struct Status {
    int lSum, rSum, mSum, iSum;
};

Status pushUp(Status l, Status r) {
    int iSum = l.iSum + r.iSum;
    int lSum = max(l.lSum, l.iSum + r.lSum);     // 要么只取左边,要么左边全取+右边前缀
    int rSum = max(r.rSum, r.iSum + l.rSum);     // 要么只取右边,要么右边全取+左边后缀
    int mSum = max({l.mSum, r.mSum, l.rSum + r.lSum}); // 三种情况取最大
    return {lSum, rSum, mSum, iSum};
}

Status get(vector<int> &a, int l, int r) {
    if (l == r) {
        return {a[l], a[l], a[l], a[l]}; // 叶子节点:四个值都等于 a[l]
    }
    int m = (l + r) >> 1;
    Status lSub = get(a, l, m);
    Status rSub = get(a, m + 1, r);
    return pushUp(lSub, rSub);
}

int maxSubArray(vector<int>& nums) {
    return get(nums, 0, nums.size() - 1).mSum;
}

⚠️ 注意:C++11 以后 max({a,b,c}) 需要 <algorithm>,但 <bits/stdc++.h> 已包含。


解题思路(分步骤)

动态规划解法步骤:

  1. 初始化pre = 0(前一个子数组和),maxAns = nums[0](防止全负)。
  2. 遍历数组:对每个元素 x
    • 更新 pre = max(pre + x, x):决定是否延续前面的子数组。
    • 更新 maxAns = max(maxAns, pre):记录历史最大值。
  3. 返回结果maxAns 即为答案。

分治法步骤:

  1. 递归划分:将数组不断二分,直到单个元素。
  2. 回溯合并:从叶子向上合并,计算每个区间的四个关键值。
  3. 返回根节点的 mSum:即整个数组的最大子数组和。

算法分析

方法时间复杂度空间复杂度是否推荐面试价值
动态规划O(n)O(1)✅ 强烈推荐⭐⭐⭐⭐⭐
分治法O(n)O(log n)❌ 了解即可⭐⭐⭐(展示思维深度)

💬 面试官可能会问

  • “如果数组全是负数怎么办?” → 动态规划天然支持,因为每次 max(pre+x, x) 会保留最大负数。
  • “能处理空数组吗?” → 题目保证非空,但实际工程中需判空。
  • “这个算法叫什么?” → Kadane’s Algorithm,务必记住名字!

完整代码(保留原模板,含测试)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// ==================== 动态规划解法(主推) ====================
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int pre = 0, maxAns = nums[0];
        for (const auto &x: nums) {
            pre = max(pre + x, x);
            maxAns = max(maxAns, pre);
        }
        return maxAns;
    }
};

// ==================== 分治法(进阶) ====================
struct Status {
    int lSum, rSum, mSum, iSum;
};

Status pushUp(Status l, Status r) {
    int iSum = l.iSum + r.iSum;
    int lSum = max(l.lSum, l.iSum + r.lSum);
    int rSum = max(r.rSum, r.iSum + l.rSum);
    int mSum = max({l.mSum, r.mSum, l.rSum + r.lSum});
    return {lSum, rSum, mSum, iSum};
}

Status get(vector<int> &a, int l, int r) {
    if (l == r) {
        return {a[l], a[l], a[l], a[l]};
    }
    int m = (l + r) >> 1;
    Status lSub = get(a, l, m);
    Status rSub = get(a, m + 1, r);
    return pushUp(lSub, rSub);
}

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    vector<int> nums1 = {-2,1,-3,4,-1,2,1,-5,4};
    cout << "Test 1: " << sol.maxSubArray(nums1) << " (Expected: 6)" << endl;

    vector<int> nums2 = {1};
    cout << "Test 2: " << sol.maxSubArray(nums2) << " (Expected: 1)" << endl;

    vector<int> nums3 = {5,4,-1,7,8};
    cout << "Test 3: " << sol.maxSubArray(nums3) << " (Expected: 23)" << endl;

    // 分治法测试(可选)
    cout << "Divide & Conquer Test: " << get(nums1, 0, nums1.size()-1).mSum << endl;

    return 0;
}

💡 面试高频考点总结

  1. Kadane 算法 是最大子数组和的标准解法,必须手写无误。
  2. 状态定义 是动态规划的关键:f(i) 表示“以 i 结尾”的最大和,而非“到 i 为止”的最大和。
  3. 边界处理:全负数组的正确性验证。
  4. 空间优化:滚动变量替代数组,体现工程素养。
  5. 扩展问题
    • 返回子数组起止位置?
    • 求最大子数组乘积?(需同时记录最大最小值)
    • 支持修改数组元素后的多次查询?→ 引入线段树

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第 56 题 —— 合并区间(中等)

🔹 题目:给你一个二维整数数组 intervals,其中 intervals[i] = [start_i, end_i] 表示第 i 个区间的开始和结束。请你合并所有重叠的区间,并返回一个不重叠的区间列表,该列表需按起始点升序排列。

🔹 核心思路:先按起点排序,然后遍历合并相邻或重叠的区间。

🔹 考点:排序、贪心、区间合并、边界判断。

🔹 难度:中等,但非常常见于系统设计与调度问题中,是区间类问题的入门经典

💡 提示:不要暴力枚举所有区间组合!排序 + 贪心是关键!


📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!