LeetCode算法日记 - Day 82: 环形子数组的最大和

目录

[1. 环形子数组的最大和](#1. 环形子数组的最大和)

[1.1 题目解析](#1.1 题目解析)

[1.2 解法](#1.2 解法)

[1.3 代码实现](#1.3 代码实现)


1. 环形子数组的最大和

https://leetcode.cn/problems/maximum-sum-circular-subarray/

给定一个长度为 n环形整数数组 nums ,返回*nums 的非空 子数组 的最大可能和*。

环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n]nums[i] 的前一个元素是 nums[(i - 1 + n) % n]

子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n

示例 1:

复制代码
输入:nums = [1,-2,3,-2]
输出:3
解释:从子数组 [3] 得到最大和 3

示例 2:

复制代码
输入:nums = [5,-3,5]
输出:10
解释:从子数组 [5,5] 得到最大和 5 + 5 = 10

示例 3:

复制代码
输入:nums = [3,-2,2,-3]
输出:3
解释:从子数组 [3] 和 [3,-2,2] 都可以得到最大和 3

提示:

  • n == nums.length
  • 1 <= n <= 3 * 104
  • -3 * 104 <= nums[i] <= 3 * 104

1.1 题目解析

题目本质

在环形数组中寻找最大连续子数组和。环形意味着数组首尾相连,但子数组不能重复使用元素。

常规解法

用 动态规划 算法求最大子数组和。遍历数组,维护当前位置结尾的最大和,取全局最大值。

问题分析

普通 动态规划 算法只能处理线性数组,无法处理跨越边界的情况。

例如 [5,-3,5],最优解是选首尾 [5,5]=10,但线性算法只会算出 [5,-3,5]=7。环形数组的最大子数组要么在中间,要么跨越首尾,需要分情况讨论。

思路转折
跨边界的情况可以转换思维------选首尾等价于不选中间。
如果我们能找到中间的最小子数组和,用总和减去它就得到了跨边界的最大和。因此需要同时维护两个 DP:一个求最大子数组和(不跨边界),一个求最小子数组和(用于计算跨边界)。

1.2 解法

算法思想:动态规划分两种情况,f[i] 表示以 i 结尾的最大子数组和,g[i] 表示以 i 结尾的最小子数组和。

状态转移:

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

  • g[i] = min(g[i-1] + nums[i], nums[i])

**最终答案:**max(fmax, sum - gmin),其中 fmax 是不跨边界的最大值,sum - gmin 是跨边界的最大值。

**i)**初始化两个 DP 数组 f 和 g,分别用于记录最大和最小子数组和,初始值设为 0。

**ii)**第一次遍历计算数组总和 sum,这是计算跨边界情况的基础。

**iii)**第二次遍历同时更新 f[i] 和 g[i],维护全局最大值 max 和全局最小值 min。

**iv)**计算跨边界的最大和 tMin = sum - min。如果 tMin == 0 说明所有数都是负数,只能返回 max(最大的负数),否则返回 max(tMin, max)。

易错点

  • f 和 g 的含义理解

    • f[i] 表示以位置 i 结尾的最大子数组和,用于求不跨界情况。

    • g[i] 表示以位置 i 结尾的最小子数组和,用于计算跨界情况(通过 sum - gmin 得到跨界的最大值)。

  • 全负数的边界情况

    • 当数组全为负数时,gmin 会等于 sum(因为整个数组就是最小子数组),导致 sum - gmin = 0。

    • 这个 0 表示"什么都不选",但题目要求子数组非空,所以必须返回 fmax(即最大的那个负数)。判断条件是 tMin == 0 ? max : Math.max(tMin, max)。

  • max 和 min 的初始化:max 不能初始化为 0,必须是 Integer.MIN_VALUE/2,否则当所有数都是负数时,fmax 会被错误地判定为 0。同理,min 初始化为 Integer.MAX_VALUE/2。

  • 跨界计算的本质:sum - gmin 不是简单的数学运算,而是"总和减去不要的最小部分 = 保留的最大部分(跨边界)"。理解这个转换是关键,不要误以为是 sum + |gmin|。

1.3 代码实现

java 复制代码
class Solution {
    public int maxSubarraySumCircular(int[] nums) {
        int n = nums.length;
        int[] f = new int[n + 1];  // 最大子数组和
        int[] g = new int[n + 1];  // 最小子数组和
        f[0] = g[0] = 0;
        
        int max = Integer.MIN_VALUE / 2;
        int min = Integer.MAX_VALUE / 2;
        int sum = 0;
        
        // 计算总和
        for (int i = 0; i < n; i++) {
            sum += nums[i];
        }
        
        // 同时计算最大和最小子数组和
        for (int i = 1; i <= n; i++) {
            f[i] = Math.max(f[i - 1] + nums[i - 1], nums[i - 1]);
            max = Math.max(max, f[i]);
            
            g[i] = Math.min(g[i - 1] + nums[i - 1], nums[i - 1]);
            min = Math.min(min, g[i]);
        }
        
        // 跨边界的最大和 = 总和 - 最小子数组和
        int tMin = sum - min;
        
        // 全是负数时返回最大的负数
        return tMin == 0 ? max : Math.max(tMin, max);
    }
}

复杂度分析

  • **时间复杂度:O(n),**两次遍历数组。

  • **空间复杂度:O(n),**使用了两个 DP 数组。可优化至 O(1),只需维护前一个状态的值。

相关推荐
Code_Shark4 小时前
AtCoder Beginner Contest 426 题解
数据结构·c++·算法·数学建模·青少年编程
仰泳的熊猫4 小时前
LeetCode:698. 划分为k个相等的子集
数据结构·c++·算法·leetcode
豐儀麟阁贵4 小时前
4.5数组排序算法
java·开发语言·数据结构·算法·排序算法
Shinom1ya_4 小时前
算法 day 32
算法
Halo_tjn4 小时前
Java Map集合
java·开发语言·计算机
程序猿小蒜5 小时前
基于springboot的车辆管理系统设计与实现
java·数据库·spring boot·后端·spring·oracle
WBluuue5 小时前
数据结构与算法:摩尔投票算法
c++·算法·leetcode
zl9798996 小时前
SpringBoot-Web开发之Web原生组件注入
java·spring boot·spring
2401_858286116 小时前
OS36.【Linux】简单理解EXT2文件系统(2)
linux·运维·服务器·数据结构·文件系统·ext2