单调栈 & 单调队列
单调栈 & 单调队列
如果一个人比你小,还比你强,那你就永远打不过他了。——单调队列
经过痛苦的折磨,我终于,把单调栈和单调队列学会并理解了,但做的题还比较简单。
单调栈
顾名思义,就是维护一个具有单调性的栈,用于解决求序列中第 \(i\) 个数右边第一个大于它的数(或者其下标),当然还有右边第一个小于它的,左边第一个大于它的,右边第一个小于它的,此类问题。
luogu P5788 【模板】单调栈
求第 \(i\) 个数右边第一个大于它的数(或者下标),就要维护一个从栈顶至栈底 单调递增 的栈(注意,为了方便,一般栈中存的是下标),即栈顶是最小的数,当遍历到 \(i\) 并要加入 \(a_i\) 时,如果单调性将被破坏,就要弹栈,直到栈空或者栈顶元素所对应的数(因为栈中存的是下标,所以是以该元素为下标所对应的数)大于 \(a_i\) 时,停止弹栈,并把下标 \(i\) 压入栈,而那些弹出的下标,开一个 \(r_i\) 数组,让弹出的每个元素的 \(r\) 等于 \(i\)。最后,输出的 \(r_i\) 就是第 \(i\) 个数右边第一个大于它的数的下标。
#include<bits/stdc++.h>
using namespace std;
const int N=3e6+5;
int n,a[N],f[N],st[N],top;
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++){
while(top&&a[st[top]]<a[i]) f[st[top--]]=i;
st[++top]=i;
}
for(int i=1;i<=n;i++) cout<<f[i]<<" ";
return 0;
}
接着是一些练习题:
luogu B3666 求数列所有后缀最大值的位置
本题只需利用异或特性,在弹出每个元素时,用 \(ans\) 异或该元素,然后输出 \(ans\) 即可
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N=1e6+5;
unsigned long long a[N],st[N];
int n,top,ans;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++){
while(top&&a[st[top]]<=a[i]) ans^=st[top],top--;
st[++top]=i;
ans^=i;
cout<<ans<<endl;
}
return 0;
}
luogu P6503 [COCI 2010/2011 #3] DIFERENCIJA
给出一个长度为 \(n\) 的序列 \(a_i\),求出下列式子的值:
其中 \(2 \le n \le 3e5\),\(1 \le a_i \le 1e8\)。
不难想到 \(O(n^2)\) 的暴力做法,这里不过多赘述。
考虑另一种思路:
拆成最大值和最小值两部分的
一个数 \(a_i\) (先考虑最大值)对最后的答案有贡献,当且仅当其在一个区间 \([L,R]\) 中是最大的数,其贡献就是最大的满足这个条件的区间的 \([L,R]\) 的长度乘它这个数,即:\((R-L+1)*a_i\),所以我们要求的就是每个 \(i\) 所对应的 \([L,R]\),用单调栈正反分别跑一次,分别求出每个 \(R\),\(L\)。最小值部分的贡献也同理。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=3e5+5;
int n,t1,t2,a[N];
ll l[2][N],r[2][N],s1[N],s2[N],ans;
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++){
r[0][i]=r[1][i]=n;
while(t1&&a[s1[t1]]<=a[i]) r[0][s1[t1]]=i-1,t1--;
while(t2&&a[s2[t2]]>=a[i]) r[1][s2[t2]]=i-1,t2--;
s1[++t1]=i;
s2[++t2]=i;
}
t1=t2=0;
for(int i=n;i;i--){
l[0][i]=l[1][i]=1;
while(t1&&a[s1[t1]]<a[i]) l[0][s1[t1]]=i+1,t1--;
while(t2&&a[s2[t2]]>a[i]) l[1][s2[t2]]=i+1,t2--;
s1[++t1]=i;
s2[++t2]=i;
}
for(int i=1;i<=n;i++){
ans+=1ll*a[i]*((i-l[0][i]+1)*(r[0][i]-i+1)-(r[1][i]-i+1)*(i-l[1][i]+1));
}
cout<<ans;
return 0;
}
luogu P2422 良好的感觉
这题依旧一样,仍然用单调栈求出每个 \(i\) 所能到达的最大的 \([L,R]\),满足 \(a_i \le a_j\) \((i,j∈[L,R])\) 即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
int n,l[N],r[N];
ll a[N],sum[N],st[N],top,ans;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i];
}
for(int i=1;i<=n;i++){
while(top&&a[st[top]]>=a[i]) top--;
l[i]=st[top];
st[++top]=i;
}
top=0;
st[top]=n+1;
for(int i=n;i>0;i--){
while(top&&a[st[top]]>=a[i]) top--;
r[i]=st[top]-1;
st[++top]=i;
}
for(int i=1;i<=n;i++) ans=max(ans,(sum[r[i]]-sum[l[i]])*a[i]);
cout<<ans;
return 0;
}
单调队列
单调队列解决的是求某一 滑动窗口 的区间最值问题,虽然叫单调队列,但事实上是基于双端队列实现的,我一般喜欢用手写的双端队列,当然,\(STL\) 有双端队列容器 \(deque\),但我觉得,像这种线性的数据结构自己手写就好了(但是像优先队列,\(map\) 和 \(set\) 这种容器肯定要用 \(STL\),应该没人想要赛时手写这玩意吧。。。)
好了,说回正题,考虑怎么维护单调队列?以求长度为 \(L\) 的区间最小值为例:
维护一个由队头到队尾 严格单调递增 的双端队列,那么队头就是最小值(注意,双端队列里存的一般还是下标,这有利于后面维护长度 \(L\),并且把双端队列简称为队列)
两种操作:
1.当加入的数 \(a_i\) 会破坏队列的单调性,即 \(a_i\) 大于等于队尾元素所对应的数时,弹出队尾,直到队列为空或者满足单调性
2.当队首的元素已经超出了滑动窗口最左段,弹出队首
对于每个下标 \(i\),每次进行完操作 \(2\) 后就把队首的最值更新 \(i\) 的答案,然后把 \(i\) 的答案通过操作 \(1\) 入队(当然还是入的下标)
luogu P1886 滑动窗口 /【模板】单调队列
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,k,a[N],q[N],hd=1,tl;
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++){
while(hd<=tl&&a[q[tl]]>=a[i]) tl--;
q[++tl]=i;
while(hd<=tl&&q[hd]<=i-k) hd++;
if(i>=k) cout<<a[q[hd]]<<" ";
}
cout<<endl;
hd=1,tl=0;
for(int i=1;i<=n;i++){
while(hd<=tl&&a[q[tl]]<=a[i]) tl--;
q[++tl]=i;
while(hd<=tl&&q[hd]<=i-k) hd++;
if(i>=k) cout<<a[q[hd]]<<" ";
}
return 0;
}
luogu B3667 求区间所有后缀最大值的位置
披着羊皮的板题
代码:
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N=1e6+5;
int n,k,l=1,r,q[N];
unsigned long long a[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
while(l<=r&&q[l]+k<=i) l++;//注意本题维护滑动窗口的边界,要算上当前下标i,所以要带等号
while(l<=r&&a[q[r]]<=a[i]) r--;
q[++r]=i;
if(i>=k) cout<<r-l+1<<endl;
}
return 0;
}
单调队列一般是用于优化的,例如多重背包,\(DP\) 等
luogu P1776 宝物筛选(多重背包单调队列优化)
#include<bits/stdc++.h>
using namespace std;
const int N=105,M=1e5+5;
int n,m,v,w,s,f[M],l,r,q[M],num[M];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>w>>v>>s;
int len=min(s,m/v);
for(int b=0;b<v;b++){
l=1,r=0;
for(int y=0;y<=(m-b)/v;y++){
int tmp=f[b+y*v]-y*w;
while(l<=r&&q[r]<=tmp) r--;
q[++r]=tmp;
num[r]=y;
while(l<=r&&num[l]<y-len) l++;
f[b+y*v]=max(f[b+y*v],q[l]+y*w);
}
}
}
cout<<f[m];
return 0;
}
luogu P2627 [USACO11OPEN] Mowing the Lawn G
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
int n,m,l=1,r;
ll a[N],f[N][2],sum[N],q[N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++){
sum[i]=sum[i-1]+a[i];
f[i][0]=max(f[i-1][0],f[i-1][1]);
while(l<=r&&q[l]<i-m) l++;
if(i<=m) f[i][1]=sum[i];
else f[i][1]=f[q[l]][0]-sum[q[l]]+sum[i];
while(l<=r&&f[q[r]][0]-sum[q[r]]<=f[i][0]-sum[i]) r--;
q[++r]=i;
}
cout<<max(f[n][0],f[n][1]);
return 0;
}
目前先到这里,11月会继续更新的,也会把上面的解释补充一下

浙公网安备 33010602011771号