数组累加和问题三连
第一题
题目:
给定一个全是正数的数组arr,一个目标数字target,求数组中满足和为target的最长子数组的长度
思路:
这是很简单的题目,用双指针和窗口就可以解决。具体见代码
代码:
public static int getMaxLength(int[] arr, int K) {
if (arr == null || arr.length == 0 || K <= 0) {
return 0;
}
int left = 0;
int right = 0;
int sum = arr[0];
int len = 0;
while (right < arr.length) {
if (sum == K) {
len = Math.max(len, right - left + 1);
sum -= arr[left++];
} else if (sum < K) {
right++;
if (right == arr.length) {
break;
}
sum += arr[right];
} else {
sum -= arr[left++];
}
}
return len;
}
第二题
题目:
给定一个数组arr,数组中整数,负数和0都有,一个目标数字target,求数组中满足和为target的最长子数组的长度
思路
对于这种数组问题,要求某个S问题的解,一般来说有两种解决思路
- 以数组元素i作为起始位置时,S问题的解。遍历所有的解,能得到最终答案
- 以数组元素i作为终止位置时,S问题的解。遍历所有的解,能得到最终答案
对于这个题,采用的第二种思路。
对于数组元素a[j]来说,要求其作为子数组最后一个元素时,满足条件的子数组的最大长度,对于这个问题来说,那么需要找到一个a[i],使得sum(a[i],…,a[j])=target,同时,这个i要尽可能的小,这样才能满足子数组最长。对于每一个a[j],都这样做,最后遍历结果,就可以找到最终的结果。
按照上面的思路,感觉时间复杂度是O(N^2)的。这里可以继续优化,我们可以在O(n)的时间内求出数组的前缀和S,如果想要得到一个最小的i,使得sum(a[i],…,a[j])=target,那么就相当于找到一个i,使得S[i] = S[j] - target。当然,如果找不到满足条件的i,说明以a[j]结尾的子数组没有满足条件的,直接遍历下一个就可以了。
时间复杂度为O(N),空间复杂度也是O(N)
代码
public int maxSubArrayLen(int[] nums, int k) {
// Write your code here
if (nums == null || nums.length == 0) {
return 0;
}
//用Map存储出现过的前缀和及其对应的索引
HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(0, -1); // 需要提前压入一个键值对<0,-1>
//如果不压入的话,当从0到j正好满足条件时,Map中找不到前缀为0对应的索引,
//所以会导致答案不对
int len = 0;
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
if (map.containsKey(sum - k)) {
len = Math.max(i - map.get(sum - k), len);
}
if (!map.containsKey(sum)) { //对于同样的前缀和,只保存第一次出现的位置,因为要保 证子数组最长
map.put(sum, i);
}
}
return len;
}
变体
题目:给定一个数组arr,称1和2的个数相同的子数组为合格的子数组,求arr中合格的子数组的最大长度
思路:
首先对数组进行一次遍历,对于每个元素,如果为1,则不做处理;如果为2,则变为-1;如果为其他数,则设置为0。那么问题就转化为,求arr中,满足目标和为0的子数组的最大长度。
第三题
题目
给定一个数组arr,数组中整数,负数和0都有,一个目标数字target,求数组中满足和小于等于target的最长子数组的长度
思路
首先定义概念,定义两个数组,分别是
- MinSum[]:MinSum[i]代表着子数组以i位置元素开头,能取得的最小累加和
- MinSumEnd[]:MinSumEnd[i]代表着子数组以i位置元素开头,能取得最小累加和的子数组的结束位置
MinSum[i]表示从i开始往后,所有子数组的最小的累加和,MinSumEnd[i]代表着对应的结束位置,这两个数据是非常有用的。
下面开始主流程:从数组的index位置开始,设index = 0 ,利用上面两个数组,求出0位置作为子数组开头所能达到的满足条件的子数组的最大长度。很简单,如果MinSum[0]<=target,那么可以得到末尾索引j(j=MinSumEnd[i]),此时sum = MinSum[0],接下来计算sum +MinSum[j+1],如果sum<=target,则sum = sum +MinSum[j+1],并且得到新的末尾位置k(k=MinSumEnd[j+1]);继续执行上面的操作直到 sum +MinSum[k+1] >target 时,停止,此时记录0位置作为子数组的最大长度(满足条件的子数组),记此时的子数组末尾位置为p;
然后,sum = sum - arr[0],也就是将0位置元素从sum中减去,此时sum为从1到p位置所有元素的和,如果sum + MinSum[p+1] <=target,则sum = sum + MinSum[p+1],p = MinSumEnd[p+1],继续循环;如果sum + MinSum[p+1] > target,说明以1为开头的,能达到的满足条件的子数组,最大长度已经找到了,尝试更新这个全局的最大长度,然后将index++,重复上述过程。
这个算法的精髓在于,在找到0位置开头的满足条件的最长子数组之后,将0位置删除以后,以1位置开始找满足长度的子数组时,不需要从头开始找。假设从0开始的满足条件的最大子数组的长度是p+1,也就是到索引p位置终止(这就是说0到p的sum值,加上MinSum[p+1]的值,大于target),那么当1为子数组开头元素时,如果1到p的sum值,加上MinSum[p+1]的值还是大于target的,说明1作为开始位置的,能达到满足条件的子数组中,长度不会超过p,这是比从0开始的满足条件的最大子数组的长度要小的,所以1开头的子数组就没有继续讨论的意义了。
这个算法的关键在于,除了第一次遍历,后面的遍历,都不需要从头开始找,删除了对那些不可能成为答案的结果的遍历,所以可以达到O(N)
代码
public static int maxLengthAwesome(int[] arr, int k) {
if (arr == null || arr.length == 0) {
return 0;
}
int[] minSums = new int[arr.length];
int[] minSumEnds = new int[arr.length];
//初始化两个关键数组
minSums[arr.length - 1] = arr[arr.length - 1];
minSumEnds[arr.length - 1] = arr.length - 1;
for (int i = arr.length - 2; i >= 0; i--) {
if (minSums[i + 1] < 0) {
minSums[i] = arr[i] + minSums[i + 1];
minSumEnds[i] = minSumEnds[i + 1];
} else {
minSums[i] = arr[i];
minSumEnds[i] = i;
}
}
int end = 0;
int sum = 0;
int res = 0;
// i是窗口的最左的位置,end扩出来的最右有效块儿的最后一个位置的,再下一个位置
// end也是下一块儿的开始位置
// 窗口:[i~end)
for (int i = 0; i < arr.length; i++) {
// while循环结束之后:
// 1) 如果以i开头的情况下,累加和<=k的最长子数组是arr[i..end-1],看看这个子数组长度能不能更新res;
// 2) 如果以i开头的情况下,累加和<=k的最长子数组比arr[i..end-1]短,更新还是不更新res都不会影响最终结果;
while (end < arr.length && sum + minSums[end] <= k) {
sum += minSums[end];
end = minSumEnds[end]+1;
}
res = Math.max(res, end - i);
if (end > i) { // 窗口内还有数 [i~end) [4,4)
sum -= arr[i];
} else { // 这是需要特殊考虑的一点,窗口内已经没有数了,
//说明从i开头的所有子数组累加和都不可能<=k,这时end=i了,让end= i+1,下一轮开始时
//i也会加1,就自动跳到下一个数去遍历了
end = i + 1;
}
}
return res;
}
代码的循环中,两个遍历,i和end,都是从0跑到n,所以时间复杂度是O(N).
对于代码中end=i的情况,可以看下面这个例子
[-7,3,3,3,3,3,…],target=0
从0位置开始,可以最多到3位置,此时end = 3,i=0,sum = sum - (-7),i++
当i=1时,sum = 6 > target ,sum = sum-3,i++,
i=2时,sum = 3 >target,sum = sum-3,i++ ,
i=3时,sum = 0 =target(此时没有元素在窗口中了,但是end仍然扩不动,此时的end==i,所以让end = i+1,end变成4,下一轮i++也会变成4,就会跳过3位置的元素,继续遍历了)