滑动窗口常见题型技巧 | 青训营

157 阅读3分钟

前言

写下这篇文章的原因主要是本人在力扣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;
    }
};

参考

[1]『 一招吃遍七道 』滑动窗口的应用