LeetCode算法日记 - Day 82: 环形子数组的最大和
目录
1. 环形子数组的最大和
1.1 题目解析
1.2 解法
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.length1 <= 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 代码实现
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),只需维护前一个状态的值。
