1.题目链接:
给定一个长度为 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. 解法(动态规划):
算法思路:
本题与「最大子数组和」的区别在于,考虑问题的时候不仅要分析「数组内的连续区域」,还要考 虑「数组首尾相连」的一部分。结果的可能情况分为以下两种:
i. 结果在数组的内部,包括整个数组;
ii. 结果在数组首尾相连的一部分上。
其中,对于第一种情况,我们仅需按照「最大子数组和」的求法就可以得到结果,记为 fmax 。对于第二种情况,我们可以分析一下:
i. | 如果数组首尾相连的一部分是最大的数组和,那么数组中间就会空出来一部分; |
ii. 因为数组的总和 sum 是不变的,那么中间连续的一部分的和一定是最小的;因此,我们就可以得出一个结论,对于第二种情况的最大和,应该等于 sum - gmin ,其中 gmin 表示数组内的「最小子数组和」。
两种情况下的最大值,就是我们要的结果。
但是,由于数组内有可能全部都是负数,第一种情况下的结果是数组内的最大值(是个负数),第二种情况下的 gmin == sum ,求的得结果就会是 0 。若直接求两者的最大值,就会是 0 。但是实际的结果应该是数组内的最大值。对于这种情况,我们需要特殊判断一下。
由于「最大子数组和」的方法已经讲过,这里只提一下「最小子数组和」的求解过程,其实与「最 大子数组和」的求法是一致的。用 f 表示最大和,g 表示最小和。
1. 状态表示:
g[i] 表示:以 i 做结尾的「所有子数组」中和的最小值。
2. 状态转移方程:
g[i] 的所有可能可以分为以下两种:
i. | 子数组的长度为 1 :此时 g[i] = nums[i] ; |
ii. 子数组的长度大于 1 :此时 g[i] 应该等于 以 i - 1 做结尾的「所有子数组」中和的 最小值再加上 nums[i] ,也就是 g[i - 1] + nums[i] 。
由于我们要的是最小子数组和,因此应该是两种情况下的最小值,因此可得转移方程:
g[i] = min(nums[i], g[i - 1] + nums[i]) 。
3. 初始化:
可以在最前面加上一个辅助结点,帮助我们初始化。使用这种技巧要注意两个点: i. 辅助结点里面的值要保证后续填表是正确的;
ii. 下标的映射关系。
在本题中,最前面加上一个格子,并且让 g[0] = 0 即可。
4. 填表顺序:
根据状态转移方程易得,填表顺序为「从左往右」。
5. 返回值:
a. 先找到 f 表里面的最大值 -> fmax ;
b. 找到 g 表里面的最小值 -> gmin ;
c. 统计所有元素的和 -> sum ;
b. 返回 sum == gmin ? fmax : max(fmax, sum - gmin) 。
Java算法代码:
class Solution {
public int maxSubarraySumCircular(int[] nums) {
// 1.创建dp表
// 2.初始化
// 3.填表
// 4.返回值
int n = nums.length;
int[] f = new int[n+1];
int[] g = new int[n+1];
int sum = 0,fmax= Integer.MIN_VALUE,gmin=Integer.MAX_VALUE;
for(int i = 1; i <= n; i++){
int x = nums[i-1];
f[i] = Math.max(x,x + f[i-1]);
fmax = Math.max(fmax,f[i]);
g[i] = Math.min(x,x + g[i-1]);
gmin = Math.min(gmin,g[i]);
sum +=x;
}
return sum == gmin ? fmax : Math.max(fmax,sum - gmin);
}
}
执行结果:
动态规划: