滑动窗口技巧:维持左右边界都不回退的一段范围,解决子数组或字串问题
滑动窗口的关键:找到范围和答案指标之间的单调关系
单调性关系:就是窗口越大,越不容易/容易满足题目的条件,这就是单调性。从而当窗口维持的范围不满足/满足条件时,需要移动窗口的左边界利用单调性来继续探索答案
滑动的过程:滑动窗口可以用变量或结构来维护信息
解题的主要流程:求子数组在每个位置的开头或结尾情况下的答案
例题:
长度最小的子数组 求和最小为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统计数字的频率。
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的字符的个数