滑动窗口就像是一个爬虫,在一个序列中进行遍历,头部扩容进入窗口,在尾部舍弃,在此刻进行条件判断。
写法一(from labuladong 的算法小抄):
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移(增大)窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 左移(缩小)窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
// 或者
int slidingWindow(string s) {
unordered_map<char, int> window;
int left = 0, right = 0;
int res = 0; // 记录结果
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移(增大)窗口
right++;
// 进行窗口内数据的一系列更新
...
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 左移(缩小)窗口
left++;
// 进行窗口内数据的一系列更新
...
}
// 在这里更新答案
// 例如 res = max(res, right - left);
}
// return res;
}
注意,该模板为左闭右开区间
[left, right)
,因此在遍历的时候,窗口只到达right - 1
的范围,窗口大小为right - left
。
写法二(from 负雪明烛):
int slidingWindow(vector<int>& nums) {
int left = 0, right = 0;
int sums = 0; // 用于统计 子数组/子区间 是否有效,根据题目可能会改成求和/计数
int res = 0; // 保存最大的满足题目要求的 子数组/子串 长度
while (right < nums.size()) {
sums += nums[right]; # 增加当前右边指针的数字/字符的求和/计数
while (区间[left, right]不符合题意) {
// 此时需要一直移动左指针,直至找到一个符合题意的区间
sums -= nums[left]; // 移动左指针前需要从counter中减少left位置字符的求和/计数
left++; // 真正的移动左指针,注意不能跟上面一行代码写反
}
// 到 while 结束时,我们找到了一个符合题意要求的 子数组/子串
res = max(res, right - left + 1); // 需要更新结果
right++; // 移动右指针,去探索新的区间
}
return res;
}
1004. 最大连续1的个数 III
题目描述
题解
题目可转化为:在最多变换 k
次的条件下,找出最长连续子序列。
左闭右开区间写法:
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int left = 0, right = 0, res = 0, zeros = 0;
while (right < nums.size()) {
right++;
if (nums[right - 1] == 0) {
zeros++;
}
while (zeros > k) {
if (nums[left++] == 0) {
zeros--;
}
}
res = max(res, right - left);
}
return res;
}
};
闭区间写法:
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int left = 0, right = 0, res = 0, zeros = 0;
for (right = 0; right < nums.size(); right++) {
if (nums[right] == 0) zeros++;
while (zeros > k) {
if (nums[left++] == 0) zeros--;
}
res = max(res, right - left + 1);
}
return res;
}
};
2024. 考试的最大困扰度
题目描述
题解
这道题和 1004. 最大连续1的个数 III 的思路一模一样,都可以转化为 “在给定条件下,求最长连续子序列”,该题增加的内容无非就是考虑两种情况:
- 将 ‘T’ 变成 ‘F’
- 将 ‘F’ 变成 ‘T’
然后综合选取二者中的较大值。
class Solution {
public:
int maxConsecutive(string answerKey, int k, char ch) {
int left = 0, right = 0, res = 0, flags = 0;
for (right = 0; right < answerKey.length(); right++) {
if (answerKey[right] == ch) flags++;
while (flags > k) {
if (answerKey[left++] == ch) {
flags--;
}
}
res = max(res, right - left + 1);
}
return res;
}
int maxConsecutiveAnswers(string answerKey, int k) {
return max(maxConsecutive(answerKey, k, 'T'), maxConsecutive(answerKey, k, 'F'));
}
};
904. 水果成篮
题目描述
题解
该题看着题目很长,理解下来转换后,就变成 “只有两种数字的最长连续子序列”。
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int left = 0, right = 0, res = 0, kinds = 0;
unordered_map<int, int> map;
while (right < fruits.size()) {
int c = fruits[right++];
if (map[c] == 0) {
++kinds;
}
map[c]++;
// 当窗口内数字种类大于二,考虑缩小窗口
while (kinds > 2 && left < right) {
int d = fruits[left++];
if (map[d] != 0) {
map[d]--;
if (map[d] == 0) {
--kinds;
}
}
}
res = max(res, right - left);
}
return res;
}
};
1695. 删除子数组的最大得分
题目描述
题解
这道题与 904. 水果成篮 可以说是一模一样
问题转化为“求不含重复元素的连续子数组最大和”,思路还是利用滑动窗口记录数组区间,当遇到重复元素就缩小窗口,直到窗口内不含有重复元素。(元素个数的记录可利用Map)
class Solution {
public:
int maximumUniqueSubarray(vector<int>& nums) {
int left = 0, right = 0, res = 0, sum = 0;
unordered_map<int, int> map;
while (right < nums.size()) {
int c = nums[right++];
sum += c;
map[c]++;
while (left < right && map[c] > 1) {
int d = nums[left++];
map[d]--;
sum -= d;
}
res = max(res, sum);
}
return res;
}
};
713. 乘积小于 K 的子数组
题目描述
题解
这道题出现了 “小于k” 和 “连续子数组”,立马想到滑动窗口
class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
if(k == 0 || k == 1) return 0;
int left = 0, right = 0, ans = 0, prod = 1;
while (right < nums.size()) {
prod *= nums[right];
while (left <= right && prod >= k) {
prod /= nums[left++];
}
// ans加上从[left, right]区间中的不重复连续子数组个数
/**
举个例子 [...5,6,3,4,8,...] 假设r遍历到了数值8的位置, l经过摘除后移到了数值5处,
此时所有的组合情况是:
[56348]
[6348]
[348]
[48]
[8]
即5种
*/
ans += right - left + 1;
right++;
}
return ans;
}
};
Note
滑动窗口是动态规划的一种具体用法,当题目有具体限制要求的时候,比如求和为k的最大子数组的长度,滑动窗口不失为一种可考虑的方法。
滑动窗口不能使用的情况:
- 没有更多的限制条件能使得窗口收缩
- 某些求 最大值 而不是求 最长 连续子数组的情况(如 53. 最大子数组和),不能把求最大值转化为求最长
此时需要考虑使用DP之外或者DP类的其他方法(如前缀和)