给定一个含有若干个正整数的数组,和一个正整数target
,找出该数组中,满足其和>= target
且长度最小的连续子数组,返回其长度。
又是连续子数组,又是和,首先想到前缀和。其次就是求一个连续区间,而由于全都是正整数,和具有单调递增性,那么想到滑动窗口。
(可以先想暴力法,两层循环,外层循环将每个位置j
,作为子数组的右边界,内层循环枚举所有子数组的左边界i ∈ [0,j]
,求解最大的连续和。可以观察到有单调性,就是内层循环的指针,和外层循环的指针,只会向着同一个方向走。若[i,j]
这个区间的和是满足>= target
,那么对于以j + 1
(所有大于j
的都一样)作为结尾的子数组,其左边界不可能取到i
的左边)
所以直接用滑动窗口来做。
class Solution {
public int minSubArrayLen(int target, int[] nums) {
// 前缀和 + 滑动窗口
int[] preSum = new int[nums.length + 1];
for (int i = 1; i <= nums.length; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
// 从第一个位置开始滑动, l窗口左边界, r窗口右边界
int l = 1, r = 1, ans = Integer.MAX_VALUE;
while (r <= nums.length) {
while (preSum[r] - preSum[l - 1] >= target) {
// 当前窗口的和大于等于target, 可尝试缩小窗口
ans = Math.min(ans, r - l + 1); // 更新答案
l++; // 左端点可以往右移动
}
r++;
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
}
可以优化为 O ( 1 ) O(1) O(1)的空间复杂度
class Solution {
public int minSubArrayLen(int target, int[] nums) {
// 前缀和 + 滑动窗口
// 从第一个位置开始滑动
int l = 0, r = 0, ans = Integer.MAX_VALUE;
int sum = 0;
while (r < nums.length) {
sum += nums[r]; // 当前位置先纳入窗口
while (sum >= target) {
ans = Math.min(ans, r - l + 1); // 更新答案
sum -= nums[l];
l++; // 左端点可以往右移动
}
r++;
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
}
如果数组中存在负数怎么办?
由于负数对和的贡献是负,只会把和拉低,并且纳入负数进来,还会增大子数组的长度。这种吃力不讨好的事,肯定是不会做的。也就是说,作为答案的子数组中,不会包含负数。所以只需要根据负数,把数组切开,分别对其余部分用上面的方式求解即可。
事实证明,这种想法是有问题的,作为答案的子数组中,是可能包含负数的。比如[3,4,-1,5,2]
,target = 8
,如果分别对负数两边进行求解,答案是0
(找不到满足条件的子数组),但实际应该是3
,中间的[4,-1,5]
。
推了一下,上面的代码,原封不动就能解决[4,-1,5]
这个用例。
那么在引入负数时,原先的解法是否不需要改动呢?答案是否定的。比如这个用例
[-1,-1,8]
,target=5
,答案应该是1
原先的做法中,滑动窗口的左边界l
没有机会往右移,导致计算出错误答案3
那么只需要,在滑动窗口的过程中,判断左边界l
,当左边界的数是负数时,则将其右移(l++
),来提高sum
,并减少子数组长度。
即。在滑动窗口的循环过程中,增加一个while
循环,当l <= r
并且nums[l] < 0
时,进行l++
,并判断是否满足条件
class Solution {
public int minSubArrayLen(int target, int[] nums) {
// 前缀和 + 滑动窗口
int[] preSum = new int[nums.length + 1];
for (int i = 1; i <= nums.length; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
// 从第一个位置开始滑动
int l = 1, r = 1, ans = Integer.MAX_VALUE;
while (r <= nums.length) {
while (preSum[r] - preSum[l - 1] >= target) {
ans = Math.min(ans, r - l + 1); // 更新答案
l++; // 左端点可以往右移动
}
// 当左边界是负数时, 将滑动窗口的左边界右移
while (l <= r && nums[l] < 0) {
l++;
if (preSum[r] - preSum[l - 1] >= target) ans = Math.min(ans, r - l + 1);
}
r++;
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
}
但实际这样的做法也不行。比如负数位于窗口的中间,窗口边界是正数。比如下面这个用例
[1,1,-9,3,4]
,target=5
左边界l
被数字1
挡住了,无法往右移动,导致-9
一直位于窗口中。
为了解决上面的负数位于中间,被左侧的正数挡住,而导致窗口左边界l
无法右移的情况。经过和朋友反复的讨论和列举特殊的测试用例,我们发现:
对于位置i
,我们期望知道,在i
左侧的最大连续和,是否是负数,若是负数,说明左侧一段连续区间,对和的贡献为负,则可以将左边界往右移动,跨过这段对和的贡献为负的区间。从而来增加当前子数组的和,那么对于i
我们判断一下i-1
的最大连续和,若为负,则右移l
直接到当前位置(l = r
)
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int[] preSum = new int[nums.length + 1]; // 前缀和
int[] f = new int[nums.length + 1]; // f[i] 表示以 i 结尾的最大连续和, 求解f的过程有点动态规划的感觉
int ans = Integer.MAX_VALUE;
// 预处理
for (int i = 1; i <= nums.length; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1]; // 前缀和
f[i] = nums[i - 1];
if (f[i - 1] > 0) f[i] += f[i - 1];
}
int l = 1, r = 1;
while (r <= nums.length) {
while (preSum[r] - preSum[l - 1] >= target) {
ans = Math.min(ans, r - l + 1);
l++;
}
// 直接把左边界挪过来
if (f[r - 1] <= 0) l = r;
r++;
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
}
用这个可以处理负数的代码,去提交一下
在想到上面这个方法之前,我自己想的是:考虑对于每个位置i
,求出以i
结尾的连续子数组中,最大的和,并且记录这个最大和的起始位置。只要遍历以每个位置作为结尾的最大可能的连续和,并将其起始位置作为左边界l
,尝试右移l
找到一个最短长度,即可。
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int[] preSum = new int[nums.length + 1]; // 前缀和
int[] f = new int[nums.length + 1]; // f[i] 表示以 i 结尾的最大连续和, 求解f的过程有点动态规划的感觉
int[] index = new int[nums.length + 1]; // index[i] 表示以 i 结尾的最大连续和的起始位置
int ans = Integer.MAX_VALUE;
// 预处理
for (int i = 1; i <= nums.length; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1]; // 前缀和
if (f[i - 1] > 0) {
f[i] = f[i - 1] + nums[i - 1];
index[i] = index[i - 1];
} else {
f[i] = nums[i - 1];
index[i] = i;
}
}
// 遍历每个位置, 若以当前位置结尾的最大连续和 >= target , 则取出其起始位置, 作为滑动窗口的左边界l, 一直l++ 直到不满足条件即可
for (int i = 1; i <= nums.length ; i++) {
if (f[i] >= target) {
int l = index[i], r = i;
while (preSum[r] - preSum[l - 1] >= target) {
ans = Math.min(ans, r - l + 1);
l++;
}
}
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
}
这其实是暴力法了,提交了一下,发现耗时很高。