862. 和至少为 K 的最短子数组
给定一个整数数组 nums 和一个整数 k ,找出 nums 中和至少为 k 的 最短非空子数组 ,并返回该子数组的长度。如果不存在这样的 子数组 ,返回 -1 。
子数组是数组中连续的一部分。
示例1:
输入:nums = [1], k = 1
输出:1
示例2:
输入:nums = [1,2], k = 4
输出:-1
示例3:
输入:nums = [2,-1,2], k = 3
输出:3
思路:题目要求找到一个最短的子数组,使得子数组的和大于等于 k。不难想到,可以使用前缀和快速计算子数组的和。
我们用一个长度为 n+1 的数组 s[i] 表示数组 nums 前 i 个元素的和。另外,我们需要维护一个严格单调递增的队列 q,队列中存储的是前缀和数组 s[i] 的下标。注意,这里的单调递增是指下标对应的前缀和的大小,而不是下标的大小。
为什么存的是下标呢?这是为了方便计算子数组的长度。那为什么队列严格单调递增?我们可以用反证法来说明。
假设队列元素非严格单调递增,也即是说,存在下标 i 和 j,满足 i<j,且 s[i]≥s[j]。
当遍历到下标 k,其中 i<j<k≤n,此时 s[k]−s[j]≥s[k]−s[i],且 nums[j…k−1] 的长度小于 nums[i…k−1] 的长度。由于下标 jjj 的存在,子数组 nums[i…k−1] 一定不是最优解,队列中的下标 iii 是不必要的,需要将其移除。因此,队列中的元素一定严格单调递增。
回到这道题目上,我们遍历前缀和数组 s,对于遍历到的下标 i,如果 s[i]−s[q.front]≥k,说明当前遇到了一个可行解,我们可以更新答案。此时,我们需要将队首元素出队,直到队列为空或者 s[i]−s[q.front]<k为止。
如果此时队列不为空,为了维持队列的严格单调递增,我们还需要判断队尾元素是否需要出队,如果 s[q.back]≥s[i],则需要循环将队尾元素出队,直到队列为空或者 s[q.back]<s[i] 为止。然后,我们将下标 i 入队。
遍历结束,如果我们没有找到可行解,那么返回 −1。否则,返回答案。
代码:
class Solution {
public int shortestSubarray(int[] nums, int k) {
int n = nums.length;
long[] preSumArr = new long[n + 1];
for (int i = 0; i < n; i++) {
preSumArr[i + 1] = preSumArr[i] + nums[i];
}
int res = n + 1;
Deque<Integer> queue = new ArrayDeque<Integer>();
for (int i = 0; i <= n; i++) {
long curSum = preSumArr[i];
while (!queue.isEmpty() && curSum - preSumArr[queue.peekFirst()] >= k) {
res = Math.min(res, i - queue.pollFirst());
}
while (!queue.isEmpty() && preSumArr[queue.peekLast()] >= curSum) {
queue.pollLast();
}
queue.offerLast(i);
}
return res < n + 1 ? res : -1;
}
}
执行结果:
总结: 通过每日一题来锻炼自己,提升自己的能力,开拓自己的思维,每天收获一点点,对自己有很大帮助。