cpp
// ============================================================
// 滑动窗口通用解题模板(C++)
// 适用场景:数组/字符串中寻找满足某种条件的子数组/子串
// ============================================================
// ==================== 一、定长滑动窗口模板 ====================
// 窗口大小固定为 k,每次滑动一步,维护窗口内的状态
int fixedSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
int ans = 初始值; // 根据题目要求初始化(最大值问题用INT_MIN,最小值用INT_MAX)
int window_state = 0; // 窗口状态:可以是窗口内元素的和、满足条件的元素个数等
for (int i = 0; i < n; i++) {
// 1. 右边界进入窗口:处理新加入窗口的元素
window_state += nums[i]; // 根据题目修改状态更新逻辑
// 2. 计算左边界下标,判断窗口是否完全形成
int left = i - k + 1; // 窗口左边界下标
if (left < 0) {
continue; // 窗口尚未形成,继续添加元素
}
// 3. 窗口已形成,更新答案
ans = max(ans, window_state); // 根据题目修改答案更新逻辑
// 4. 左边界离开窗口:处理即将移出窗口的元素
int out = nums[left]; // 即将移出窗口的元素
window_state -= out; // 根据题目修改状态回退逻辑
}
return ans;
}
// ==================== 二、不定长滑动窗口模板 ====================
// 窗口大小可变,在不满足条件时通过移动左边界来收缩窗口
int variableSlidingWindow(vector<int>& nums) {
int n = nums.size();
int ans = 初始值; // 根据题目要求初始化
int left = 0; // 窗口左边界
int window_state = 0; // 窗口状态:哈希表统计频次、窗口求和、满足条件的计数等
for (int right = 0; right < n; right++) {
// 1. 右边界进入窗口:处理新加入窗口的元素
window_state += nums[right]; // 根据题目修改状态更新逻辑
// 2. 当窗口不满足题目条件时,移动左边界收缩窗口
// 注意:这里是 while 而非 if,可能需要连续收缩多次
while (窗口不满足条件) {
// 2.1 左边界离开窗口:处理即将移出窗口的元素
window_state -= nums[left]; // 根据题目修改状态回退逻辑
left++; // 收缩左边界
}
// 3. 此时窗口满足条件,更新答案
ans = max(ans, right - left + 1); // 窗口长度为 right - left + 1
}
return ans;
}
// ==================== 使用提示 ====================
// 1. 定长模板:窗口大小 k 固定,重点在于维护入窗和出窗的元素对窗口状态的影响
// 2. 不定长模板:窗口大小动态变化,重点在于 while 收缩条件的判断与左边界移动
// 3. 常用窗口状态工具:
// - 求和/计数:用 int/long long 变量直接累加累减
// - 频次统计:用 unordered_map 或数组 hash[128] 记录字符/元素出现次数
// - 去重判断:通过 map.size() 或 hash 非零个数判断不同元素数量
// 4. 不定长模板也可使用 if 代替 while,当每次最多只需收缩一次时(如"最多包含 k 个不同字符")
// 5. 涉及浮点数判断时,优先转化为乘法避免精度问题(如 sum >= threshold * k)
定长滑动窗口
1456.定长字串中元音的最大数目
1456. 定长子串中元音的最大数目 - 力扣(LeetCode)
本题是滑动窗口的第一题,没接触过算法的可以简单看过题目稍微思考一下就看题解进行学习。
就如同这个算法的名字一样,滑动的窗口,如下图所示。



这还是有点抽象了,那么将滑动窗口想象成你在玩一款建筑的游戏,你准备给一栋大楼安一个窗户。在你调整窗户的位置的时候,你在一个位置能看到一部分的风景,但是当你挪动这个窗户时,原本在窗户内的一部分东西看不见了,同时你也看到了一部分新的东西。
就像上面的图片一样,当我们挪动这个定长的窗口时有一部分的数据被移出去了,同时也有新的数据被添加进来了。
问题是怎么和程序连接起来呢?
在本题中,很明显窗口的大小就是 k ,而满足题意的就是在滑动的过程中窗口中所包含得最大的元音字母数。这其实是有套路的,一开始遍历少部分数据时窗口肯定还没有建立起来,这时需要一边判断当前数据的情况,一边往后继续遍历。
直到窗口建立起来时,由于下一个数据加进来的时候就超出了窗口的大小,所以我们需要将窗口左边的元素给踢出去。在踢出去之前还需要判断左边元素的属性,如果说左边的元素原本是满足题意得,那么一旦将其移除就需要将原本在加入窗口时所增加得答案数减1。
cpp
class Solution {
public:
int maxVowels(string s, int k)
{
int ans = 0,vowel = 0;
for(int i = 0; i < s.size(); i++)
{
//右窗口进
if(s[i] == 'a' || s[i] == 'e' || s[i] == 'i' || s[i] == 'o' || s[i] == 'u')
{
vowel++;
}
//窗口左边界
int left = i - k + 1;
if(left < 0)//判断窗口是否形成
{
continue;
}
ans = max(ans,vowel);//更新答案
char out = s[left];//出窗口的数据
if(out == 'a' || out == 'e' || out == 'i' || out == 'o' || out == 'u')
{
vowel--;
}
}
return ans;
}
};
643.子数组最大平均数 I
643. 子数组最大平均数 I - 力扣(LeetCode)
本题和上一题一样的套路,只不过一开始接触滑窗会不太熟悉,借助这一题来更好的体会滑窗的思想。
本题中需要找的是最大的平均数,最大的平均数的原本的最大和也是最大的,所以我们不必在循环中进行平均值的计算。
滑窗的操作基本可以照抄前面的,写好循环体之后第一行是我们进行对右窗口的操作,上一题是判断加入右窗口的元素是否与条件相等,本题则是将右窗口中的元素给加到累加的变量中。
然后就是计算左窗口的下标和让窗口完全形成,然后更新答案之后再让左边界的元素出窗口。遍历完之后在返回值处进行平均值的计算,记得将返回值得类型强制转换成 double 类型。
cpp
class Solution {
public:
double findMaxAverage(vector<int>& nums, int k)
{
int ans = INT_MIN,sum = 0;//因为本题中含有负数,所以ans 初始化为-∞。
for(int i = 0; i < nums.size(); i++)
{
sum += nums[i];//右窗口的逻辑
int left = i - k + 1;//左窗口的下标
if(left < 0)//未形成窗口
{
continue;
}
ans = max(ans, sum);//更新答案
int out = nums[left];//左窗口的元素出窗口
sum -= out;
}
return (double) ans / k;
}
};
1343.大小为K且平均值大于等于阈值的子数组数目
1343. 大小为 K 且平均值大于等于阈值的子数组数目 - 力扣(LeetCode)
通过上面两题就算再有疑问也是能把结构写个七七八八的,如果你还没有独立的写出这一题,那么看完题解之后建议自己总结一下效果更好。
整体的结构还是写出用于返回的变量后开始思考题目的要求,题目中要求返回长度为 k 且平均值大于等于 threshold 的子数组数目。既然是滑窗就往窗口这个想法去靠,也就是求数组中长度为 k 的子数组的和大于 threshold * k。那么就需要一个变量来存储求和的值,同时也是作为"窗口"。
ps:我认为在滑窗阶段不要将理解的想法和程序中的实际实现有过于的连接,因为滑窗实际上不是我们真的有这么一个窗口,滑窗反而是一个比较抽象出来的东西,是广义的。这么说可能还是太过抽象了,就比如这几道题,实际的窗口只是一个承载了一些性质的东西,整体的作用是小于一个真正的窗口的。既只有一个真正的数据窗口的部分功能,比如求和,统计某个数的性质或者个数。
回到题目的实际中,滑窗的结构其实很明显,就是右边进->更新答案->左边出。右边进就是进入窗口,一个数据是有很多的性质的,所以我们需要根据题目来抽象出某一个为了完成题目的性质。在本题中我们需要抽象的就是数的 "值" 这一性质,因为落实到实际我们也是先求出 k 个数的和再除 k 得到平均值的。
然后依然是固定结构判断窗口是否形成,如果形成的话就往下继续。再往下就是判断并更新答案,这里值得注意的是前面两题找的是最大值,而本题返回的是符合要求的子数组的个数。所以判断条件也就不一样,结合上面优化的判断条件改成 threshold * k。
最后依然是左边出窗口的固定结构。
cpp
class Solution {
public:
int numOfSubarrays(vector<int>& arr, int k, int threshold)
{
int ans = 0, sum = 0;
for(int i = 0; i < arr.size(); i++)
{
sum += arr[i];
int left = i - k + 1;
if(left < 0)
continue;
if(sum >= threshold * k)
ans++;
//由于平均值不一定都是整数,但是涉及到浮点数的操作又稍微有点麻烦,所以可以换成乘法的
//条件判断
int out = arr[left];
sum -= out;
}
return ans;
}
};
2090.半径为 k 的子数组平均值
2090. 半径为 k 的子数组平均值 - 力扣(LeetCode)
本题对于刚开始学算法的新手来说难度可能稍微大了点,虽然整体还是滑窗那一套但是有了很多坑或者说可以优化的地方。所以本题如果做不出来或者没办法AC也不要灰心。
首先在创建数组的地方,题目中说如果在下标 i 前或后不足 k 个元素,那么半径为 k 的子数组平均值是 -1。你可能会想我都判断一下然后再把 -1 放到创建好的数组中,但是没那个必要,因为如果 k 的值特别大那么在返回的数组中的首位就一定会有一部分是 -1。同时如果 k 特别小,那么无论当前数组中是什么数都会被计算过后的正确答案给更新。所以既然范围小无所谓,范围大一定有一部分数是 -1 ,那么不妨在初始化的时候就让数组中都是 -1 而不是默认初始化为 0。
另外本题的一个坑就是给出的 k 是半径,而且题目中的 i 是中心,所以窗口的长度不是 k 而是 2 * k + 1。接下来的具体细节看代码部分。
cpp
class Solution {
public:
vector<int> getAverages(vector<int>& nums, int k)
{
int n = nums.size(), len = 2 * k + 1;//len代表的是窗口的长度
vector<int> ans(n,-1);
long long sum = 0;
//根据题目给的条件,极端的情况 sum 将会存储10^10大小的数字,但是整形的数字最大只能存储
//10^9所以需要改变sum的数据类型
for(int right = 0;right < n; right++)
{
//这里不用 i 是为了防止和题目中的 i 混淆
//同时回想我们之前的滑窗的题目,我们的 i 充当的是不就是窗口右边界的角色吗?
sum += nums[right];
int mid = right - k; //需要更新数据的位置就是中心的位置
int left = mid - k; //左边界,为了下面的出窗口做准备
if(right < 2 * k) //核心还是判断窗口有没有形成
continue;
ans[mid] = sum / len; //求和除的是窗口的长度而不是本题中的 k
int out = nums[left];
sum -= out;
}
return ans;
}
};
总的来说,本题是让你从只会无脑套模板变成思考转化为你自身的东西的一道好题。同时也带了初始化数组时的一种新思路,这种思路在后面的算法题中也有体现。
2379.得到 K 个黑块的最少涂色次数
2379. 得到 K 个黑块的最少涂色次数 - 力扣(LeetCode)
本题的某个地方的考虑和题单中第二题的考虑有点像,根本原因都是由于题目条件导致的更新答案时所收到的部分影响,所以在初始化返回值的时候既需要多考虑一下。
题目中的一次操作将一个白色块改成黑色块其实不用过多的考虑,因为在靠近滑窗的思路的过程中,实际就是统计在窗口内的白色块的数量。所以问题就变成熟悉的统计大小为 k 的子数组内白色块最少的情况,这种情况是我们熟悉的,所以依然可以套模板。
本题中设置初始化的时候一般设置为 0 的话,在更新答案的时候就会被我们抽象出来的 change 给影响。所以我们一开始可以将 ans 给设置成理论最大值,这样就不会被 change 给影响了,这种情况类似第二题的初始化情况。
cpp
class Solution {
public:
int minimumRecolors(string blocks, int k)
{
int ans = INT_MAX, change = 0;
for(int i = 0;i < blocks.size(); i++)
{
if(blocks[i] == 'W')
change++;
int left = i - k + 1;
if(left < 0)
continue;
ans = min(ans, change);
char out = blocks[left];
if(out == 'W')
change--;
}
return ans;
}
};
2841.几乎唯一子数组的最大和
2841. 几乎唯一子数组的最大和 - 力扣(LeetCode)
本题让求的是返回nums中长度为 k 的几乎唯一子数组的最大和,先不看几乎唯一的性质那就是返回一个子数组的最大和,那么 "几乎唯一" 就是在子数组的基础上进行一个限制。
那么在解决这道题的时候就可以先把滑动窗口的结构写出来,然后再考虑怎么再实现 "几乎唯一" 。相信经过这么多题目写出滑窗的结构还是很简单的,怎么样才能判断子数组中的不同元素个数呢?
如果我们设置一个空数组,这个空数组对应着一个具体的元素,每当遇到对应的元素就将其大小加1,那么该数组的大小就是不同的元素的数量。也就是键值对,一个值对应着该值出现的次数。那么就可以用 unordered_map 来存储相应的键值对,每次将数据加进 sum 中的时候也将该数据的次数在哈希中加 1,同时那么就可以在窗口形成之后进行通过判断 map 的大小来判断子数组中不同的元素。
最后在元素出窗口的时候不要忘了将哈希中对应的元素的数量给减 1。
cpp
class Solution {
public:
long long maxSum(vector<int>& nums, int m, int k)
{
int n = nums.size();
long long ans = 0, sum = 0;
unordered_map<int,int> count;
for(int i = 0; i < n; i++)
{
sum += nums[i];
count[nums[i]]++;
int left = i - k + 1;
if(left < 0)
continue;
if(count.size() >= m)
ans = max(ans,sum);
int out = nums[left];
sum -= out;
if(--count[out] == 0)
count.erase(out);
}
return ans;
}
};
2461.长度为 K 子数组中的最大和
2461. 长度为 K 子数组中的最大和 - 力扣(LeetCode)
本题与上一题比只改了一个条件判断,所以做过上一题的话本题尽量还是自己动手写一遍。
cpp
class Solution {
public:
long long maximumSubarraySum(vector<int>& nums, int k)
{
long long ans = 0, sum = 0;
unordered_map<int,int> count;
for(int i = 0; i < nums.size(); i++)
{
sum += nums[i];
count[nums[i]]++;
int left = i - k + 1;
if(left < 0)
continue;
if(count.size() == k)
ans = max(ans,sum);
int out = nums[left];
sum -= out;
if(--count[out] == 0)
count.erase(out);
}
return ans;
}
};
本题只需要在更新答案的时候将 >= m 改成 ==k 即可。
1423.可获得的最大点数
本题的主要难点在于如何将问题转换成我们熟悉的方式,第一眼看到这题可能不会觉得要用滑窗。什么叫每次从开头或者末尾拿一张卡牌,这感觉也太有随机性了。所谓 "正难则反" ,换个角度看问题就会变得很简单,不去看拿的卡牌而是看那拿完之后剩下的。
比如说我们每次只拿开头的一张,那么剩下的就可以看作一个窗口滑动到最后的情况。如果每次只拿末尾的一张那不就是窗口刚形成的情况吗?必须要注意的是,我们拿的卡牌数是 k 所以在长度为 n 数组 cardPoints 中的窗口的长度就是 n - k 。如果拿的卡牌是最大的点数,那么在窗口中的就应该是最小的,于是我们的问题就转换成了求长度为 n - k 的子数组的最小值,最后通过数组的综合减去最小值就得到了我们需要的最大值。
接下来的细节看代码部分。
cpp
class Solution {
public:
int maxScore(vector<int>& cardPoints, int k)
{
int minscore = INT_MAX, n = cardPoints.size();//因为滑窗算的是最小值,所以初始化成INT_MAX
int num = std::accumulate(cardPoints.begin(), cardPoints.end(), 0);//计算数组全部的元素大小
int len = n - k;//计算窗口的大小
if(len == 0)//根据题目拿走的卡牌数可能是与数组大小一样大,这时就返回总和即可
return num;
int sum = 0;
for(int i = 0; i < n; i++)
{
sum += cardPoints[i];
int left = i - len + 1;
if(left < 0)
continue;
minscore = min(minscore,sum);
int out = cardPoints[left];
sum -= out;
}
return num - minscore;
}
};
1052.爱生气的书店老板
本题还是有点难度的,因为之前的滑窗中的元素都是一直加到一起的,同时也需要将问题给转换成我们熟悉的情况。
仔细阅读题目后可以得出,老板通过技巧使自己来连续 mintues 不生气即为窗口,循环找从生气变为不生气时所增加的顾客的最大的数量。但是有一个问题是我们单循环如果不细分生气和不生气的情况,那么不生气时的顾客由于技巧的连续性一定会包含在统计的生气的时候,而我们修改只修改并统计生气的情况。所以不生气的顾客是固定的,循环遍历的时候对于需要修改的我们单独用一个变量统计,不生气时的顾客用另一个变量统计,判断就用数组 grumpy ,那么怎么解决修改生气时包含不生气的顾客的问题呢?
当然也是分情况修改,滑窗虽然是连续的,但是我们将不生气的顾客看成0,这也是我们分类统计自动就做好的事。什么意思呢?简单的说就是形式上我们形成了窗口,但是我们的窗口中只有(只处理)生气时的数据,这个地方可以仔细想一想。举个例子,拿示例一来说,窗口形成后按以前的思路窗口中的数据就是 1,0,1 ,但是我们根据 grumpy 中的情况来说就是不生气的顾客数 1,1 和生气的顾客数 0。
接下来的细节看代码部分。
cpp
class Solution {
public:
int maxSatisfied(vector<int>& customers, vector<int>& grumpy, int minutes)
{
int ans = 0, n = customers.size();
int mad_sum = 0, satisfied_sum = 0;
for(int i = 0; i < n; i++)
{
if(grumpy[i] == 1)
mad_sum += customers[i];
else
satisfied_sum += customers[i];
int left = i - minutes + 1;
if(left < 0)
continue;
ans = max(ans,mad_sum);
int out = customers[left];
if(grumpy[left] == 1)
mad_sum -= out;
}
return ans + satisfied_sum;
}
};
3679.使库存平衡的最少丢弃次数
3679. 使库存平衡的最少丢弃次数 - 力扣(LeetCode)
本题的难度已经接近1700,开始不局限于单一的算法思想了,本题另外的一种算法思想就是贪心思想。仔细阅读题目后可以发现窗口大小即为 w ,那么如何在窗口滑动的过程中正确的保留和丢弃物品呢?
当我们遇到某一物品的个数大于 m 次时我们会有两种选择,第一就是现在就丢弃,第二就是回溯到前面的物品并丢弃。在窗口滑动的过程中哪一种更好呢?当然是现在丢弃更好,因为题目中让求的时最少需要丢弃的数量,那么在窗口滑动的过程中靠前的物品会随着窗口的滑动而自动丢弃,同时,现在丢弃也保证了后面如果还有同类型的物品的话能够存储更多的物品。
因为不同种物品之间要分别计数,那么就用哈希统计每个元素出现的次数。
cpp
class Solution {
public:
int minArrivalsToDiscard(vector<int>& arrivals, int w, int m)
{
int ans = 0;
unordered_map<int,int> count;
for(int i = 0; i < arrivals.size(); i++)
{
int& x = arrivals[i];//作用是修改 arrivals 数组,真丢弃物品
if(count[x] == m)//在窗口未形成时也有可能有物品需要丢弃
{
x = 0; //虽然现在已经丢弃了,但是依然在窗口中,只不过不计入而已,同时不
//能在正常的滑窗中通过左窗口丢弃而修改 count ,所以直接置为0。
//这时离开窗口就是 count[0]--,不影响结果。
ans++;
}
else
count[x]++;
int left = i - w + 1;
int out = arrivals[left];
if(left >= 0)
count[out]--;//更新答案操作由于是丢弃后立马进行的,所以下面就没有了。
//同时窗口未形成时不影响答案的更新,所以判断条件可以改为窗口超出了
//规定大小,同时也将出窗口的操作也一起放在该分支下
}
return ans;
}
};
不定长滑动窗口
经过定长滑动窗口的练习,现在看到不定长滑动窗口的分类大致能明白,窗口的大小不固定了。但是本质上来说他们还是一类问题,接下来看具体的题目进行分析。
3.无重复字符的最长字串
本题第一眼可能觉得跟滑窗没什么关系,但是我们仔细想想滑窗的本质可以得出,定长滑窗就是在窗口形成之后,不断的让元素出窗口再进窗口来维护一个窗口的大小。
那么不定长的滑窗没有维护窗口大小这一说法了,你可能会问,这样的话窗口大小岂不是可以随着元素的个数而变大吗?这当然是可以的,当然仅限于窗口中的元素都和题目中的条件不冲突,那么让窗口滑动的条件就是当右边的元素加进窗口的时候,窗口中元素出现了不符合题意得情况就需要移动。直到将不符合的情况给消除掉之后才能继续的向窗口中添加元素。
本题中存在不同种类的字符并且还对应着数量上的关系,所以用哈希来统计字符的数量,同时也是抽象出来的窗口。
循环中的写法大体上是和定长滑动窗口是类似的,但是有细微的区别,就像上面所分析的一样,在出窗口的环节不是单次的出窗口。同时由于是在不满足是才缩小窗口,所以 left 的更新就写在了出窗口的时候。由于出完窗口新元素还未加进窗口中,所以在出窗口之后再更细答案,如果在出窗口之前更新答案会把不满足条件的答案也计入。
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s)
{
int ans = 0, left = 0;
unordered_map<char,int> count;
for(int i = 0; i < s.size(); i++)
{
count[s[i]]++;
while(count[s[i]] > 1)
{
count[s[left]]--;
left++;
}
ans = max(ans,i - left + 1);
}
return ans;
}
};
3090.每个字符最多出现两次的最长子字符串
3090. 每个字符最多出现两次的最长子字符串 - 力扣(LeetCode)
本题和上一题是非常相似的,建议先自己做一遍之后再看题解,如果有不明白的地方可以借鉴上一题的答案。
本题和上一题的区别就是上一题让求不重复的字母的字串,而本题是求每个字母不超过 2 个的字串。所以代码部分被没有太大的改动,仅仅将判断条件的 1 改成 2 即可。
cpp
class Solution {
public:
int maximumLengthSubstring(string s)
{
int ans = 0, left = 0;
unordered_map<char,int> count;
for(int i = 0; i < s.size(); i++)
{
count[s[i]]++;
while(count[s[i]] > 2)
{
count[s[left]]--;
left++;
}
ans = max(ans,i - left + 1);
}
return ans;
}
};
1493.删掉一个元素以后全为 1 的最长子数组
1493. 删掉一个元素以后全为 1 的最长子数组 - 力扣(LeetCode)
本题难度相对于上一题来说难度只是稍微提升了,一部分知识和定长滑动窗口反而是有相关联的,但是总归还是一些比较常用的方法。
相比于之前的不定长本题其实就是将字母换成了数字,并且要求窗口 0 的个数不能大于 1。因为要求是删除一个元素得到最长的全为 1 的字串,所以我们还像上面一样检测 0 的个数就可以了。
遍历的过程中直接将数据加进求和变量中,因为数据只有 0 和 1 ,但是不能因为数据大小也对应着答案所需要的长度就直接用 ans 当作求和的变量。因为这是我们抽象出来的窗口,随着窗口的滑动答案会一直改变,最后返回的是滑动到数组末尾的答案,不是真正的最大值。
然后我们单独设置一个变量来统计 0 的个数,然后根据这个变量的大小进行滑动的操作。最后在返回答案的时候,由于数组中可能一开始就不存在 0 ,而又必须要删除一个元素,所以在返回答案的时候再进行一次判断,具体内容看下面代码部分。
cpp
class Solution {
public:
int longestSubarray(vector<int>& nums)
{
int ans = 0, left = 0, zero = 0, sum = 0;
for(int i = 0; i < nums.size(); i++)
{
sum += nums[i];
if(nums[i] == 0)
zero++;
while(zero > 1)
{
if(nums[left] != 0)
sum -= nums[left];
else
zero--;
left++;
}
ans = max(ans, sum);
}
return ans == nums.size() ? ans - 1 : ans;
}
};
3634.使数组平衡的最少移除数目
3634. 使数组平衡的最少移除数目 - 力扣(LeetCode)
本题的简便写法可能是比较难想的,因为这是第一次接触排序处理数组,同时也有之前通过的几种思想。
仔细观察题目发现,如果要求移除的元素的最小数量,那么移除之后剩下的就是符合条件的最大的滑动窗口。所以只需要维护一个窗口,使其最大之后再减去窗口的大小就剩下了需要移除的元素。
由于滑动窗口在出元素时是固定从左边出的,而数组中的元素大小并不确定,同时也不好进行条件判断,所以先将数组进行排序再进行滑窗。
由于题目给的数据范围极端情况下超过了 int 所能承载最大值,所以在条件判断的时候需要 * 1LL 转换成 long long 类型的数据。
cpp
class Solution {
public:
int minRemoval(vector<int>& nums, int k)
{
int max_save = 0, left = 0;
ranges::sort(nums);
for(int i = 0; i < nums.size(); i++)
{
while(nums[i] > 1LL * nums[left] * k)
left++;
max_save = max(max_save,i - left + 1);
}
return nums.size() - max_save;
}
};
1208.尽可能使字符串相等
1208. 尽可能使字符串相等 - 力扣(LeetCode)
本题就更像之前的滑窗了,尽量自己写,唯一的难点就是可能不知道绝对值怎么表示。
cpp
class Solution {
public:
int equalSubstring(string s, string t, int maxCost)
{
int ans = 0, cost = 0, n = s.size(), left = 0;
for(int i = 0; i < n; i++)
{
cost += abs(s[i] - t[i]);
while(cost > maxCost)
{
cost -= abs(s[left] - t[left]);
left++;
}
ans = max(ans,i - left + 1);
}
return ans;
}
};
904.水果成篮
本题仔细观察就会发现问题其实是维护一个窗口,窗口中的元素种类为两种,然后求这个窗口的最大值。
问题中的元素和其数量还有对应的关系,所以用哈希来存储元素,同时还可以根据哈希表的大小来判断当前存储的元素类型数。
剩下的就基本和之前的滑窗是一样的过程了,同时要记得在使元素出窗口的时候,当左边的元素对应的数量减为 0 时需要通过 erase 将其从哈希表中完全删除,不然会干扰下一次的判断。
cpp
class Solution {
public:
int totalFruit(vector<int>& fruits)
{
int ans = 0, left = 0;
unordered_map<int,int> count;
for(int i = 0; i < fruits.size(); i++)
{
count[fruits[i]]++;
while(count.size() > 2)
{
count[fruits[left]]--;
if(count[fruits[left]] == 0)
count.erase(fruits[left]);
left++;
}
ans = max(ans,i - left + 1);
}
return ans;
}
};
1695.删除子数组的最大得分
1695. 删除子数组的最大得分 - 力扣(LeetCode)
本题的元素和数量之间仍然有对应关系,所以仍然用哈希表统计。因为启用出窗口的条件是维持窗口中的元素互不相同,既元素的数量要维持在 1 ,所以用哈希表中对应的键值来判断。同时最后的返回值是将窗口中的元素进行求和,所以多加一个变量进行滑窗,最后更新答案即可。
cpp
class Solution {
public:
int maximumUniqueSubarray(vector<int>& nums)
{
int ans = 0, left = 0, sum = 0;
unordered_map<int,int> count;
for(int i = 0; i < nums.size(); i++)
{
sum += nums[i];
count[nums[i]]++;
while(count[nums[i]] > 1)
{
count[nums[left]]--;
sum -= nums[left];
left++;
}
ans = max(ans,sum);
}
return ans;
}
};
2958.最多 K 个重复元素的最长子数组
2958. 最多 K 个重复元素的最长子数组 - 力扣(LeetCode)
如果你做到这一题是认真思考并进行总结,你就会发现本题其实和前面两题没什么太大的区别,很多题目条件都是换汤不换药。如果你不能独立的做出来这一道题的话,建议先去进行总结,只有多总结你才能对算法有一个更深刻的理解。
cpp
class Solution {
public:
int maxSubarrayLength(vector<int>& nums, int k)
{
int ans = 0, left = 0;
unordered_map<int,int> count;
for(int i = 0; i < nums.size(); i++)
{
count[nums[i]]++;
while(count[nums[i]] > k)
{
count[nums[left]]--;
left++;
}
ans = max(ans,i - left + 1);
}
return ans;
}
};
2024.考试的最大困扰度
本题的根本思路还是维护一个窗口然后使得窗口中的 T 或者 F 的数量不超过 k 时的窗口的最大长度,但是难点在于如何处理 T 和 F 。仔细元素分析性质可以知道,本题中的数据是有二元性的。什么意思呢?就是如果本题中的 T 和 F 分别对应于 0 和 1 的话就会方便很多。
因为元素只有两种类型,但是在本题中元素和其数量也有对应关系,你可能想着用哈希来存。这是可以的,但是开销太大了,有没有别的方法呢?既然元素只有两种类型,可以设置一个大小为 2 的数组那么下标 0 和 1 对应的就是相应的元素,如果 T 和 F 是 0 和 1 的话。
那么有没有办法让其变成这样呢?想想 T 和 F 自身有没有什么性质?可以发现其 Ascll 值一个是奇数,一个是偶数。将其除 2 之后就无法再同时进行化简,所以此时两者在二进制的低位就不同了,可以进行位运算来统计。位运算还有个好处就是计算比较快,如果用逻辑判断之类的就不如位运算,但是逻辑判断的可读性是要比位运算高的。
接下来的详细步骤看代码部分。
cpp
class Solution {
public:
int maxConsecutiveAnswers(string answerKey, int k)
{
int ans = 0, left = 0, count[2]{};
for(int i = 0; i < answerKey.size(); i++)
{
count[answerKey[i] >> 1 & 1]++;// ">>" 是右移操作效果就等于除 2 "& 1" 效果是将低位为 1 的数结果仍置为 1
while(count[0] > k && count[1] > k)
{
count[answerKey[left] >> 1 & 1]--;
left++;
}
ans = max(ans, i - left + 1);
}
return ans;
}
};
1004.最大连续1的个数
1004. 最大连续1的个数 III - 力扣(LeetCode)
本题就可以用到上一题的思路,不过由于数据本身就是 0 和 1 所以就不再需要进行位运算的操作了。
所求的是数组中连续 1 的最大个数,也就是求窗口的大小,并且窗口中的 0 的个数不能超过 k 。像上一题一样只创建大小为 2 的数组就够用了,同时数据自身也对应着其将要放入数组中的下标。
由于需要满足的条件是 0 的个数不能超过 k 个所以只需要判断数组中第一个位置的大小(既 0 的个数)即可。
剩下的步骤和之前的一样。
cpp
class Solution {
public:
int longestOnes(vector<int>& nums, int k)
{
int ans = 0, left = 0, count[2]{};
for(int i = 0; i < nums.size(); i++)
{
count[nums[i]]++;
while(count[0] > k)
{
count[nums[left]]--;
left++;
}
ans = max(ans,i - left + 1);
}
return ans;
}
};
2730.找到最长的半重复子字符串
2730. 找到最长的半重复子字符串 - 力扣(LeetCode)
本题仍然是求窗口的大小,但是题目要求比较麻烦,第一眼看到不同的元素还有对应的个数关系,可能会想用哈希表。但是会发现当现在已经存储了超过两个的相同元素时,如果前面也有另外两个相同元素时无法定位到前面的元素,所以就不能用哈希表。
那么换种思路,既然需要判断的是两个相邻的相同的元素的个数,那能不能直接统计呢?当然是能的,可以直接通过数组的下标加减 1 来比较统计。
需要注意的是在循环中的 i 初始化的时候设为 1,因为如果设为 0 的话比较就只能是 i 和 i + 1,而这样会导致 i 在数组末尾时越界访问,所以设为 1 让比较变成 i 和 i - 1 就不会越界访问了。
同时,既然只有相邻的元素相同才进行统计,那么在出窗口的时候也是当相邻的元素相同时才将count 自减。
剩下的更新元素和之前时一样的。
cpp
class Solution {
public:
int longestSemiRepetitiveSubstring(string s)
{
int ans = 1, left = 0, count = 0;
for(int i = 1; i < s.size(); i++)
{
if(s[i] == s[i - 1])
count++;
while(count > 1)
{
if(s[left] == s[left + 1])
count--;
left++;
}
ans = max(ans,i - left + 1);
}
return ans;
}
};
2779.数组的最大美丽值
本题需要返回的最大美丽值其实就是经过一系列操作之后的到的数组中的相同元素的子序列,是子序列不是子串,也就是说中间是可以不连续的。
同时在范围0,nums.length - 1中选择一个此前没有选过的下标其实就是遍历一遍,比较难处理的就是将 nums i 替换为 nums\[ i - k, nums i + k] 内的任意整数。因为看示例可以看出来是要对于每个元素都进行范围中的选数,在程序中这肯定不好实现。
所以就需要换个思路了,之所以能够选出多个元素中相同的数是因为他们的范围都把这个数给包括了。然后发现如果说让最小的数的范围跟最大的数的范围能够相交的话,那么中间一系列的数都能取到同一个数。所以只需要滑窗比较数的范围就可以了,因为答案跟数组中的元素顺序无关,那么处理最大和最小的数就可以先将数组进行排序。
这样就只需要不断的判断每个元素的取值范围是否相交,遇到不相交的就让窗口滑动。这样还引申出一个问题就是怎么判断是否相交,设左边的区间为 x - k,x + k 右边的区间为 y - k,y + k 那么当 x + k >= y - k , 时就满足条件了。进行一下数学计算就可以得到,y - x <= 2k 时满足条件,所以不满足条件的情况就是 y - x > 2k,而这样时方便我们用程序表示的。
接下来的细节看代码部分。
cpp
class Solution {
public:
int maximumBeauty(vector<int>& nums, int k)
{
int ans = 0, left = 0;
ranges::sort(nums);
for(int i = 0; i < nums.size(); i++)
{
while(nums[i] - nums[left] > k * 2)
{
left++;
}
ans = max(ans,i - left + 1);
}
return ans;
}
};
209.长度最小的子数组
本题和之前的有所不同,本题让求长度最小的子数组,判断条件是子数组中的总和大于等于 target 。按照之前的写法的话,求最小值那么初始化 ans 的时候就需要让其尽量大,因为求的是子数组的长度,所以要大于子数组的长度即可。要判断子数组中的总和就需要一个变量每次遍历的时候将一个数据加入其中,唯一的问题就是越长的子数组就越满足条件,在这种情况下怎么能求最小值呢?
每次满足条件时有可能为最中答案,所以出窗口就可以跟更新答案放在一起了。每当满足条件先更新答案,再出窗口判断是否满足条件,如果窗口长度更小那么就再次更新答案,知道找到最小的长度为止。
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int n = nums.size(), ans = n + 1, left = 0, sum = 0;
for(int i = 0; i < n; i++)
{
sum += nums[i];
while(sum >= target)
{
ans = min(ans,i - left + 1);
sum -= nums[left];
left++;
}
}
return ans <= n ? ans : 0;
}
};
3795.不同元素和至少为 K 的最短子数组长度
3795. 不同元素和至少为 K 的最短子数组长度 - 力扣(LeetCode)
本题需要注意的地方是虽然返回的是子数组的最小长度,但是满足的条件并不是子数组中的每个数字必须是不同类型的,也就是说可以有相同的数字但是求和计算上只能是记作一个。
由于本题的数字类型之间有一定的数量关系,所以用哈希存储数字,同时判断条件又有对子数组的求和,所以还需要一个变量来求和。
但是由于只有求和时才将数字看成一个,所以当哈希表中的键值对的数量为 1 时才向 sum 中进行加操作。同时在出窗口中和上题相似值满足就更新 ans ,但是在下面从 sum 中删除数据时就和上面加进数据时是对应的,只有子数组中完全没有该数字时才进行减。
cpp
class Solution {
public:
int minLength(vector<int>& nums, int k)
{
int n = nums.size(), ans = n + 1, left = 0, sum = 0;
unordered_map<int,int> count;
for(int i = 0; i < n; i++)
{
count[nums[i]]++;
if(count[nums[i]] == 1)
sum += nums[i];
while(sum >= k)
{
ans = min(ans,i - left + 1);
count[nums[left]]--;
if(count[nums[left]] == 0)
sum -= nums[left];
left++;
}
}
return ans <= n ? ans : -1;
}
};
713.乘积小于 K 的子数组
713. 乘积小于 K 的子数组 - 力扣(LeetCode)
本题会发现结果越小越满足条件,但是难点在于不知道怎么处理计数较小的子数组,什么意思呢?就是说如果一个较大的子数组满足了条件,那么这个较大的子数组中的所有子数组也都满足条件。如果说我们每次只求满足条件的较大子数组,然后较大的子数组中的所有子数组再加到答案中就满足了题意。这样也符合我们进行滑窗的条件,右窗口一直向右,左窗口判断条件缩小。
这是一个数学问题,如果满足条件的较大的窗口是 left, right 的话那么这个窗口中所有的子窗口的个数就是 right - left + 1 ,可以带入几个简单的数据进去验证一下。
此时这个问题就变得没那么困难了,需要注意的是 sum 不能初始化成 0 了,因为这次是求积所以初始化成 1。剩下的部分就和之前的大部分题目都是一样的,只不过再 ans 的更新处需要注意的是,每次满足条件的都是较大的窗口,所以每次都需要加 right - left + 1。
cpp
class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k)
{
int ans = 0, left = 0, sum = 1;
for(int i = 0; i < nums.size(); i++)
{
sum *= nums[i];
while(sum >= k)
{
sum /= nums[left];
left++;
}
ans += i - left + 1;
}
return ans;
}
};
1358.包含所有三种字符的子字符串数目
1358. 包含所有三种字符的子字符串数目 - 力扣(LeetCode)
本题和上一题是相反的思路,在本题中会发现结果越长越满足条件。和上一题反过来看就是,当我们的滑窗已经满足条件的时候再加入其他的元素还是满足条件的,正常的思路就是在已经成立的情况下去继续加入元素再统计。但是会发现是比较困难的,有没有一中方法可以向上一题中的那样去计数呢?当然是有的,根据正难则反,由于正着在满足条件的情况下计数比较麻烦。所以当我们的窗口刚好满足条件过后进行元素的移除,直到窗口不满足条件的时候,此时会发现从 0,1,2 ...... left - 1都是满足条件的。
所以在这种越长越合法的题目中 ans 更新每次都需要加 left。
cpp
class Solution {
public:
int numberOfSubstrings(string s)
{
int ans = 0, left = 0, count[3]{};
for(int i = 0; i < s.size(); i++)
{
count[s[i] - 'a']++;
while(count[0] && count[1] && count[2])
{
count[s[left] - 'a']--;
left++;
}
ans += left;
}
return ans;
}
};
930.和相同的二元子数组
本题和上两题完全不一样,求的是有多少个元素和恰好等于 k 的子数组。这样的问题可以变成计算有多少个元素和 >= k 的子数组,也就是 >= k + 1 的子数组。
答案就是元素和 >= k 的子数组个数,减去元素和 >= k + 1 的子数组个数。这里把 > 转换成 >=,从而可以把滑窗逻辑封装成一个函数 solve,然后用 solve( k ) − solve( k + 1 ) 计算,无需编写两份滑窗代码。恰好可以拆分成两个至少,也就是两个越长越合法的滑窗问题。也可以把问题变成 <= k 减去 <= k − 1,即两个「至多」。可根据题目选择合适的变形方式。
在本题中判断条件是 sum >= k 所以维护的是窗口内的和 < k ,所以说我们在外面的 slove 就是 k - (k - 1),同时为了防止数据溢出写成了 (k + 1) - k。
cpp
class Solution {
public:
int solve(vector<int>& nums, int k)
{
int ans = 0, left = 0, sum = 0;
for(int i = 0; i < nums.size(); i++)
{
sum += nums[i];
while(sum >= k)
{
sum -= nums[left];
left++;
}
ans += i - left + 1;
}
return ans;
}
int numSubarraysWithSum(vector<int>& nums, int goal)
{
if(goal > 0)
return solve(nums,goal + 1) - solve(nums,goal);
else
return solve(nums,1);
}
};
1248.统计优美子数组
1248. 统计「优美子数组」 - 力扣(LeetCode)
本题的解法和上一题是相同的,只不过本题维护的是窗口内放入奇数的个数是 < k 的,所以说在外面的 slove 就是 k - (k + 1)。
cpp
class Solution {
public:
int solve(vector<int>& nums,int k)
{
int left = 0, count = 0, ans = 0;
for(int i = 0; i < nums.size(); i++)
{
if(nums[i] % 2 != 0)
count++;
while(count >= k)
{
if(nums[left] % 2 != 0)
count--;
left++;
}
ans += left;
}
return ans;
}
int numberOfSubarrays(vector<int>& nums, int k)
{
return solve(nums,k) - solve(nums,k + 1);
}
};