滑动窗口(双指针)的思路,一般用在某个连续的子序列/子数组里寻求某个满足答案的状态,关键在于采取什么样的数据结构去维护这个序列所应该有的状态。
滑动窗口模板:
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
...
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
作者:labuladong
链接:https://leetcode-cn.com/problems/minimum-window-substring/solution/hua-dong-chuang-kou-suan-fa-tong-yong-si-xiang-by-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
例题1:
https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/
很明显也需要用到滑动窗口的思想,当不断往右拓展连续可能序列的时候,需要看窗口内是否含有同样的字符,所以关键是判重,那在这里直接用个集合保存窗口内的元素,当右指针指向的元素出现在集合里之后,就不停移动做指针直到set里不再出现那个元素。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.size() == 0) return 0;
unordered_set<char> lookup;
int maxStr = 0;
int left = 0;
for(int i = 0; i < s.size(); i++){
//窗口内存在这个元素
while(lookup.count(s[i])){
lookup.erase(s[left]);
left++;
}
maxStr = max(maxStr,i-left+1);
lookup.insert(s[i]);
}
return maxStr;
}
};
例题2:
https://leetcode-cn.com/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/
很明显,在由两个指针框起来的窗口内,判断右指针能否继续向右拓展,最重要的是窗口内的最大和最小值,我们需要数据结构去维护最大值和最小值,而红黑树实现的STL一头一尾就是最大和最小值。
这里关键还有迭代器rbegin()的使用:返回容器reverse后的第一个元素。
int longestSubarray(vector<int>& nums, int limit) {
map<int, int> m;
int ans = 0;
int i = 0;
for (int j = 0; j < nums.size(); j++) {
m[nums[j]]++;
while (m.rbegin()->first - m.begin()->first > limit) {
m[nums[i]]--;
if (m[nums[i]] == 0) {
m.erase(nums[i]);
}
i++;
}
ans = max(ans, j - i + 1);
}
return ans;
}
选择另外的数据结构(两个优先队列)去维护:
我们需要维护的是最大值和最小值,这两个变量是需要时时访问的,因此想到优先队列,但是因为我们在这里键是该元素的值,所以这个下标可能就不在窗口里,所以当右指针无法拓展,移动做指针的时候,需要循环将影响和拓展和过期元素同时pop掉。
既然取最大最小值,两个优先队列还不是最优的,我们可以直接维护两个单调队列(一个递增一个递减),方便我们分别从队首读取窗口内的最大值和最小值,注释中有思路。
class Solution {
public:
int longestSubarray(vector<int>& nums, int limit) {
deque<int> maxq,minq;
int ans = 0,n = nums.size(),l = 0;
for(int r = 0;r < n; ++r){
int cur = nums[r];
//单调增队列里,比cur大的元素都没用了,因为永远不可能是最小值了,这里就是优于优先队列的一点,优先队列还要对下标过期的元素做处理
while(minq.size() && minq.back() > cur) minq.pop_back();
minq.push_back(cur);
//单调减队列里,比cur小的元素都没用了
while(maxq.size() && maxq.back() < cur) maxq.pop_back();
maxq.push_back(cur);
//此时 两个队不可能为空,至少有刚push进去的元素
//移动左指针
while(maxq.front() - minq.front() > limit){
//移动左指针
if(nums[l] == minq.front()) minq.pop_front();
if(nums[l] == maxq.front()) maxq.pop_front();
l++;
}
ans = max(ans,r - l + 1);
}
return ans;
}
};
例题3 单调队列和滑动窗口
- 题目:输入正整数k,和长度为n的整数序列,定义f(i)表示从元素i开始的连续k个元素的最小值。要求计算f(1),f(2)…f(n-k+1)。
- 很明显,窗口的大小都规定好了(双指针的范围),我们要维护的就是窗口内的最小值,当窗口向右移,之前左指针指向的元素应当可以被移除。所以需要一个数据结构,可以时时访问窗口内最小值,且是把元素值当作键索引的(因此可以直接按值删除),很显然这个可以是红黑树实现的map,头迭代器指向的就是最小值,O(nlogn)。
- 但是,在一个窗口内,一个大元素如果出现在一个值更小的元素的左边就完全失去了它的意义(本题的特殊性),因为它在窗口内不会是最小值,下标小但是值大的元素不可能是答案,所以我们只需要维护一个单调队列:队首是最小值,而后面的元素全是值大且下标大的元素,换言之:滑动窗口内的有效元素都是单调递增的。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<deque>
#define LL long long
using namespace std;
const int maxn = 1e6 + 5;
int n,k;
int nums[maxn];
int main(void){
while(1){
scanf("%d%d",&n,&k);
for(int i = 1;i <= n;++i) scanf("%d",&nums[i]);
deque<int> q;
int L = 1,R = k;
for(int i = L;i <= R;++i){
if(q.empty()) q.push_back(nums[i]);
else{
if(q.back() >= nums[i]) q.pop_back();
q.push_back(nums[i]);
}
}
printf("%d ",q.front());
while(R < n){
if(q.size() && q.front() == nums[L++]) q.pop_front();
int t = nums[++R];
while(q.size() && q.back() >= t) q.pop_back();
q.push_back(t);
printf("%d ",q.front());
}
break;
}
return 0;
}
/*
7 4
5 2 6 8 10 7 4
2 2 6 4
*/