具体算法思想可参考该文章: https://www.cnblogs.com/huansky/p/13488234.html
一、定长滑动窗口
模板
// 固定窗口大小为 k string s; // 在 s 中寻找窗口大小为 k 时的所包含最大元音字母个数 int right = 0; while(right < s.size()) { window.add(s[right]); right++; // 如果符合要求,说明窗口构造完成, if (right>=k) { // 这是已经是一个窗口了,根据条件做一些事情 // ... 可以计算窗口最大值等 // 最后不要忘记把 right -k 位置元素从窗口里面移除 } } return res;§1.1基础
二、不定长滑动窗口
不定长滑动窗口主要分为三类:求最长子数组,求最短子数组,求子数组个数。
注:滑动窗口相当于在维护一个队列。右指针的移动可以视作入队,左指针的移动可以视作出队。
不定长滑动窗口
§2.1 越短越合法/求最长/最大
当窗口不满足条件的时候,移动左指针
class Solution {public: int lengthOfLongestSubstring(string s) { if(s.size() == 0) return 0; unordered_set<char> lookup; int maxStr = 0; int left = 0; for(int i = 0; i < s.size(); i++){ while (lookup.find(s[i]) != lookup.end()){ lookup.erase(s[left]); left ++; } maxStr = max(maxStr,i-left+1); lookup.insert(s[i]); } return maxStr;
}};§2.1.1 基础
- 3. 无重复字符的最长子串 - 力扣(LeetCode)
- 3090. 每个字符最多出现两次的最长子字符串 - 力扣(LeetCode)
- 1493. 删掉一个元素以后全为 1 的最长子数组 - 力扣(LeetCode)
- 3634. 使数组平衡的最少移除数目 - 力扣(LeetCode)
- 1208. 尽可能使字符串相等 - 力扣(LeetCode)
- 904. 水果成篮 - 力扣(LeetCode)
- 1695. 删除子数组的最大得分 - 力扣(LeetCode)
- 2958. 最多 K 个重复元素的最长子数组 - 力扣(LeetCode)
- 2024. 考试的最大困扰度 - 力扣(LeetCode)
- 1004. 最大连续1的个数 III - 力扣(LeetCode)
§2.2 越长越合法/求最短/最小
class Solution {public: int minSubArrayLen(int target, vector<int>& nums) { int ans = INT_MAX; int sum = 0; int left = 0; for(int i = 0; i < nums.size(); i++){ sum += nums[i]; while(sum >= target){ sum -= nums[left]; ans = min(ans, i - left + 1); left++; } } return ans == INT_MAX ? 0 : ans; }};§2.3 求子数组个数
§2.3.1 越短越合法
一般要写 ans += right - left + 1。
内层循环结束后,
[left,right]这个子数组是满足题目要求的。由于子数组越短,越能满足题目要求,所以除了[left,right],还有[left+1,right],[left+2,right],…,[right,right]都是满足要求的。也就是说,当右端点固定在 right 时,左端点在 left,left+1,left+2,…,right 的所有子数组都是满足要求的,这一共有 right−left+1 个。
class Solution {public: int numSubarrayProductLessThanK(vector<int>& nums, int k) { if(k <= 1){ return 0; } int cnt = 1; int left = 0; int ans = 0; for(int i = 0; i < nums.size(); i++){ cnt *= nums[i]; while(cnt >= k){ cnt /= nums[left]; left++; } ans += i - left + 1; } return ans; }};§2.3.2 越长越合法
一般要写 ans += left。
内层循环结束后,[left,right] 这个子数组是不满足题目要求的,但在退出循环之前的最后一轮循环,[left−1,right] 是满足题目要求的。由于子数组越长,越能满足题目要求,所以除了 [left−1,right],还有 [left−2,right],[left−3,right],…,[0,right] 都是满足要求的。也就是说,当右端点固定在 right 时,左端点在 0,1,2,…,left−1 的所有子数组都是满足要求的,这一共有 left 个。
我们关注的是 left−1 的合法性,而不是 left。
class Solution {public: int numberOfSubstrings(string s) { int cnt[3]{}; int left = 0; int ans = 0; for(int i = 0; i < s.size(); i++){ cnt[s[i] - 'a']++; while(cnt[0] && cnt[1] && cnt[2]){ cnt[s[left] - 'a']--; left++; } ans += left; } return ans; }};§2.3.3 恰好型滑动窗口
例如,要计算有多少个元素和恰好等于 k 的子数组,可以把问题变成:
- 计算有多少个元素和 ≥ k 的子数组。
- 计算有多少个元素和 > k,也就是 ≥ k + 1 的子数组。
答案就是元素和 ≥ k 的子数组个数,减去元素和 ≥ k + 1 的子数组个数。这里把 > 转换成 ≥,从而可以把滑窗逻辑封装成一个函数 solve,然后用 solve(k) - solve(k + 1) 计算,无需编写两份滑窗代码。
总结:「恰好」可以拆分成两个「至少」,也就是两个「越长越合法」的滑窗问题。
注: 也可以把问题变成 ≤ k 减去 ≤ k - 1,即两个「至多」。可根据题目选择合适的变形方式。
注: 也可以把两个滑动窗口合并起来,维护同一个右端点 right 和两个左端点 left_1 和 left_2,我把这种写法叫做三指针滑动窗口。
class Solution {public: int numSubarraysWithSum(vector<int>& nums, int goal) { int ans = solve(nums, goal) - solve(nums, goal - 1); return ans; } int solve(vector<int>& nums, int k){ // <=k的子数组个数 if(k < 0) return 0; int left = 0; int ans = 0; int cnt = 0; for(int i = 0; i < nums.size(); i++){ cnt += nums[i]; while(cnt > k){ cnt -= nums[left]; left++; } ans += i - left + 1; } return ans; }};