滑动窗口题目指引01
强调:基于左程云老师的滑动窗口和单调队列上课程,思路可能有些有点不同。只是做题思路记录,详解见左程云老师课程
最小覆盖子串
思路
把“包含t中所有字符”转化为“负债模型”
t中的字符是所欠的欠债,窗口右扩时还债,左缩时可能重新欠债
当debt==0时,说明当前窗口覆盖完整(还清总债)
解题过程
滑动窗口(预判式收缩)
复杂度
- 时间复杂度: $O(|s|+|t|) $
- 空间复杂度: \(O( \Sigma )\)
代码:
class Solution {
public:
string minWindow(string s, string t) {
int debt=0; // 总债务(欠t多少字符,含重复)
int slen=s.size(),tlen=t.size();
int cnt[300]={0};
int anslen=INT_MAX,start=-1; // 最小窗口长度 答案子串起始位置
string subs; // 答案子串
for(int i=0;i<tlen;++i){
debt++; // 每欠一个字符 总债务debt加1
cnt[t[i]]--; // cnt[c] 欠了目标串t几个c
}
for(int l=0,r=0;r<slen;++r){
cnt[s[r]]++;
if(cnt[s[r]]<=0) debt--; // 若s[r]时欠债字符,则还上了一笔账
if(debt==0){ // debt==0 : [l,r]已覆盖t中所有字符
while(l<=r&&cnt[s[l]]-1>=0){
// 预判式收缩 : 尝试将l右移,却又保证移完之后合法
// cnt[s[l]]-1>=0 : 踢除s[l],仍不欠债
cnt[s[l]]--;
l++;
}
// [l,r] : 以r结尾的最短覆盖子串
if(r-l+1<anslen){
anslen=r-l+1;
start=l;
}
}
}
if(start==-1) subs=""; // 边界 : 未找到合法子串,返回空串
else subs=s.substr(start,anslen);
return subs;
}
};
替换子串得到平衡字符串
思路
核心转换:
用子串s'替换s的某一段字符后,s'外的字符数量不受影响
若子串s'外Q已经超出了 n/4,则s'必须把超过的那部分Q也包括进去
子串外每一种字符的数量都<=n/4
因此:求最小窗口,使其包含所有“超载字符”的最小覆盖子串!
为什么包含所有超载字符的最小子串一定能凑出一种平衡的替换方案?
-- 总盈余=总亏损
-- 盈余的字符逐一还给对应亏损的字符即可
解题过程
关键变量:
quarter:n/4,平衡时每种字符的目标数量
debt:窗口内还需要找到的“超载字符”总数
cnt[c]:初始为负时,表示欠了|cnt[c]|个字符
所谓“欠了|cnt[c]|个字符”,意思是c比quarter多占了|cnt[c]|个字符
导致字符串不平衡
复杂度
- 时间复杂度: $ O(n) $
- 空间复杂度: \(O(1)\)
代码:
class Solution {
public:
//统计Q,W,E,R字符个数->最小覆盖子串
int balancedString(string s) {
int len=s.size(),quarter=len/4;
int debt=0,anslen=INT_MAX;
int cnt[300]={0};
string str="QWER";
// cnt[c]的语义:
// 减去s中c的总数
// 加上quartter
// 结果<0说明c字符“超载”了,需要还清多余的|cnt[c]|个字符
for(int i=0;i<len;++i){
cnt[s[i]]--;
}
for(int i=0;i<str.size();++i){
cnt[str[i]]+=quarter;
if(cnt[str[i]]<0) debt+=abs(cnt[str[i]]);
}
// 特判 : 已经平衡,无需替换空子串
// 窗口 : [l,r]至少包含一个元素,所以必须特判
if(debt==0) return 0;
// 滑动窗口: 试探性收缩while(debt==0)
// 右扩: 若cnt[s[r]]<=0,则还了一笔债
// 左缩: while合法继续缩,缩完欠债则debt++
for(int l=0,r=0;r<len;++r){
cnt[s[r]]++;
if(cnt[s[r]]<=0) debt--;
while(l<=r&&debt==0){
// 只要还清了债,就不断尝试收缩左边界,追求最短窗口
// 先取合法答案,再缩,若缩后不合法debt>0,while自然退出
anslen=min(anslen,r-l+1);
cnt[s[l]]--;
// 若踢除的s[l]是“超载字符”,重新欠债
if(cnt[s[l]]<0) debt++;
l++;
}
}
return anslen;
}
};
预判式:
class Solution {
public:
//统计Q,W,E,R字符个数->最小覆盖子串
int balancedString(string s) {
int len=s.size(),quarter=len/4,debt=0,lans=INT_MAX,start=0;
int cnt[300]={0};
string str="QWER";
for(int i=0;i<len;++i){
cnt[s[i]]--;
}
for(int i=0;i<str.size();++i){
cnt[str[i]]+=quarter;
if(cnt[str[i]]<0) debt+=abs(cnt[str[i]]);
}
for(int l=0,r=0;r<len;++r){
cnt[s[r]]++;
if(cnt[s[r]]<=0) debt--;
if(debt==0){
while(l<=r&&cnt[s[l]]-1>=0){
cnt[s[l]]--;
l++;
}
if(r-l+1<lans){
lans=r-l+1;
start=l;
}
}
}
return lans;
}
};
加油站
思路
贪心淘汰:
若从s出发走到k,但是在k处没法继续往下走[抛锚]
(且没有走完一圈),则[s,k]中所有站都不可能作为起点
证明:
[s,...,t,...,k-1,k]既然从s一路支撑到k-1,还能继续走到k,
说明从k-1到k(刚到k,没加油时),攒下的油量>=0
k[抛锚]:cost[k]>gas[k]
s->k:没有算上gas[k]时,剩余油量为m
假设t是答案,推出矛盾:
s->t:积攒油量
直接从t出发:抛弃s->t的余油
s->k:抛锚(m+gas[k]<cost[k])
t->k:可行(m'+gas[k]>=cost[k])
但是m'<m,矛盾!
解题过程
模拟开车过程,不断尝试起点
复杂度
- 时间复杂度: $ O(n) $
- 空间复杂度: \(O(1)\)
代码:
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int sum1=0,sum2=0;
int l=0,r=0,len=gas.size();
int remain=0,cnt=0; // remain 余油量,cnt累计经过的加油站数
int remn=len; // 余下还有几个加油站没被排除 可作为起点
for(int g:gas){sum1+=g;}
for(int c:cost){sum2+=c;}
if(sum1<sum2){return -1;}
while(remn>0){
remain=gas[l]; // 从l出发
cnt=1; // 初始经过1站
while(cnt<len&&remain-cost[r]>=0){ // 还可以继续前进
remain-=cost[r];
cnt++;
r=(r+1)%len; // r++前进
remain+=gas[r];
}
if(cnt==len) return l; // 成功找到答案
else {l=(r+1)%len;r=l;remn-=cnt;} //l直接跳到r+1,排除[l,r]内所有加油站
}
return -1;
}
};
K 个不同整数的子数组
class Solution {
public:
int subarraysWithKDistinct(vector<int>& nums, int k) {
//思路
int l=0,r=0,diff=0,len=0,ans=0;
len=nums.size();
map<int,int> mpk,mpk_1;
vector<int> atMostK(len,0),atMostK_1(len,0);
for(;r<len;++r){
if(mpk[nums[r]]++==0) diff++;
while(l<r&&diff>k){
if(--mpk[nums[l]]==0) diff--;
l++;
}
if(diff<=k){
atMostK[r]=r-l+1;
}
}
l=0,r=0,diff=0;
for(;r<len;++r){
if(mpk_1[nums[r]]++==0) diff++;
while(l<r&&diff>k-1){
if(--mpk_1[nums[l]]==0) diff--;
l++;
}
if(diff<=k-1){
atMostK_1[r]=r-l+1;
}
}
for(int i=0;i<len;++i){
ans+=atMostK[i]-atMostK_1[i];
}
return ans;
}
};
至少有 K 个重复字符的最长子串
class Solution {
public:
int longestSubstring(string s, int k) {
int l=0,r=0,ans=0,len=0,diff=0;
len=s.size();
for(int m=1;m<=26;++m){ // m 子串中的字符种类数
l=0,r=0,diff=0;
int mp[300]={0};
for(;r<len;++r){
if(mp[s[r]]++==0) diff++;
while(l<=r&&diff>m){
if(--mp[s[l]]==0) diff--;
l++;
}
if(diff==m){
bool isValid=true;
for(int i=l;i<=r;++i){
if(mp[s[i]]<k) isValid=false;
}
if(isValid) ans=max(ans,r-l+1);
}
}
}
return ans;
}
};
class Solution {
public:
int longestSubstring(string s, int k) {
int l=0,r=0,ans=0,len=0,diff=0,less=0;
len=s.size();
for(int m=1;m<=26;++m){ // m 子串中的字符种类数
l=0,r=0,diff=0,less=0;
int mp[300]={0};
for(;r<len;++r){
mp[s[r]]++;
if(mp[s[r]]==1){
diff++;
if(mp[s[r]]<k) less++;
}
else{
if(mp[s[r]]==k) less--;
//mp[s[r]]>k less之前就剔除s[r]了
}
while(l<=r&&diff>m){
mp[s[l]]--;
if(mp[s[l]]==0){
diff--;
less--;
}
else{
if(mp[s[l]]==k-1) less++;//只在刚好不够时记录 不重复less++
//else mp[s[l]]>=k less不变
}
l++;
}
if(diff==m){
if(less==0) ans=max(ans,r-l+1);
}
}
}
return ans;
}
};
滑动窗口最大值
代码:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int len=nums.size();
int head=0,tail=-1;
vector<int> q(len,0),res;
for(int r=0;r<len;++r){
while(head<=tail&&nums[q[tail]]<=nums[r]) tail--;
q[++tail]=r;
while(q[head]+k<=r) head++;
if(r>=k-1) res.push_back(nums[q[head]]);
}
return res;
}
};
绝对差不超过限制的最长连续子数组
思路
用两个单调队列维护窗口[l,r]的最大值和最小值
解题过程
h1,t1:q1队列的指针
h2,t2:q2队列的指针
l,r:滑动窗口的指针
求[l,r]窗口长度(l-r+1)的最大值
外层r尽可能扩充,内层l收缩保证y_max-y_min<=limit
用q1,q2维护[l,r]的y_max和y_min
复杂度
- 时间复杂度: $ O(n) $
- 空间复杂度: \(O(n)\)
代码:
class Solution {
public:
int longestSubarray(vector<int>& nums, int limit) {
int l=0,r=0,ans=0,len=0,h1=0,t1=-1,h2=0,t2=-1;
len=nums.size();
vector<int> q1(len,0),q2(len,0);
//[l,r]为窗口
//q1:q1[h1]表示当前窗口最大值,记为y_max
//q2:q2[h2]表示当前窗口的最小值,记为y_min
//如果y_max-y_min<=limit,r可以继续扩展
//否则l++
for(;r<len;++r){
// 入队+维护最大/最小
while(h1<=t1&&nums[q1[t1]]<=nums[r]) t1--;
q1[++t1]=r;
while(h2<=t2&&nums[q2[t2]]>=nums[r]) t2--;
q2[++t2]=r;
// 收缩左边界,l右移到y_max-y_min<=limit合法
while(l<=r&&nums[q1[h1]]-nums[q2[h2]]>limit){
l++;
while(q1[h1]<l) h1++; // 检查窗口head所指元素是否过期
while(q2[h2]<l) h2++; // 清理过期(可能需要连续清理)
}
ans=max(ans,r-l+1); // 合法时:更新答案
}
return ans;
}
};
P2698 Flowerpot S
代码:
#include<bits/stdc++.h>
#define N 100005
using namespace std;
int n,d,q1[N],q2[N],h1,t1,h2,t2,l,r,ans=INT_MAX;
struct node{
int x,y;
}nd[N];
bool cmp(node nd1,node nd2)
{
return nd1.x<nd2.x;
}
int main()
{
cin>>n>>d;
for(int i=0;i<n;++i){
cin>>nd[i].x>>nd[i].y;
}
sort(nd,nd+n,cmp);
for(;r<n;++r){
while(h1<=t1&&nd[q1[t1]].y<=nd[r].y) t1--;
q1[++t1]=r;
while(h2<=t2&&nd[q2[t2]].y>=nd[r].y) t2--;
q2[++t2]=r;
while(l<=r&&nd[q1[h1]].y-nd[q2[h2]].y>=d){
ans=min(ans,nd[r].x-nd[l].x);
l++;
while(q1[h1]<l) h1++;
while(q2[h2]<l) h2++;
}
}
if(ans==INT_MAX) ans=-1;
cout<<ans;
return 0;
}

浙公网安备 33010602011771号