滑动窗口技巧

滑动窗口技巧:维持左右边界都不回退的一段范围,解决子数组或字串问题

滑动窗口的关键:找到范围和答案指标之间的单调关系

单调性关系:就是窗口越大,越不容易/容易满足题目的条件,这就是单调性。从而当窗口维持的范围不满足/满足条件时,需要移动窗口的左边界利用单调性来继续探索答案

滑动的过程:滑动窗口可以用变量或结构来维护信息

解题的主要流程:求子数组在每个位置的开头或结尾情况下的答案

例题:

长度最小的子数组  求和最小为target的最小子数组

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int ans = INT_MAX;
        int l = 0, r = 0, sum = 0;
        for (int r = 0; r < nums.size(); r++) {
            sum += nums[r];
            while (sum - nums[l] >= target) {
                sum -= nums[l++];
            }
            if (sum >= target)
                ans = min(ans, r - l + 1);
        }
        return ans == INT_MAX ? 0 : ans;
    }
};

因为数组中的数全部为正数,所以当子数组的和大于target时,右边界再向右移动和会更大,范围越大,和越大,范围和答案之间存在单调性关系。此时固定右边界,左边界向右移动尝试最小长度



最长无重复字符的字串    求字符串中不含重复字符的最长字串

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        vector<int> last(256, -1);
        int ans = 0;
        for (int l = 0, r = 0; r < s.size(); r++) {
            l = max(l, last[s[r]] + 1);
            last[s[r]] = r;
            ans = max(ans, r - l + 1);
        }
        return ans;
    }
};

范围越大,出现重复字符可能就会越大,用数组来记录每种字符最近出现的位置,当窗口滑到窗口内已有的字符时,以右边界为止,左边界移动到上一次此字符出现的位置。每次记录最大长度

最小覆盖字串  

class Solution {
public:
    string minWindow(string s, string t) {
        vector<int> nums(256, 0);
        string ans;
        int length = INT_MAX;
        for (int i = 0; i < t.size(); i++)
            nums[t[i]]--;
        int debts = t.size();
        for (int l = 0, r = 0; r < s.size(); r++) {
            if (nums[s[r]]++ < 0)
                debts--;
            if (debts == 0) {
                while (nums[s[l]] > 0)
                    nums[s[l++]]--;
                if (r - l + 1 < length) {
                    length = r - l + 1;
                    ans = s.substr(l, r - l + 1);
                }
            }
        }
        return ans;
    }
};

用数组记录每种字符的"欠债"数量,debts记录还欠的字符数量;范围越大,覆盖的字串越长,当debts为0时,此时左边界上的字符如果"欠债"的数量大于0,以右右边界为止,可以移动左边界来减小长度记录答案

加油站  判断车能不能走一圈

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        vector<int> differ(gas.size(), 0);
        for (int i = 0; i < gas.size(); i++)
            differ[i] = gas[i] - cost[i];
        int sum = 0;
        int n = gas.size();
        int l = 0;
        int len = 0;
        for (int r = 0; l < n; l++) {
            while (sum >= 0) {
                if (len == n)
                    return l;
                r = (l + (len++)) % n;
                sum += differ[r];
            }
            len--;
            sum -= differ[l];
        }
        return -1;
    }
};

当sum小于0时,此时以左边界开头的情况要向右移动至sum大于等于0;用sum维护窗口的和,len记录窗口的大小

平衡字符串

class Solution {
public:
    int cnt[4] = {0};

    bool judge(int sum) {
        for (int i = 0; i < 4; i++) {
            if (cnt[i] > sum / 4)
                return false;
        }
        return true;
    }
    bool special(int sum) {
        for (int i = 0; i < 4; i++) {
            if (cnt[i] != sum / 4)
                return false;
        }
        return true;
    }
    int balancedString(string s) {
        int sum = s.size();
        vector<int> arr(sum, 0);
        for (int i = 0; i < s.size(); i++) {
            if (s[i] == 'Q')
                arr[i] = 0;
            else if (s[i] == 'W')
                arr[i] = 1;
            else if (s[i] == 'E')
                arr[i] = 2;
            else
                arr[i] = 3;
        }
        for (int i = 0; i < sum; i++)
            cnt[arr[i]]++;
        if (special(sum))
            return 0;
        int ans = INT_MAX;
        for (int l = 0, r = 0; r < sum; r++) {
            cnt[arr[r]]--;
            while (r >= l && judge(sum)) {
                ans = min(ans, r - l + 1);
                cnt[arr[l]]++;
                l++;
            }
        }
        return ans;
    }
};

用cnt数组记录窗口以外的字符的词频,如果窗口外每个字符的词频都小于等于sum/4,则窗口包含的字串是一个答案,此时以右边界为止,缩小左边界寻求答案

不同整数的子数组

class Solution {
public:
    int get_limit(vector<int>& nums, int k) {
        int cnt[20004] = {0};
        int ans = 0;
        int rem = 0;
        for (int l = 0, r = 0; r < nums.size(); r++) {
            if (cnt[nums[r]]++ == 0)
                rem++;
            while (rem > k) {
                if (--cnt[nums[l++]] == 0)
                    rem--;
            }
            ans += r - l + 1;
        }
        return ans;
    }
    int subarraysWithKDistinct(vector<int>& nums, int k) {
        return get_limit(nums, k) - get_limit(nums, k - 1);
    }
};

  如果按照常规思路窗口右边界移动,当种类数满足时,个数增加;当种类数大于规定值时,窗口左边界移动直至种类数为规定值,但这样窗口内会有一些满足条件的字串不会被统计,所以此题需要转化。

  求整个数组满足字符种类为k的子数组的个数,可以先求不超过k个的子数组的个数,再求不超过k-1个的子数组的个数。求不超过k个的子数组,右边界移动,只要不超过k,那么以右边界为止的子数组右r-l+1个。随着窗口的增大,超过k的可能性越大,当超过k时,左边界移动。窗口移动的过程中用rem记录种类数,cnt统计数字的频率。

至少有k个重复字符的字串的最大长度

class Solution {
public:
    int get_nums(string s, int k, int require) {
        int cnt[256] = {0};
        int ans = 0;
        int satisfy = 0, collect = 0;
        for (int l = 0, r = 0; r < s.size(); r++) {
            if (cnt[s[r]]++ == 0)
                collect++;
            if (cnt[s[r]] == k)
                satisfy++;
            while (collect > require && l <= r) {
                if (--cnt[s[l]] == 0)
                    collect--;
                if (cnt[s[l]] == k - 1)
                    statis--;
                l++;
            }
            if (satisfy == require)
                ans = max(ans, r - l + 1);
        }
        return ans;
    }
    int longestSubstring(string s, int k) {
        int ans = 0;
        for (int i = 1; i <= 26; i++) {
            ans = max(ans, get_nums(s, k, i));
        }
        return ans;
    }
};

  此题如果直接求超过k的字串,是不好求的,因为也没规定字串中有多少个不同的字符,这样就使的窗口滑动时,不好确定什么时候边界移动的条件。因为题目中字符串都是小写字母组成,所以可以枚举字符的种类数,规定字串中字符的个数,求最大值。

  规定好字符的个数,就可以滑动窗口了。此题和上一题不同,上一题只要每个数字满足条件就统计,但此题求最大的字串,不用考虑窗口内满足条件的字串。窗口越大,越容易超出规定的字符个数;当超出规定的字符个数时,左边界移动直至右边界可以继续移动。滑动的过程中,用collect记录收集的不同字符的个数,satisfy记录频率超过k的字符的个数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值