滑动窗口

滑动窗口一般分为滑动窗口和固定窗口。
判断是否需要用滑动窗口:
对象是数组、链表、字符串等线性结构
题目要求求具有最短、最长、或固定长度的子序列
滑动窗口问题一般具有以下结构:
先移动右窗口边界,逐步扩大右窗口,直到达到临界条件,然后循环移动左窗口,并更新问题答案,直到又不满足临界条件需要移动右窗口边界为止;如此循环
而且一般右窗口通过for循环来实现移动,左窗口通过if或者while循环来实现。

《挑战程序设计竞赛》这本书中把滑动窗口叫做「虫取法」,我觉得非常生动形象。
因为滑动窗口的两个指针移动的过程和虫子爬动的过程非常像:前脚不动,把后脚移动过来;后脚不动,把前脚向前移动。
分享一个滑动窗口的模板,能解决大多数的滑动窗口问题:

def findSubArray(nums):
    N = len(nums) # 数组/字符串长度
    left, right = 0, 0 # 双指针,表示当前遍历的区间[left, right],闭区间
    sums = 0 # 用于统计 子数组/子区间 是否有效,根据题目可能会改成求和/计数
    res = 0 # 保存最大的满足题目要求的 子数组/子串 长度
    while right < N: # 当右边的指针没有搜索到 数组/字符串 的结尾
        sums += nums[right] # 增加当前右边指针的数字/字符的求和/计数
        while 区间[left, right]不符合题意:# 此时需要一直移动左指针,直至找到一个符合题意的区间
            sums -= nums[left] # 移动左指针前需要从counter中减少left位置字符的求和/计数
            left += 1 # 真正的移动左指针,注意不能跟上面一行代码写反
        # 到 while 结束时,我们找到了一个符合题意要求的 子数组/子串
        res = max(res, right - left + 1) # 需要更新结果
        right += 1 # 移动右指针,去探索新的区间
    return res

滑动窗口中用到了左右两个指针,它们移动的思路是:以右指针作为驱动,拖着左指针向前走。右指针每次只移动一步,而左指针在内部 while 循环中每次可能移动多步。右指针是主动前移,探索未知的新区域;左指针是被迫移动,负责寻找满足题意的区间。

模板的整体思想是:

定义两个指针 left 和 right 分别指向区间的开头和结尾,注意是闭区间;定义 sums 用来统计该区间内的各个字符出现次数;
第一重 while 循环是为了判断 right 指针的位置是否超出了数组边界;当 right 每次到了新位置,需要增加 right 指针的求和/计数;
第二重 while 循环是让 left 指针向右移动到 [left, right] 区间符合题意的位置;当 left 每次移动到了新位置,需要减少 left 指针的求和/计数;
在第二重 while 循环之后,成功找到了一个符合题意的 [left, right] 区间,题目要求最大的区间长度,因此更新 res 为 max(res, 当前区间的长度) 。
right 指针每次向右移动一步,开始探索新的区间。
模板中的 sums 需要根据题目意思具体去修改,本题是求和题目因此把sums 定义成整数用于求和;如果是计数题目,就需要改成字典用于计数。当左右指针发生变化的时候,都需要更新 sums 。

另外一个需要根据题目去修改的是内层 while 循环的判断条件,即: 区间 [left, right][left,right] 不符合题意 。

滑动窗口:

//1208.尽可能使字符串相等
/*

  • 给你两个长度相同的字符串,s 和 t。
    将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。
    用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。
    如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。
    如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。

示例 1:
输入:s = “abcd”, t = “bcdf”, maxCost = 3
输出:3
解释:s 中的 “abc” 可以变为 “bcd”。开销为 3,所以最大长度为 3。
示例 2:

输入:s = “abcd”, t = “cdef”, maxCost = 3
输出:1
解释:s 中的任一字符要想变成 t 中对应的字符,其开销都是 2。因此,最大长度为 1。
示例 3:

输入:s = “abcd”, t = “acde”, maxCost = 0
输出:1
解释:a -> a, cost = 0,字符串未发生变化,所以最大长度为 1。*/

class Solution {
public:
    int equalSubstring(string s, string t, int maxCost) {
        if (s.size() != t.size() || s.size() == 0) {
            return 0;
        }
        int n = s.size();
        vector<int> dis(n, 0);
        for (int i = 0; i < n; i++) {
            dis[i] = abs(s[i] - t[i]);
        }
        // 转化未求dis数组中最长连续元素和不大于maxCost
        int l = 0, sum = 0, res = 0;
        for (int r = 0; r < n; r++) {
            sum += dis[r];
            while (sum > maxCost) {
                sum -= dis[l];
                l++;
            }
            res = max(res, r-l+1);
        }
        return res;
    }
};

//Leetcode 209. 长度最小的子数组
/*

  • 给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组。如果不存在符合条件的连续子数组,返回 0。示例:
    输入: s = 7, nums = [2,3,1,2,4,3]
    输出: 2
    解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。
    */
int minSubArrayLen(int s, vector<int>& nums) {
    int n = nums.size();
    int l = 0, sum = 0, res = n+1;
    for (int r = 0; r < n; r++) {
        sum += nums[r];
        while(sum >= s) {
            res = min(res, r-l+1);
            sum -= nums[l];
            l++;
        }
    }
    return res == n+1 ? 0 : res;
}

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

  • 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
    示例 1:
    输入: “abcabcbb”
    输出: 3
    解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
    示例 2:
    输入: “bbbbb”
    输出: 1
    解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
    示例 3:
    输入: “pwwkew”
    输出: 3
    解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
    请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
    */
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int mp[256] = {0};
        int n = s.size();
        if (n <= 0) {
            return 0;
        }
        int l = 0, res = 0;
        for (int r = 0; r < n; r++) {
            mp[s[r]]++;
            while (mp[s[r]] > 1) {
                mp[s[l]]--;
                l++;
            }
            res = max(res, r-l+1);
        }
        return res;
    }
};

//Leetcode 1004. 最大连续1的个数 III
给定一个由若干 0 和 1 组成的数组 A,我们最多可以将 K 个值从 0 变成 1 。
返回仅包含 1 的最长(连续)子数组的长度。

示例 1:
输入:A = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:
[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。
示例 2:
输入:A = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:10
解释:
[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 10。

class Solution {
public:
    int longestOnes(vector<int>& A, int K) {
        int res = 0, zeros = 0, l = 0;
        for (int r = 0; r < A.size(); r++) {
            if (A[r] == 0) {
                zeros++;
            }
            while (zeros > K) {
                if (A[l++] == 0) {
                    zeros--;
                }
            }
            res = max(res, r - l + 1);
        }
        return res;
    }
};

//340 至多包含K个不同字符的最长子串
/*

  • 给定一个字符串 s ,找出 至多 包含 k 个不同字符的最长子串 T。
    示例 1:
    输入: s = “eceba”, k = 2
    输出: 3
    解释: 则 T 为 “ece”,所以长度为 3。

示例 2:
输入: s = “aa”, k = 1
输出: 2
解释: 则 T 为 “aa”,所以长度为 2。*/

class Solution {
public:
    int lengthOfLongestSubstringKDistinct(string s, int k) {
        int mp[256] = {0};
        int l = 0;
        int ans = 0;
        int diff = 0;
        for (int r = 0; r < s.size(); r++) {
            if (mp[s[r]] == 0) {
                diff++;
                mp[s[r]]++;
            }
            while (diff > k) {
                if (mp[s[l]] == 1) {
                    diff--;
                }
                mp[s[l++]]--;
            }
            ans = max(ans, r-l+1);
        }
        return ans;
    }
};

固定窗口:

固定窗口是n,固定窗口的右边界到终点的位置至少是n,
// 1151.最少交换次数来组合所有的1
/*

  • 给出一个二进制数组 data,你需要通过交换位置,将数组中 任何位置 上的 1 组合到一起,并返回所有可能中所需 最少的交换次数。
  • 示例 1:
    输入:[1,0,1,0,1]
    输出:1
    解释:
    有三种可能的方法可以把所有的 1 组合在一起:
    [1,1,1,0,0],交换 1 次;
    [0,1,1,1,0],交换 2 次;
    [0,0,1,1,1],交换 1 次。
    所以最少的交换次数为 1。

示例 2:
输入:[0,0,0,1,0]
输出:0
解释:
由于数组中只有一个 1,所以不需要交换。

示例 3:
输入:[1,0,1,0,1,0,0,1,1,0,1]
输出:3
解释:
交换 3 次,一种可行的只用 3 次交换的解决方案是 [0,0,0,0,0,1,1,1,1,1,1]。

提示:
1 <= data.length <= 10^5
0 <= data[i] <= 1
/
/

  • 统计数组中有m个1,以m为滑动窗口的长度,统计窗口中1的个数为n,需要交换的次数就是m-n*/
class Solution {
public:
    int minSwaps(vector<int> &data) {
        int sz = data.size();
        int m = 0; //数组中1的个数
        for (int i = 0; i < sz; i++) {
            if (data[i] == 1) {
                m++;
            }
        }
        int n = 0;
        int ans = sz;
        int l = 0;
        int r = 0;
        while (r < m-1) {
            if (data[r++]) {
                n++;
            }
        }
        for (; r < n; r++) {
            if (data[r]) {
                n++;
            }
            ans = min(ans, m-n);
            if (data[i++]) {
                n--;
            }
        }
        return ans;
    }
};

//159.至多包含两个不同字符的最长子串
/*

  • 给定一个字符串 s ,找出 至多 包含 2 个不同字符的最长子串 T。
    示例 1:
    输入: s = “eceba”
    输出: 3
    解释: 则 T 为 “ece”,所以长度为 3。

示例 2:
输入: s = “ccaabbb”
输出: 5
解释: 则 T 为 “aabbb”,所以长度为 5。*/

//1100.长度为K的无重复字符子串
/*

  • 给你一个字符串 S,找出所有长度为 K 且不含重复字符的子串,请你返回全部满足要求的子串的 数目。
    示例 1:
    输入:S = “havefunonleetcode”, K = 5
    输出:6
    解释:
    这里有 6 个满足题意的子串,分别是:‘havef’,‘avefu’,‘vefun’,‘efuno’,‘etcod’,‘tcode’。

示例 2:
输入:S = “home”, K = 5
输出:0
解释:
注意:K 可能会大于 S 的长度。在这种情况下,就无法找到任何长度为 K 的子串。*/

class Solution {//C++
public:
    int numKLenSubstrNoRepeats(string S, int K) {
        int i = 0, j = 0, n = S.size(), count = 0;
        unordered_set<char> set;
        for (; j < n; j++) {
            while(set.size() >= K || set.count(S[j])) {
                set.erase(S[i++]);
            }
            set.insert(S[j]);
            if (set.size() == K) {
                count++;
            }
        }
        return count;
    }
};

//leetcode76 最小覆盖子串
/*

  • 给你一个字符串S,一个字符串T,请在字符串S里面找出包含T所有字母的最小子串
  • 示例:
  • 输入S=“ADOBECODEBANC” T=“ABC”
  • 输出"BANC"*/

/*

  • 当我们发现某个字符在window的数量满足了need的需要,就要更新valid,表示有一个字符已经满足要求。而且,你能发现,两次对窗口内数据的更新操作是完全对称的。*/
string minWindow(string s, string t) {
    unordered_map<char, int> need, window;
    for (auto c : t) {
        need[c]++;
    }
    int left = 0, right = 0;//[left, right)
    int valid = 0; //表示窗口中满足need条件的字符个数,即若valid == need.size(),则窗口已经满足条件,完全覆盖串T
    int start = 0, len = INT_MAX; //记录最小覆盖子串的起始索引和长度
    while (right < s.size()) {
        char c = s[right];//c是将移入窗口的字符
        right++;// 右移窗口
        //进行窗口内数据的一系列更新
        if (need.count(c)) {
            window[c]++;
            if (window[c] == need[c]) {
                valid++;
            }
        }
        // 判断左侧窗口是否要收缩
        while (valid == need.size()) {
            //更新最小覆盖子串
            if (right - left < len) {
                start = left;
                len = right - left;
            }
            char d = s[left];// d是将要移除窗口的字符
            left++;// 左移窗口
            if (need.count(d)) {
                if (window[d] == need[d]) {
                    valid--;
                }
                window[d]--;
            }
        }
    }
    return len == INT_MAX ? "" : s.substr(start, len);
}

//567.字符串的排列
/*

  • 给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
    换句话说,第一个字符串的排列之一是第二个字符串的子串。
    示例 1:
    输入: s1 = “ab” s2 = “eidbaooo”
    输出: True
    解释: s2 包含 s1 的排列之一 (“ba”).

示例 2:
输入: s1= “ab” s2 = “eidboaoo”
输出: False*/

/*

  • 对于这道题的解法代码,基本上和最小覆盖子串一模一样,只需要改变两个地方:
    1、本题移动left缩小窗口的时机是窗口大小大于t.size()时,因为排列嘛,显然长度应该是一样的。
    2、当发现valid == need.size()时,就说明窗口中就是一个合法的排列,所以立即返回true。
    至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。*/
class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        unordered_map<char, int> need, window;
        for (auto c : s1) {
            need[c]++;
        }
        int left = 0, right = 0;
        int valid = 0;
        while (right < s2.size()) {
            char c = s2[right];
            right++;
            if (need.count(c)) {
                window[c]++;
                if (window[c] == need[c]) {
                    valid++;
                }
            }

            while (right - left >= s1.size()) {
                if (valid == need.size()) {
                    return true;
                }
                char d = s2[left];
                left++;
                if (need.count(d)) {
                    if (window[d] == need[d]) {
                        valid--;
                    }
                    window[d]--;
                }
            }
        }
        return false;
    }
};

//438.找到字符串中所有字母异位词
/*

  • 给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
    字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。

说明:
字母异位词指字母相同,但排列不同的字符串。
不考虑答案输出的顺序。

示例 1:
输入:
s: “cbaebabacd” p: “abc”
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。

示例 2:
输入:
s: “abab” p: “ab”
输出:
[0, 1, 2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的字母异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的字母异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的字母异位词。*/

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        unordered_map<char, int> need, window;
        for (auto c : p) {
            need[c]++;
        }
        int left = 0, right = 0;
        int valid = 0;
        vector<int> res;
        while (right < s.size()) {
            char c = s[right];
            right++;
            if (need.count(c)) {
                window[c]++;
                if (window[c] == need[c]) {
                    valid++;
                }
            }

            while (right - left >= p.size()) {
                if (valid == need.size()) {
                    res.push_back(left);
                }
                char d = s[left];
                left++;
                if (need.count(d)) {
                    if (window[d] == need[d]) {
                        valid--;
                    }
                    window[d]--;
                }
            }
        }
        return res;
    }
};

//424. 替换后的最长重复字符
给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度。

注意:字符串长度 和 k 不会超过 104。
示例 1:
输入:s = “ABAB”, k = 2
输出:4
解释:用两个’A’替换为两个’B’,反之亦然。
示例 2:
输入:s = “AABABBA”, k = 1
输出:4
解释:
将中间的一个’A’替换为’B’,字符串变为 “AABBBBA”。
子串 “BBBB” 有最长重复字母, 答案为 4。

class Solution {
public:
    int characterReplacement(string s, int k) {
        int n = s.size();
        int l = 0, ans = 0;
        int maxn = 0;
        unordered_map<char, int> mp;
        for (int r = 0; r < n; r++) {
            mp[s[r]]++;
            maxn = max(maxn, mp[s[r]]);
            while (r-l+1 > maxn + k) {
                mp[s[l]]--;
                l++;  
            }
            ans = max(ans, r - l + 1);
        }
        return ans;
    }
};

//1423. 可获得的最大点数
几张卡牌 排成一行,每张卡牌都有一个对应的点数。点数由整数数组 cardPoints 给出。
每次行动,你可以从行的开头或者末尾拿一张卡牌,最终你必须正好拿 k 张卡牌。
你的点数就是你拿到手中的所有卡牌的点数之和。
给你一个整数数组 cardPoints 和整数 k,请你返回可以获得的最大点数。
示例 1:
输入:cardPoints = [1,2,3,4,5,6,1], k = 3
输出:12
解释:第一次行动,不管拿哪张牌,你的点数总是 1 。但是,先拿最右边的卡牌将会最大化你的可获得点数。最优策略是拿右边的三张牌,最终点数为 1 + 6 + 5 = 12 。

示例 2:
输入:cardPoints = [2,2,2], k = 2
输出:4
解释:无论你拿起哪两张卡牌,可获得的点数总是 4 。

示例 3:
输入:cardPoints = [9,7,7,9,7,7,9], k = 7
输出:55
解释:你必须拿起所有卡牌,可以获得的点数为所有卡牌的点数之和。

示例 4:
输入:cardPoints = [1,1000,1], k = 1
输出:1
解释:你无法拿到中间那张卡牌,所以可以获得的最大点数为 1 。

示例 5:
输入:cardPoints = [1,79,80,1,1,1,200,1], k = 3
输出:202

class Solution {
public:
    int maxScore(vector<int>& cardPoints, int k) {
        int n = cardPoints.size();
        int l = 0, sum = 0, totalsum = 0;
        for (int r = 0; r < n-k; r++) {
            sum += cardPoints[r];
            totalsum += cardPoints[r];
        }
        int minsum = sum;
        for (int r = n-k; r < n; r++) {
            sum = sum + cardPoints[r] - cardPoints[r-(n-k)];
            totalsum += cardPoints[r];
            minsum = min(minsum, sum);
        }
        return totalsum - minsum;
    }
};
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值