前言
写下这篇文章的原因主要是本人在力扣86场双周赛碰到的第四题,以为是dp,但其实是滑动窗口进行求解,以下特此记录一下滑动窗口的几种常见题型,也可以作为今后复习用的题单。
滑动窗口的本质
首先,得了解一下滑动窗口的本质,在我看来,滑动窗口本质是及时舍弃不需要的元素。
思路就是每次遍历过程都要实现三部曲:
- 处理队尾
- 处理队头
- 实时更新
题型1:单调队列的模拟
239. 滑动窗口最大值
这道题基本上是这个题型我们做的第一题,主要原理就是手动实现一个双端队列,使得队列里的数满足严格单调递增的规律,从而获取在窗口内的最大最小值只需要O(1)的时间。具体代码如下:
const int N = 1e5 + 10;
class Solution {
public:
int q[N], hh, tt = -1;
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
for(int i = 0; i < nums.size(); ++ i) {
if(hh <= tt && i - k + 1 > q[hh]) hh ++;
while(hh <= tt && nums[q[tt]] <= nums[i]) tt --;
q[++ tt] = i;
if(i - k + 1 >= 0) res.push_back(nums[q[hh]]);
}
return res;
}
};
注意循环内3、4行的顺序不能交换,因为有可能新加入的这个数刚好是这个窗口内的最大值!
2398. 预算内的最多机器人数目
上题的拓展版本
const int N = 1e5;
class Solution {
public:
int q[N], hh = 0, tt = -1;
int maximumRobots(vector<int>& chargeTimes, vector<int>& runningCosts, long long budget) {
int ans = 0;
long long s = 0;
int n = chargeTimes.size();
for(int l = 0, r = 0; r < n; ++r) {
// 处理队尾
while(hh <= tt && chargeTimes[q[tt]] <= chargeTimes[r]) tt--;
q[++tt] = r;
s += runningCosts[r];
// 处理队头
while(hh <= tt && chargeTimes[q[hh]] + (r - l + 1) * s > budget) {
if(q[hh] == l) {
hh++;
}
s -= runningCosts[l];
++l;
}
// 实时更新
ans = max(ans, r - l + 1);
}
return ans;
}
};
题型2:字符串匹配问题
这一类题型其实非常重要,我看了网上很多解法,看到一位叫做flix的大佬写的非常好,他的模板可以秒杀非常多同类题目,强烈推荐!(见文末参考)
这类题型主要是通过滑动窗口和哈希表来解决!
题型2.1:变长滑动窗口
76. 最小覆盖子串
注意,其实这类题型的流程还是不离开上述所说的三部曲,下面贴一下flix大佬的思路详解:
class Solution {
public:
string minWindow(string s, string t) {
string ans = "";
int ns = s.size(), nt = t.size();
if(ns < nt) return ans; // 特判
unordered_map<char, int> ht;
for(int i = 0; i < nt; ++i) ht[t[i]]++; // 统计所有t中字符个数
int need = nt; // 符合题意的窗口内必须含有的字符数量
int min_len = ns + 10; // 初始化为不可能达到的数
int st = -1;
for(int l = 0, r = 0; r < ns; ++r) { // 每次循环右移r指针拓展窗口
if(ht.count(s[r])) { // 如果当前字符是t中出现的
if(ht[s[r]] > 0) need--; // 当前窗口内该字符数量还没满足的话,need--,说明又可以满足一个
ht[s[r]]--; // 当前窗口仍旧需要该字符数量-1
}
while(need == 0) { // 满足题意的窗口目标:need == 0
if(r - l + 1 < min_len) { // 更新最小字串
st = l;
min_len = min(min_len, r - l + 1);
ans = s.substr(st, min_len);
}
// 当前窗口已经满足,开始右移l指针缩小窗口
if(ht.count(s[l])) { // 如果当前l指针指向的是t中出现的字符
if(ht[s[l]] == 0) need++; // 如果当前窗口只是刚好符合s[l]字符的数量要求,那么右移后势必会不满足,所需要的字符数need + 1
ht[s[l]]++; // 当前窗口仍旧需要该字符数量-1
}
l++; // 注意为什么l在这里+1,因为考虑到窗口内存在不是t中的字符,这样在当前while循环中可以不断缩小窗口实现实时更新,比如t字符为ABC,当前窗口内为xxxBCA,这样会不断缩小,得到当前r指针情况下的最小的窗口。
}
}
return ans;
}
};
面试题 17.18. 最短超串
直接套模板
class Solution {
public:
vector<int> shortestSeq(vector<int>& big, vector<int>& small) {
int nb = big.size(), ns = small.size();
unordered_map<int, int> cnt;
for (int i = 0; i < ns; ++i) cnt[small[i]]++;
int need = ns;
int min_len = nb + 1, st = -1, ed = -1;
for(int l = 0, r = 0; r < nb; ++r) {
if(cnt.count(big[r])) {
if(cnt[big[r]] > 0) need--;
cnt[big[r]]--;
}
while(need == 0) {
if(r - l + 1 < min_len) {
st = l, ed = r;
min_len = min(min_len, r - l + 1);
}
if(cnt.count(big[l])) {
if(cnt[big[l]] == 0) need++;
cnt[big[l]]++;
}
l++;
}
}
vector<int> ans;
if(~st && ~ed) {
ans.push_back(st);
ans.push_back(ed);
}
return ans;
}
};
题型2.2:定长滑动窗口
567. 字符串的排列
在我看来,区别仅仅是左指针l的位置在每一步循环中直接实时更新赋值即可
class Solution {
public:
bool checkInclusion(string s1, string s2) {
int n1 = s1.size(), n2 = s2.size();
if(n1 > n2) return false;
unordered_map<char, int> cnt;
for (int i = 0; i < n1; ++i) cnt[s1[i]]++;
int need = n1;
for(int r = 0; r < n2; ++r) {
if(cnt.count(s2[r])) {
if(cnt[s2[r]] > 0) need--;
cnt[s2[r]]--;
}
int l = r - n1 + 1; // 注意这一步
if(l >= 0) { // 判断定长的窗口是否形成
if(need == 0) return true;
if(cnt.count(s2[l])) {
if(cnt[s2[l]] >= 0) need++;
cnt[s2[l]]++;
}
}
}
return false;
}
};
题型3:滑动窗口+二分
220. 存在重复元素 III
还是走三部曲,外加用一个multiset存储窗口内的元素,并在每次加入元素时二分查找set是否存在满足题意的元素即可。
typedef long long ll;
class Solution {
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
int n = nums.size();
multiset<ll> s;
for(int r = 0; r < n; ++r) {
ll lower = (ll)nums[r] - t, upper = (ll)nums[r] + t;
auto it = s.lower_bound(lower);
if(it != s.end() && *it <= upper) return true;
s.insert(nums[r]);
if(r - k >= 0) {
auto it = s.find(nums[r - k]);
s.erase(it);
}
}
return false;
}
};