问题
对于连续子数组问题,有几种常见的思路,比如前缀和、动态规划、单调栈、滑动窗口等。
我们来看这样一个问题:
给定一个数组 A 和一个整数 K,对于它所有的连续子数组,筛选出和大于 K 的连续子数组,返回满足条件的连续子数组的最小长度。如果不存在,返回 -1.
分析
连续子数组的和一般可以用前缀和求得,这道题一定是要比较完所有的连续子数组和大小,如果有某个连续子数组没考虑到,那么肯定无法得到正确答案。
对于数组的题,遍历肯定是避免不了的。这道题遍历原始数组显然没什么意义,那就从遍历前缀和数组开始吧。
优化 1
现在我们从前缀和数组的第一个元素开始往后遍历了,每遍历一个元素把该元素的下标存储在双端队列里,同时计算以当前下标为结束符、以第一个元素为起始点的连续子数组是否满足条件,一旦某个元素发现满足条件,很容易就有如下猜想:有没有更短的连续子数组满足条件呢?于是,从双端队列的队头开始丢弃元素,直到不能再丢为止。为什么可以把双端队列的那些头部元素丢弃呢?现在分两种情况:假如这些元素作为更短的满足条件的连续子数组起始点,那么,这些连续子数组结束下标肯定就是当前的遍历位置;假如这些元素作为更短的满足条件的连续子数组的结束节点,显然是更不可能的。
优化 2
上面的操作已经给我们剔除了许多元素,在继续往后的遍历中不用在考虑以这些丢弃的元素为起始点的连续子数组了,那还有没有方法剔除更多的无用的元素呢?答案是有。
在遍历前缀数组的过程中,如果发现当前的元素比之前的元素小,那意味着什么?可以将双端队列里队尾的元素删除掉,因为如果存在以这些元素为起始点的连续子数组,那么以当前元素为起始点的数组也能满足要求且数组长度更小。
至此,所有的优化都完成了,具体实现代码如下:
class Solution {
public:
int shortestSubarray(vector& A, int K) {
int len=A.size();
vector preSum(len + 1, 0);
for (int i=1; i <=len; i++) {
preSum[i]=preSum[i - 1] + A[i - 1];
}
deque dq;
dq.push_back(0);
int ans=INT_MAX;
int j=1;
while (j <=len) {
while (dq.empty()==false && preSum[j] < preSum[dq.back()]) {
dq.pop_back();
}
while (dq.empty()==false && preSum[j] - preSum[dq.front()] >=K)
{
ans=min(j - dq.front(), ans);
dq.pop_front();
}
dq.push_back(j);
j++;
}
if (ans==INT_MAX) {
return -1;
} else {
return ans;
}
}
};
总结
这道题非常典型,几乎涵盖了数组问题的中所有知识点,如前缀和、单调栈、双端队列等,非常值得我们好好品味。