给你一个整数数组 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
提示:
1 <= nums.length <= 10^5
-10^5 <= nums[i] <= 10^5
1 <= k <= 10^9
解法1:前缀和 + 单调双端队列
记数组 nums 的前缀和数组为 preSum,可以根据 preSum[i] = sum(nums[0...i-1]) 计算得到。
对于边界情况,preSum[0]=0。而从数组 nums 下标 i 开始长为 m 的子数组的和就可以根据 preSum[i+m]−preSum[i] 快速计算得到。
题目要求计算 nums 中,和大于等于 k 的最短子数组的长度。可以以 nums 的每一个下标作为候选子数组的起点下标,都计算满足条件的最短子数组的长度,然后再从这些长度中找出最小值即可。
遍历 preSum 数组,访问过的前缀和先暂存在某种集合 queue 中。
根据前缀和数组的性质,后访问到的某个前缀和 preSum[j] 减去之前访问到的某个前缀和 preSum[i](j>i)即为 nums 中某段子数组的和。
因此,每次访问到某个前缀和 preSum[j] 时,可以用它尝试减去集合 queue 中所有已经访问过的前缀和。
当某个 queue 中的前缀和 preSum[i],第一次出现 preSum[j]−preSum[i]≥k 时,这个下标 i 就找到了以它为起点的最短子数组的长度 j−i。此时,可以将它从 queue 中移除,后续即使还有以它为起点的满足条件的子数组,长度也会大于当前的长度。
当一个前缀和 preSum[j] 试减完 queue 中的元素时,需要将它也放入 queue 中。将它放入 queue 前, queue 中可能存在比 preSum[j] 大的元素,而这些元素和 preSum[j] 一样,只能作为再后续访问到的某个前缀和 preSum[h] 的减数。而作为减数时,更大的值只会让不等式 preSum[h]−preSum[i]≥k 更难满足。即使都满足,后访问到的值也可以带来更短的长度。
因此,在把 preSum[j] 放入 queue 时,需要将 queue 中大于等于 preSum[j] 的值也都移除。
接下来考虑 queue 的性质。我们会往 queue 中增加和删除元素。
每次增加一个元素 curSum 前,先根据不等式删除一部分元素(也可能不删),然后再删除 queue 中所有大于等于 curSum 的元素,这样每次加进去的元素都会是 queue 中的唯一最大值,使得 queue 中的元素是按照添加顺序严格单调递增的,我们知道单调队列是满足这样的性质的。而这一性质,也可以帮助找到 q 中所有满足不等式的值。按照添加的顺序从早到晚,即元素的值从小到大来比较是否满足不等式即可。按照这个顺序,一旦有一个元素不满足,queue 中后续的元素也不会满足不等式,即可停止比较。
基于此,我们需要一个集合,可以在两端删除元素,在一端添加元素,因此使用双端队列。
在完成代码时,queue 中暂存的元素是 preSum 的下标,对应下标的前缀和严格单调递增。
Java版:
class Solution {
public int shortestSubarray(int[] nums, int k) {
int n = nums.length;
long[] presum = new long[n + 1];
for (int i = 0; i < n; i++) {
presum[i + 1] = presum[i] + nums[i];
}
int res = n + 1;
Deque<Integer> queue = new ArrayDeque<>();
for (int i = 0; i < n + 1; i++) {
long cursum = presum[i];
while (!queue.isEmpty() && cursum - presum[queue.peek()] >= k) {
res = Math.min(res, i - queue.poll());
}
while (!queue.isEmpty() && presum[queue.peekLast()] >= cursum) {
queue.pollLast();
}
queue.offer(i);
}
return res == n + 1 ? -1 : res;
}
}
Python3版:
class Solution:
def shortestSubarray(self, nums: List[int], k: int) -> int:
n = len(nums)
presum = [0] * (n + 1)
for i in range(n):
presum[i + 1] = presum[i] + nums[i]
res = n + 1
queue = deque()
for i in range(n + 1):
cursum = presum[i]
while queue and cursum - presum[queue[0]] >= k:
res = min(res, i - queue.popleft())
while queue and presum[queue[-1]] >= cursum:
queue.pop()
queue.append(i)
return -1 if res == n + 1 else res
复杂度分析
- 时间复杂度:O(n),其中 n 是数组 nums 的长度。求 preSum 消耗 O(n)。preSum 每个下标会入 queue 一次,最多出 queue 一次。
- 空间复杂度:O(n)。preSum 和 queue 长度均为 O(n)。