题目描述
给定一个含有若干个正整数的数组,和一个正整数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;
}
}
可以优化为的空间复杂度
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;
}
}
这其实是暴力法了,提交了一下,发现耗时很高。