Slide Window 专题
209. Minimum Size Subarray Sum
给定正整数数组和正整数s,找到加和大于等于s的连续子数组的最小长度。
基础slide window题目。
class Solution { public: int minSubArrayLen(int s, vector<int>& nums) { int ret = INT_MAX, win_size = 0, len = nums.size(), sum = 0; for(int i=0; i<len; i++){ sum += nums[i]; win_size++; while(sum >= s && win_size > 0){ ret = min(ret, win_size); sum -= nums[i-win_size+1]; win_size--; } } return ret==INT_MAX ? 0 : ret; } };
862. Shortest Subarray with Sum at Least K
209加强版,与上一题唯一不同的地方在于,数组中存在负数。
Detailed intuition behind deque solution 这份题解写得特别好。其中就上一题209讲解了滑动窗口算法工作的基本原理:
-
Incremeting the end pointer while the sum of current subarray (defined by current values of
start
andend
) is smaller than the target. -
Once we satisfy
our condition (the sum of current subarray >= target) we keepincrementing
the start pointer until weviolate
it (untilsum(array[start:end+1]) < target
). -
Once we violate the condition we keep incrementing the end pointer until the condition is satisfied again and so on.
也讲解了为什么在有负数的情况下,传统滑动窗口算法不能工作:
The problem with this solution is that it doesn't work if we have negative values, this is because of the sentence above Once we "violate" the condition we stop incrementing start
.
同时举例说明:
Now, let's take an example with negative values nums = [3, -2, 5]
and target=4
. Initially start=0
, we keep moving the end pointer until we satisfy the condition, here we will have start=0
and end=2
. Now we are going to move the start pointer start=1
. The sum of the current subarray is -2+5=3 < 4
so we violate the condition. However if we just move the start pointer another time start=2
we will find 5 >= 4
and we are satisfying the condition. And this is not what the Sliding window assumes.
引出deque改进的滑动窗口算法:
What does the Deque store :
deque保存start指针可能的值,因为deque需要保证单调递增,所以不一定是连续。
The deque stores the possible
values of the start pointer. Unlike the sliding window, values of the start
variable will not necessarily be contiguous.
Why is it increasing :
之所以维护递增的队列,是为了保证,当d[0]不满足条件时,d[i] i>0 都不满足条件。这样一来才能适用于滑动窗口算法。
So that when we move the start pointer and we violate the condition, we are sure we will violate it if we keep taking the other values from the Deque. In other words, if the sum of the subarray from start=first value in the deque
to end
is smaller than target
, then the sum of the subarray from start=second value in the deque
to end
is necessarily smaller than target
.
So because the Deque is increasing (B[d[0]] <= B[d[1]]
), we have B[i] - B[d[0]] >= B[i] - B[d[1]]
, which means the sum of the subarray starting from d[0]
is greater than the sum of the sub array starting from d[1]
.
Why do we have a prefix array and not just the initial array like in sliding window :
由于deque里的值不是连续的,所以不能像传统滑动窗口(start指针的取值是连续变化)那样仅通过一个sum来维护每个窗口的和。因此需要维护前缀和来求得每个窗口的和。
Because in the sliding window when we move start
(typically when we increment it) we can just substract nums[start-1]
from the current sum and we get the sum of the new subarray. Here the value of the start
is jumping
and one way to compute the sum of the current subarray in a constant
time is to have the prefix array.
Why using Deque and not simply an array :
既然需要从start端取数,又需要从end端取数,还需要从end端插入数,因此使用deque。
We can use an array, however we will find ourselves doing only three operations:
1- remove_front
: when we satisfy our condition and we want to move the start pointer
2- append_back
: for any index that may be a future start pointer
3- remove_back
: When we are no longer satisfying the increasing order of the array
Deque enables doing these 3 operations in a constant time.
解法:
首先计算nums的前缀和P。对于每个下标有y,我们希望找到opt(y),opt(y)是最大的 x(x<y),使得 P[y]-P[x] >= k。
- 若存在 x2>x1 ,且P[x2] <P[x1],那么opt(y)一定不是x1,因为 P[y] - P[x2] >= P[y] - P[x1],且 y - x2 < y - x1
- 若已存在opt(y1)==x,那么x就不用再被考虑,因为若存在y2 > y1,opt(y2)==x,y2-x > y1-x
维护一个存有P下标的单调队列,当把idx入到队尾前,需将队尾满足P[tail] > P[idx]的tail出队。
若对头head,满足P[y]-P[head] >= k,则将head弹出。
class Solution { public: int shortestSubarray(vector<int>& nums, int k){ int len = nums.size(), result = len+1; vector<long long> prefix(len+1, 0); for(int i=1; i<=len; i++) prefix[i] = prefix[i-1] + nums[i-1]; deque<int> mono_q; mono_q.push_back(0); for(int i=1; i<=len; i++){ while(!mono_q.empty() && prefix[i] - prefix[mono_q.front()] >= k){ result = min(result, i - mono_q.front()); mono_q.pop_front(); } while(!mono_q.empty() && prefix[mono_q.back()] >= prefix[i]) mono_q.pop_back(); mono_q.push_back(i); } return result==len+1 ? -1 : result; } };
992. Subarrays with K Different Integers
给定一个正整数数组,计算刚好有K个不同数的子数组的个数。(For example, [1,2,3,1,2]
has 3
different integers: 1
, 2
, and 3
.)
解法一:slide window
如果是求最多有K个不同元素的子数组,那么就是典型的slide window的题目,只需要在这个典型题目上增添一步:
exactly(K) = atMost(K) - atMost(K-1)
class Solution { public: int subarraysWithKDistinct(vector<int>& A, int K) { //cout<<atMost(A, K)<<" "<<atMost(A, K-1)<<endl; return atMost(A, K) - atMost(A, K-1); } int atMost(vector<int> &A, int K){ map<int, int> count; int ret = 0, win_size = 0, len = A.size(), ctr = 0; for(int i=0; i<len; i++){ if(count[A[i]] == 0){ ctr++; } count[A[i]]++; while(ctr > K){ if((--count[A[i-win_size--]]) == 0) ctr--; } ++win_size; //cout<<i<<" "<<win_size<<endl; ret += (win_size); } return ret; } };
解法二:prefix slide window
思路:
如果子数组[j, i]包含K个不同元素,并且前prefix个元素也出现在子数组[j+prefix, i]中,那么可以得到1+prefix个符合要求的子数组。例如,[1, 2, 1, 2, 3],前两个数[1, 2]也出现在子数组[1,2,3]中,可以得到1+2
个符合要求的子数组,[1, 2, 1, 2, 3]
, [2, 1, 2, 3]
和 [1, 2, 3]
.
遍历数组,维护滑动窗口,窗口尾指向当前元素,窗口头head移动至j,使A[j]在窗口中只出现一次。换句话说,在保证不同元素数不变的情况下,尽量缩短窗口。为达到这个目的,对出现在窗口中元素进行计数,当下一个元素添加到窗口尾时,从窗口头移除尽量多的元素,直至窗口头指向的元素仅在窗口中出现一次,在移除元素的同时,递增prefix。
如果窗口中存在K个不同元素,可以得到1+prefix个符合要求的子数组。
如果窗口中有K+1个不同元素,我们需要移除窗口头指向的元素(该元素仅出现在窗口头),因为我们开始计算一组新的子数组所以重置prefix。
class Solution { public: int subarraysWithKDistinct(vector<int>& A, int K) { int len = A.size(), prefix = 0, head = 0, ret = 0; map<int, int> count; for(int i=0; i<len; i++){ if(!count[A[i]]++) K--; if(K < 0){ --count[A[head++]]; prefix = 0; K++; } while(count[A[head]] > 1){ --count[A[head++]]; prefix++; } if(K==0) ret += prefix+1; } return ret; } };
类似题目:
1248. Count Number of Nice Subarrays
解法一:slide window
exactly(K) = atMost(K) - atMost(K-1)
class Solution { public: int numberOfSubarrays(vector<int>& nums, int k) { return atMost(nums, k) - atMost(nums, k-1); } int atMost(vector<int> &nums, int k){ int len = nums.size(), ret = 0, head = 0; for(int i=0; i<len;i++){ if(nums[i]%2) k--; while(k<0){ if(nums[head++]%2) k++; } ret += i-head+1; } return ret; } };
解法二:prefix slide window
class Solution { public: int numberOfSubarrays(vector<int>& nums, int k) { int len = nums.size(), head = 0, ret = 0, prefix = 0; for(int i=0; i<len; i++){ if(nums[i]%2) k--; if(k<0){ head++; k++; prefix = 0; } while(head<=i && nums[head]%2==0){ head++; prefix++; } if(k==0) ret += prefix+1; //cout<<k<<" "<<prefix<<endl; } return ret; } };
395. Longest Substring with At Least K Repeating Characters
求字符串s的子串的最大长度,该子串要求所有字符在该子串中出现的次数都至少是k次。
解法一:滑动窗口
一开始把题目看成了,子串中每个字符至多出现k次。如果是这样,那么是一道典型的滑动窗口的题目。
然而,题目是至少出现k次。这样一来,滑动窗口不再适用。因为,在字符出现至多k次的问题中,当窗口尾部的字符超过k个,意味着只需要将窗口头部往后移动直至尾部字符等于k个;当尾部字符出现次数小于k次时,只需要将窗口尾部继续往后移动即可。而在字符出现至少k次的问题中,当尾部字符出现次数小于k次时,无法判断是移动窗口头部(舍弃尾部当前字符,后面可能不再有窗口尾部的字符)还是移动窗口尾部(期待后面还有更多当前尾部字符)。
在此题中,字符出现的次数不能用来决定窗口的动作(移动头还是移动尾)。但是可以引入别的依据来移动窗口,例如,窗口中不同字符至多多少个。问题从至少转换为至多。那转换后的问题与原问题的关系是什么呢?窗口依照其中出现的不同字符数量移动,我们对窗口中的不同字符(unique)和出现次数大于等于k的字符(at_least_k)进行计数,若对当前窗口unique==at_least_k,说明该窗口中所有不同的字符都至少出现了k次。
class Solution { public: int longestSubstring(string s, int k) { int result = 0; for(int i=1; i<=26; i++){ int unique = 0, at_least_k = 0, win_size = 0, len = s.size(); unordered_map<char, int> ctr; for(int j=0; j<len; j++){ if(ctr[s[j]]++ == 0) unique++; if(ctr[s[j]] == k) at_least_k++; while(unique > i){ if(ctr[s[j-win_size]] == k) at_least_k--; if(--ctr[s[j-win_size--]] == 0) unique--; } win_size++; if(unique == at_least_k) result = max(result, win_size); } } return result; } };
解法二:分治法 https://www.cnblogs.com/jasonlixuetao/p/11945760.html