单调栈 & 单调队列

单调栈 & 单调队列

如果一个人比你小,还比你强,那你就永远打不过他了。——单调队列

经过痛苦的折磨,我终于,把单调栈和单调队列学会并理解了,但做的题还比较简单。

单调栈

顾名思义,就是维护一个具有单调性的栈,用于解决求序列中第 \(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\),求出下列式子的值:

\[\sum_{i=1}^{n} \sum_{j=i}^{n} (\max_{i\le k\le j} a_k-\min_{i\le k\le j} a_k) \]

其中 \(2 \le n \le 3e5\)\(1 \le a_i \le 1e8\)

不难想到 \(O(n^2)\) 的暴力做法,这里不过多赘述。

考虑另一种思路:

\[\sum_{i=1}^{n} \sum_{j=i}^{n} (\max_{i\le k\le j} a_k-\min_{i\le k\le j} a_k) =\sum_{i=1}^{n} \sum_{j=i}^{n} \max_{i\le k\le j} a_k -\sum_{i=1}^{n} \sum_{j=i}^{n} \min_{i\le k\le j} a_k \]

拆成最大值和最小值两部分的

一个数 \(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月会继续更新的,也会把上面的解释补充一下

posted @ 2025-10-29 08:43  czh(抽纸盒)  阅读(6)  评论(0)    收藏  举报