题目
给你一个整数数组 nums 和一个整数 k ,找出 nums 中和至少为 k 的 最短非空子数组 ,并返回该子数组的长度。如果不存在这样的 子数组 ,返回 -1 。
子数组 是数组中 连续 的一部分。
直觉和前缀和有关,但数组有负数,简单的前缀和解决不了该问题。
暴力求解
遍历每个值,找到每个值最短的子数组,最后对数组长度求min
滑动窗口
官方给出的题解有点抽象。
有些题目中,滑动窗口体现了整个子数组,而在本题中,由于数组中元素元素是有负数的,使用滑动窗口来保存子
数组,不能确认某个子数组的长度是否为最短。
所以,本题中的滑动窗口中保存的是起点的集合。遍历每个元素y时,元素y作为子数组的终点,滑动窗口中保存的是子数组的可能的起点x。子数组的长度是用y-x来计算,而不是滑动窗口的长度。并且相关的前缀和需要满足presum[y]-presum[x]>k。
首先计算前缀和。关于前缀和第一位置0(presum[0] =0),而不是presume[0] = arr[0],是为了解决arr数组为空的情况。这样不需要判断特殊情况,逻辑比较清晰。
其次,构建滑动窗口,即链表。这里使用双向队列实现。
-
保证滑动窗口的单调性。对于给定的元素y,在滑动窗口中找对应的x,使得presum[y]-presum[x]>k。显然需要presum[x]小于presum[y],所以需要移除presum[x]大于presum[y]的x。此时,只保证了presum[x]小于presum[y],但presum[x]前面的值是否小于presum[y]?显然是小于的,在计算以x为结尾的子数组时,会保证前面小于presum[x],所以只需要保证presum[y]大于滑动窗口中的最后一个值。保证将当前的y加入滑动窗口后,滑动窗口依然是单调递增的。在移除了presum[x]大于presum[y]的所有x后,再找到差大于k的既可。
-
当presum[y]-presum[x]>k时,此时x是一个解,但未必是最小的。由于滑动窗口是中的x所对应的presum是单调递增的,所以从滑动窗口的头部取出元素,若做差大于k,则更新 子数组的最短距离,并从滑动窗口删除该元素,直到差值小于k。
-
最后,将y加入滑动窗口。完成了对以元素y结尾的所有的子数组的搜索。
是否存在保证滑动窗口的单调性时,删除后面可能存在的最短的子数组的解?如下图,在处理b时,会将a移除,在计算c时,假设c-a大于k,那么c-b一定也大于k,且c和b之间的距离是小于c和a之间的距离。更严谨的证明见官方题解。
*(c)
*(a)
*(b)
class Solution {
public int shortestSubarray(int[] nums, int k) {
int len = nums.length;
long[] dp = new long[len+1];
Deque<Integer> list = new LinkedList();// Deque !!! not sublist
long r = len +1;
dp[0] = 0;
for(int i=0;i<len;i++){
dp[i+1] = (long)nums[i] +dp[i];
}
for(int i=0;i<len+1;i++){
//dp[i] = dp[i]+(long)nums[i];
while(!list.isEmpty() && dp[i] < dp[list.getLast()]){
list.removeLast();
}
while(!list.isEmpty() && dp[i] -k >= dp[list.getFirst()]){
r = Math.min(r,i-list.removeFirst());
}
list.addLast(i);
}
return r== len+1 ? -1:(int)r;
}
}
trick
链表在java中可以直接使用arrayList,或linkedlist,但也可以使用原生数组,使用left和right来标记区间。
reference
https://github.com/Shellbye/Shellbye.github.io/issues/41