持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第17天,点击查看活动详情
题目描述
2022/10/26 每日一题
难度:困难
给定一个整数数组 nums
和一个整数 k
,找出 nums 中和至少为 k 的最短非空子数组 ,并返回该子数组的长度。如果不存在这样的子数组 ,返回 -1 。子数组是数组中连续的一部分。
输入:整数数组nums
、整数 k
输出:使子数组(连续)和大于K的最小子数组长度
算法思路
- 初始想法:从题中的连续数组第一个想到的是滑动窗口,于是尝试使用滑动窗口。确实滑动窗口可以实现一半左右的用例,但是当数组中出现负数时,会造成子数组和的非单调递增。
例如用例nums = [84, -37, 32, 40, 95] ,k = 167
:
- 首次遍历时需将整个数组都添加到子数组中才能满足k,此时可以开始滑动左边界来减小窗口,以获取满足条件的最小窗口,但是当将最左元素87移除后,子数组不满足K了。
- 若数组中元素均为正数,此时再继续删除元素,子数组和势必是更小于K的,因此左边界无需再滑动,而是继续滑动右边界。
- 但若数组中元素存在负数,则继续删除元素后子数组和必然会继续增大,且仍有可能是满足K的。因此不适合使用滑动窗口
- 前缀和:前缀和也是连续区间问题的一种常用解法,核心思想为先固定左边界,依次增加右边界计算数组前缀和
s[i]
,中间区间[i, j)
的和则可使用s[j] - s[i]
计算得到。比较各前缀和的差以及它们下标的差就可以求解。
滑动窗口和前缀和
共同点:适用于连续区间问题
区别:区间中存在负数时使用前缀和更好,不存在负数时使用滑动窗口更好
优化
前缀和依次比较的时间复杂度为O(n^2),可以进行优化。依次遍历前缀和数组s[i],同时将其加入到双端队列中:
- 优化1:若s[cur] - s[first] >= k,此区间内元素满足条件,无论s[cur + 1] - s[first]是否大于k,s[first]都不再能构成更优解(若大于K,区间的长度会+1,不满足最小区间;小于k不满足条件),因此可将队列中的第一个元素出队。
- 优化2:当s[cur] <= s[last],若后续存在s[i],使得s[i] - s[last] >= k,则必然存在s[i] - s[cur] >= k,且cur的区间小于last的区间,因此可将s[last],即队列中最后一个元素出队。(保持队列单调递增)
数据结构
- long数组s[n + 1]:存储前缀和,前缀和比原数组长度多1,因为整个数组的前缀和为0;long类型防止前缀和大小溢出
- 初始化ans = n + 1:若为n则无法区分整个数组相加 >= k的情况,和永远无法满足条件返回-1的情况
- deque双端队列:由两个优化可知需对存储前缀和的数据结构的两端都进行操作
代码实现
前缀和+单调双端队列
public int shortestSubarray(int[] nums, int k) {
int n = nums.length;
long[] s = new long[n + 1];
int ans = n + 1;
for(int i = 1; i <= n; i++){
s[i] = s[i - 1] + nums[i - 1];
}
Deque<Integer> deque = new ArrayDeque<>();
for(int i = 0; i <= n; i++){
while(!deque.isEmpty() && s[i] - s[deque.peekFirst()] >= k){
ans = Math.min(ans, i - deque.pollFirst());
}
while(!deque.isEmpty() && s[i] <= s[deque.peekLast()]){
deque.pollLast();
}
deque.offer(i);
}
return ans == n + 1 ? -1 : ans;
}