《LeetCode 53 最大子数组和 || LeetCode 918 环形子数组的最大和》

一、题目

二、做题思路

2.1 状态表示(核心基础)

本题要求计算数组中连续子数组的最大和 。我们定义 dp[i] 表示以 nums[i] 结尾的连续子数组的最大和

2.2 状态转移方程(关键难点)

对于以 nums[i] 结尾的子数组,有两种可能:

  • nums[i] 接在前一个子数组后面 ,即 dp[i-1] + nums[i]

  • 重新开始一个新的子数组 ,即仅包含 nums[i] 本身。

    为了获得最大和,我们取两者中的较大值:

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

2.3 初始化(边界防护)

第一个元素(下标 0)没有前驱子数组,因此 dp[0] = nums[0]

2.4 填表顺序(递推方向)

dp[i] 仅依赖 dp[i-1],因此必须从左到右 (即 i 从 1 到 n-1)依次填充 dp 表,确保每个状态计算时,其前置状态已就绪

2.5 返回值(目标映射)

题目要求返回整个数组的最大子数组和 ,即所有以各位置结尾的子数组和中的最大值:max(dp[0], dp[1], ..., dp[n-1]) 。可在递推过程中同步维护一个变量 ret 记录最大值,最终返回 ret

三、代码

cpp 复制代码
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        // 边界处理:若数组为空,返回0(题目通常保证非空,但为健壮性加上)
        if (n == 0) return 0;

        // 1. 创建dp表
        // dp[i] 表示以 nums[i] 结尾的连续子数组的最大和
        vector<int> dp(n);

        // 2. 初始化:第一个元素结尾的最大和就是它本身
        dp[0] = nums[0];

        // 3. 填表顺序:从左到右(i 从 1 到 n-1)
        for (int i = 1; i < n; i++) {
            // 4. 状态转移方程:
            // 要么将当前元素 nums[i] 加入到前一个子数组(dp[i-1] + nums[i]),
            // 要么重新开始一个新的子数组(仅包含当前元素 nums[i]),取两者较大值
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);
        }

        // 5. 返回值:所有 dp[i] 中的最大值,即为整个数组的最大子数组和
        int ret = -0x3f3f3f3f; // 初始化为极小值
        for (int i = 0; i < n; i++) {
            ret = max(ret, dp[i]);
        }
        return ret;

        // 优化:可以在循环中同时维护最大值,避免最后的遍历
        // int ret = dp[0];
        // for (int i = 1; i < n; i++) {
        //     dp[i] = max(dp[i-1] + nums[i], nums[i]);
        //     ret = max(ret, dp[i]);
        // }
        // return ret;
    }
};

四、流程图

五、题目

六、做题思路

6.1 状态表示(核心基础)

本题要求计算环形数组中非空子数组的最大和 。环形数组意味着答案可能来自跨越首尾的连续子数组,也可能是普通线性子数组。为同时处理两种情况,我们定义两类状态:

  • f[i] 表示以 nums[i] 结尾的线性(非环形)最大子数组和

  • g[i] 表示以 nums[i] 结尾的线性(非环形)最小子数组和

6.2 状态转移方程(关键难点)

对于线性子数组,转移与"最大子数组和"完全一致:

  • f[i] = max(nums[i], f[i-1] + nums[i])(取延续之前或重新开始的最大值)。

  • g[i] = min(nums[i], g[i-1] + nums[i])(取延续之前或重新开始的最小值)。

6.3 初始化(边界防护)

第一个元素(下标 0)没有前驱,因此 f[0] = g[0] = nums[0]

6.4 填表顺序(递推方向)

每个 i 状态仅依赖 i-1,因此必须从左到右 (即 i 从 1 到 n-1)依次填充两个数组,确保每个状态计算时,其所有前置状态均已就绪

6.5 返回值(目标映射)

环形最大子数组和有两种可能:

  • 普通线性最大 :即 max(f[i])

  • 跨越首尾 :等价于总和减去线性最小子数组和 (即 total - min(g[i])),因为去掉中间一段最小和,剩下的就是环形子数组。

    但需注意特殊边界:若数组中所有元素均为负数 (此时 total == min(g[i])),则 total - min(g[i]) = 0,表示空子数组,不符合"非空"要求,此时应返回普通线性最大。因此最终返回 max(普通最大, 总和 - 普通最小),若总和等于最小子数组和,则直接返回普通最大

七、代码

cpp 复制代码
class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums) {
        const int INF = 0x3f3f3f3f; // 定义一个足够大的数,用于表示正负无穷
        int n = nums.size();

        // 1. 创建dp表
        // f[i] : 以 nums[i] 结尾的(非环形)最大子数组和
        // g[i] : 以 nums[i] 结尾的(非环形)最小子数组和
        vector<int> f(n);
        vector<int> g(n);

        // 2. 初始化:第一个元素结尾的最大和最小都等于 nums[0]
        f[0] = g[0] = nums[0];

        // 3. 填表顺序:从左到右(i 从 1 到 n-1),因为只依赖前一个状态
        for (int i = 1; i < n; i++) {
            // 4. 状态转移方程:
            // 最大子数组和:要么延续之前的子数组(f[i-1]+nums[i]),要么重新开始(nums[i]),取较大
            f[i] = max(nums[i], f[i - 1] + nums[i]);
            // 最小子数组和:要么延续之前的子数组(g[i-1]+nums[i]),要么重新开始(nums[i]),取较小
            g[i] = min(nums[i], g[i - 1] + nums[i]);
        }

        // 5. 计算非环形最大子数组和(普通情况)
        int maxNormal = -INF;
        for (int i = 0; i < n; i++) 
        {
            maxNormal = max(maxNormal, f[i]);
        }

        // 计算非环形最小子数组和(用于环形情况的补集)
        int minNormal = INF;
        for (int i = 0; i < n; i++) 
        {
            minNormal = min(minNormal, g[i]);
        }

        // 计算数组总和
        int total = 0;
        for (int num : nums) {
            total += num;
        }

        // 环形最大子数组和 = max(非环形最大, 总和 - 非环形最小)
        // 但若数组全为负数,则"总和 - 非环形最小"会等于0(因为最小子数组和等于总和),
        // 此时环形方案会取空子数组(不允许),所以应返回非环形最大(即最大的负数)。
        if (total == minNormal) 
        {
            // 全为负数(或全为非正且最小子数组就是整个数组),返回非环形最大
            return maxNormal;
        } else 
        {
            return max(maxNormal, total - minNormal);
        }
    }
};

八、流程图

相关推荐
变量未定义~1 小时前
单调栈+倍增思想 皇家守卫【算法赛】、单调队列 附近最小
算法
QN1幻化引擎1 小时前
给 AI 做一次「意识体检」——基于 QN1 幻化引擎的灵鉴意识识别框架与 DalinX V5 实测
大数据·数据结构·人工智能·算法·架构
拂拉氏2 小时前
【知识讲解】 AVL树从基本成员的介绍到核心接口的实现(插入、判断、删除等等)
数据结构·算法·avl树
可靠的仙人掌2 小时前
SAC(Soft Actor-Critic)算法底座
开发语言·算法·php
王老师青少年编程3 小时前
csp信奥赛C++高频考点专项训练:【二分答案】案例2:木材加工
c++·二分答案·csp·高频考点·信奥赛·木材加工
海石4 小时前
单调栈复健,顺便,牺牲一下吧,空间复杂度!一切献给AC
算法·leetcode
海石4 小时前
JS击败94%,Hard题想不到动态规划,那就用数组和栈试试
算法·leetcode
aaPIXa6224 小时前
C++模板元编程:编译期计算Fibonacci数列
java·开发语言·c++
Augustzero4 小时前
`co_await` 按下暂停键之后:从零看懂 C++20 协程
c++·后端