滑动窗口

滑动窗口

  • 维持左右边界都不回退的一段范围,求解子数组、子串相关问题
  • 求子数组以每个位置开头或者结尾情况下的答案
  • 找范围和答案指标之间的单调性关系

209. 长度最小的子数组

#include <vector>
#include <valarray>

using namespace std;

class Solution {
public:
    // 时间复杂度 O(n)
    int minSubArrayLen(int target, vector<int> &nums) {
        int res = INT_MAX;
        for (int l = 0, r = 0, sum = 0; r < nums.size(); r++) {
            // 查看以 nums[r] 结尾的子数组中是否有符合条件的
            sum += nums[r];
            // 尽量减小以 nums[r] 结尾的子数组长度
            while (sum - nums[l] >= target) {
                sum -= nums[l];
                l++;
            }
            // 记录所有位置结尾中的最短数组
            if (sum >= target) res = min(res, r - l + 1);
        }
        return res == INT_MAX ? 0 : res;
    }
};

3. 无重复字符的最长子串

  • 记录字符上一次出现的位置
#include <vector>
#include <string>

using namespace std;

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int len = s.length();
        int res = 0;
        // 记录每个字符上次出现的位置
        vector<int> last(128, -1);
        for (int l = 0, r = 0; r < len; r++) {
            // 更新窗口左边界
            l = max(l, last[s[r]] + 1);
            // 记录最大窗口长度
            res = max(res, r - l + 1);
            // 更新当前字符最后一次出现的位置
            last[s[r]] = r;
        }
        return res;
    }
};
  • 记录不重复子串中有哪些字符
#include <vector>
#include <string>

using namespace std;

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        // 记录当前在滑动窗口中的字符
        vector<bool> entered(128, false);
        int res = 0;
        
        for (int l = 0, r = 0; r < s.length(); r++) {
            char ch = s[r];
            if (entered[ch]) {
                // 已经在窗口中,从窗口左边弹出元素,直到弹出 ch
                while (l < r && s[l] != ch) {
                    entered[s[l]] = false;
                    l++;
                }
                l++;
            } else {
                // 不在窗口中
                entered[ch] = true;
                res = max(res, r - l + 1);
            }
        }
        return res;
    }
};

76. 最小覆盖子串

#include <vector>
#include <string>

using namespace std;

class Solution {
public:
    // 题目默认如果存在则唯一
    string minWindow(string s, string t) {
        int lenS = s.length();
        int lenT = t.length();
        if (lenS < lenT) return "";

        // 负数表示 t 中字符在 s 中需要出现的次数,非负数表示其他字符在 s 中出现的次数
        vector<int> map(256, 0);
        for (char ch: t) map[ch]--;
        // 最小覆盖子串的长度
        int len = INT_MAX;
        // 最小覆盖子串的起始下标
        int start = 0;
        // 需要出现的各个字符的总个数
        int count = lenT;

        for (int r = 0, l = 0; r < lenS; ++r) {
            // 是 t 中的字符,则出现总次数减一
            if (map[s[r]] < 0) count--;
            // 当前字符出现次数加一
            map[s[r]]++;
            
            // 窗口已经包含 t 中所有字符,即 s 中已经出现了覆盖子串
            if (count == 0) {
                // 大于 0 说明可以从窗口左边移除一部分重复的字符,从而缩小窗口大小
                // 小于等于 0 时不能移除,否则就凑不齐t中的字符
                while (map[s[l]] > 0) {
                    map[s[l]]--;
                    l++;
                }
                if (r - l + 1 < len) {
                    // 记录窗口大小
                    len = r - l + 1;
                    // 记录窗口起始位置,用于返回最小覆盖子串
                    start = l;
                }
            }
        }
        if (len == INT_MAX) return "";
        return s.substr(start, len);
    }
};

134. 加油站

#include <vector>
#include <string>

using namespace std;

class Solution {
public:
    int canCompleteCircuit(vector<int> &gas, vector<int> &cost) {
        int n = gas.size();
        // 汽油余量前缀和
        int prefixSum = 0;
        // 当前窗口大小
        int len = 0;
        // 从每个位置开始尝试
        for (int l = 0, r; l < n; ++l) {
            while (prefixSum >= 0) {
                // 可以绕一圈
                if (len == gas.size()) return l;
                // 计算窗口右边界
                r = (l + len) % n;
                // 窗口扩大,r 移入窗口
                len++;
                // 到达 gas[r] 后,计算到达再后面一站的汽油余量
                prefixSum += gas[r] - cost[r];
            }
            // 移除窗口左侧
            len--;
            // 前缀和也要去除掉 l 位置的影响
            prefixSum -= gas[l] - cost[l];
        }
        return -1;
    }
};
#include <vector>

using namespace std;

class Solution {
public:
    int canCompleteCircuit(vector<int> &gas, vector<int> &cost) {
        int n = gas.size();
        // 从每个位置开始尝试
        for (int l = 0, r = 0, gasLeft; l < n; l = r + 1, r = l) {
            // 汽油余量
            gasLeft = 0;
            // 如果能到达下一站
            while (gasLeft + gas[r % n] - cost[r % n] >= 0) {
                // 可以绕一圈
                if (r - l == n) return l;
                // 到达 gas[r] 后的汽油余量
                gasLeft += gas[r % n] - cost[r % n];
                r++;
            }
        }
        return -1;
    }
};

1234. 替换子串得到平衡字符串

#include <vector>
#include <string>

using namespace std;

class Solution {
public:
    // 能否通过修改长度为 len 的滑动窗口,把字符串变成平衡字符串
    bool ok(vector<int> &counts, int len, int require) {
        for (int i = 0; i < 4; ++i) {
            // 窗口外的字符频率若超过 require,则无法消去多出来的字符
            if (counts[i] > require) return false;
            // 用长度 len 的窗口补齐每个字符缺失的个数 require - counts[i]
            len -= require - counts[i];
        }
        // 窗口刚好用完
        return len == 0;
    }

    int balancedString(string s) {
        int lenS = s.length();
        // 每种字符必须出现的次数
        int require = lenS / 4;
        // Q W E R转换成 0 1 2 3
        vector<int> nums(lenS);
        // 统计窗口外字符出现次数
        vector<int> counts(4, 0);
        for (int i = 0; i < lenS; ++i) {
            nums[i] = s[i] == 'Q' ? 0 : (s[i] == 'W' ? 1 : (s[i] == 'E' ? 2 : 3));
            counts[nums[i]]++;
        }

        // 最多调整整个数组
        int res = lenS;
        // 窗口 [l, r)
        for (int l = 0, r = 0; l < lenS; ++l) {
            while (!ok(counts, r - l, require) && r < lenS) {
                // 窗口右边移入,移入的字符在 counts 中的计数减一
                counts[nums[r]]--;
                r++;
            }
            if (ok(counts, r - l, require))
                res = min(res, r - l);
            // 窗口左边移出,移出的字符在 counts 中的计数加一
            counts[nums[l]]++;
        }
        return res;
    }
};
#include <vector>
#include <string>

using namespace std;

class Solution {
public:
    int balancedString(string s) {
        int lenS = s.length();
        // 每种字符必须出现的次数
        int require = lenS / 4;
        // Q W E R转换成 0 1 2 3
        vector<int> nums(lenS);
        // 统计窗口外字符出现次数
        vector<int> counts(4, 0);
        for (int i = 0; i < lenS; ++i) {
            nums[i] = s[i] == 'Q' ? 0 : (s[i] == 'W' ? 1 : (s[i] == 'E' ? 2 : 3));
            counts[nums[i]]++;
        }

        int debt = 0;
        for (int i = 0; i < 4; i++) {
            if (counts[i] < require) {
                // 低于需要出现的次数就改成 0
                counts[i] = 0;
            } else {
                // 高于需要出现的次数就改成负数,表示多出来的个数
                counts[i] = require - counts[i];
                // 计算一共多出来多少个
                debt -= counts[i];
            }
        }
        // 一个都没多,说明刚好平衡
        if (debt == 0) return 0;

        int res = INT_MAX;
        // todo
        for (int l = 0, r = 0; r < lenS; r++) {
            if (counts[nums[r]]++ < 0) debt--;
            if (debt == 0) {
                while (counts[nums[l]] > 0)
                    counts[nums[l++]]--;
                res = min(res, r - l + 1);
            }
        }
        return res;
    }
};

992. K 个不同整数的子数组

#include <vector>

using namespace std;

class Solution {
public:
    vector<int> counts;

    // 返回 nums 的所有子数组中,数字种类不超过 k 的子数组个数
    int numsOfMostKinds(vector<int> &nums, int k) {
        counts.clear();
        counts.resize(20001, 0);

        int res = 0;
        for (int l = 0, r = 0, types = 0; r < nums.size(); r++) {
            // 窗口右侧移入 nums[r]
            // 种类数加一
            if (counts[nums[r]] == 0) types++;
            // 词频加一
            counts[nums[r]]++;

            // 种类数超过了,需要从窗口左侧移除
            while (types > k) {
                // 词频减一
                counts[nums[l]]--;
                // 刚好把这个字符全部移除
                if (counts[nums[l]] == 0) types--;
                l++;
            }
            // 累加的是以 nums[r] 结尾,数字种类不超过 k 的子数组个数
            res += r - l + 1;
        }
        return res;
    }

    int subarraysWithKDistinct(vector<int> &nums, int k) {
        return numsOfMostKinds(nums, k) - numsOfMostKinds(nums, k - 1);
    }
};

395. 至少有 K 个重复字符的最长子串

#include <vector>
#include <string>

using namespace std;

class Solution {
public:
    int longestSubstring(string s, int k) {
        int len = s.length();
        vector<int> counts;

        int res = 0;
        // 子串只有 require 种字符,每种字符都必须大于等于 k 次,返回这样的最长子串
        for (int require = 1; require <= 26; ++require) {
            counts.clear();
            counts.resize(256, 0);

            // 窗口中字符种类总数
            int types = 0;
            // 窗口中字符出现次数大于等于 k 的种类总数
            int satisfy = 0;

            for (int l = 0, r = 0; r < len; r++) {
                counts[s[r]]++;
                // 新出现了一个种类
                if (counts[s[r]] == 1) types++;
                // 新达标了一个种类
                if (counts[s[r]] == k) satisfy++;

                // 字符种类超了,开始移除左边字符
                while (types > require) {
                    if (counts[s[l]] == 1) types--;
                    if (counts[s[l]] == k) satisfy--;
                    // 窗口左侧移出字符
                    counts[s[l]]--;
                    l++;
                }
                // 子串以 r 位置结尾,且种类等于 require 的最大长度
                if (satisfy == require) res = max(res, r - l + 1);
            }
        }
        return res;
    }
};
posted @ 2024-10-07 16:07  n1ce2cv  阅读(5)  评论(0编辑  收藏  举报