單調隊列定義:
其實單調隊列就是一種隊列內的元素有單調性的隊列,因為其單調性所以經常會被用來維護區間最值或者降低DP的維數已達到降維來減少空間及時間的目的。
單調隊列的一般應用:
1.維護區間最值
2.優化DP
例題引入:
求m區間內的最小值:https://www.luogu.org/problemnew/show/P1440
一個含有n項的數列(n<=2000000),求出每一項前的m個數到它這個區間內的最小值。若前面的數不足m項則從第1個數開始,若前面沒有數則輸出0。
例題解答:
首先看到題目可以很快想到O(NM),對於2*10^6這樣的數據無疑要TLE的;
接下來考慮用單調隊列,因為每一個答案只與當前下標的前m個有關,所以可以用單調隊列維護前m的個最小值,
考慮如何實現該維護的過程??
顯然當前下標X的m個以前的元素(即下標小於X-M+1的元素)肯定對答案沒有貢獻,所以可以將其從單調隊列中刪除。
對於兩個元素A,B,下標分別為a,b,如果有A>=B&&a<b那么B留在隊列里肯定優於A,因此可以將A刪除。
維護隊首:如果隊首已經是當前元素的m個之前,將head++,彈出隊首元素
維護隊尾:比較q[tail]與當前元素的大小,若當前元素更優tail++,彈出隊尾元素,直到可以滿足隊列單調性后加入當前元素。
考慮單調隊列的時間復雜度:由於每一個元素只會進隊和出隊一次,所以為O(N)。
一般建議用數組模擬單調隊列進行操作,而不用系統自帶的容器,因為系統自帶容器不易調試且可能有爆空間的危險。
代碼實現:
#include<bits/stdc++.h> using namespace std; #define re register int #define INF 0x3f3f3f3f #define ll long long #define maxn 2000009 #define maxm inline ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ll)(ch-'0');ch=getchar();} return x*f; } int n,m,k,tot,head,tail; int a[maxn],q[maxn]; int main() { // freopen(".in","r",stdin); // freopen(".out","w",stdout); n=read(),m=read(); for(int i=1;i<=n;i++) a[i]=read(); head=1,tail=0;//起始位置為1 因為插入是q[++tail]所以要初始化為0 for(int i=1;i<=n;i++)//每次隊首的元素就是當前的答案 { printf("%d\n",a[q[head]]); while(i-q[head]+1>m&&head<=tail)//維護隊首 head++; while(a[i]<a[q[tail]]&&head<=tail)//維護隊尾 tail--; q[++tail]=i; } // fclose(stdin); // fclose(stdout); return 0; }
習題報告:
滑動窗口:https://www.luogu.org/problemnew/show/P1886
解題思路: 此題與例題相同,只是所要求的是最大值和最小值,只需要做兩遍單調隊列即可

#include<bits/stdc++.h> using namespace std; #define re register int #define ll long long #define maxn 1000009 #define maxm inline ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ll)(ch-'0');ch=getchar();} return x*f; } int q[maxn],a[maxn]; int n,m,k,ans,tot,head,tail; void Ask_MIN() { head=1,tail=0; for(int i=1;i<=n;i++) { while(head<=tail&&i-q[head]+1>m) head++; while(head<=tail&&a[q[tail]]>=a[i]) tail--; q[++tail]=i; if(i>=m) printf("%d ",a[q[head]]); } puts(""); } void Ask_MAX() { head=1,tail=0; for(int i=1;i<=n;i++) { while(head<=tail&&i-q[head]+1>m) head++; while(head<=tail&&a[q[tail]]<=a[i]) tail--; q[++tail]=i; if(i>=m) printf("%d ",a[q[head]]); } puts(""); } int main() { // freopen(".in","r",stdin); // freopen(".out","w",stdout); n=read(),m=read(); for(int i=1;i<=n;i++) a[i]=read(); Ask_MIN(); Ask_MAX(); fclose(stdin); fclose(stdout); return 0; }
擠奶牛:https://www.luogu.org/problemnew/show/P3088
解題思路:此題題目需要維護左和右分別D區間內的最大值,因此可以正着和倒着分別做一次單調隊列,然后打標記即可。

#include<bits/stdc++.h> using namespace std; #define re register int #define ll long long #define maxn 50009 #define maxm inline ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ll)(ch-'0');ch=getchar();} return x*f; } struct cow { int h,x; }p[maxn]; int q[maxn]; bool fear[maxn]; int n,m,k,ans,tot,head,tail; bool comp(cow a,cow b) { return a.x<b.x; } int main() { // freopen(".in","r",stdin); // freopen(".out","w",stdout); n=read(),m=read(); for(int i=1;i<=n;i++) p[i].x=read(),p[i].h=read(); sort(p+1,p+1+n,comp); head=1,tail=0; for(int i=1;i<=n;i++) { while(head<=tail&&p[i].x-p[q[head]].x>m) head++; while(head<=tail&&p[i].h>=p[q[tail]].h) tail--; q[++tail]=i; if(p[q[head]].h>=2*p[i].h) fear[i]=1; } head=1,tail=0; for(int i=n;i>=1;i--) { while(head<=tail&&p[q[head]].x-p[i].x>m) head++; while(head<=tail&&p[q[tail]].h<=p[i].h) tail--; q[++tail]=i; if(p[q[head]].h>=p[i].h*2&&fear[i]) ans++; } printf("%d\n",ans); fclose(stdin); fclose(stdout); return 0; }
好消息,壞消息:https://www.luogu.org/problemnew/show/P2629
解題思路:先斷環成鏈,便於操作,然后就變成求對於每一個合法的K,都要滿足k到(n-k+1)中,任意一點的和都是非負的,用前綴和計算區間和,那么只需要滿足sum[i]-sum[k-1]>=0(k<=i<=n+k-1),不需要判斷每一個點,只需要轉換一下變成判斷最小的sum[i]減去最大的sum[k-1]是否大於等於0就行了,因為只要有一個點為負數就已經不合法了。

#include<bits/stdc++.h> using namespace std; #define re register int #define ll long long #define maxn 1000009 #define maxm inline ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ll)(ch-'0');ch=getchar();} return x*f; } int sum[maxn<<1],q[maxn<<1],a[maxn<<1]; int n,m,k,ans,tot,head,tail; int main() { // freopen(".in","r",stdin); // freopen(".out","w",stdout); n=read(); for(int i=1;i<=n;i++) a[i]=read(),a[i+n]=a[i]; for(int i=1;i<=2*n;i++) sum[i]=sum[i-1]+a[i]; head=1,tail=0; for(int i=1;i<=n*2-1;i++) { while(head<=tail&&i-q[head]+1>n) head++; while(head<=tail&&sum[i]<=sum[q[tail]]) tail--; q[++tail]=i; if(i>=n&&sum[q[head]]-sum[i-n]>=0) ans++; } printf("%d\n",ans); fclose(stdin); fclose(stdout); return 0; }
單調隊列優化DP:
大部分單調隊列優化的DP都和定長連續子區間的最值問題有關。
單調隊列一般優化線性DP,形如dp[i]=max/min(dp[j])+val[i],且j<i,val[i]與dp[j]無關,此時優化的對象是dp[j]。
一般來說i,j是需要通過兩層嵌套循環來實現枚舉,但是因為dp[j]與val[i]無關,所以我們可以維護一下已經計算好了的dp[j],使其不需要用for循環來枚舉。
例題引入:
最大連續和:https://loj.ac/problem/10176
給你一個長度為 n 的整數序列,要求從中找出一段連續的長度不超過 m 的子序列,使得這個序列的和最大。
例題解答:
首先考慮DP方程:用dp[i]表示以i為結尾的長度不超過m的最大子序列和。
轉移為:dp[i]=max{sum[i]-sum[i-k],k=1.2....m}
=sum[i]-min{sum[i-k],k=1.2....m};
最后轉變為對於所有的1<=k<=m,找出所有sum[i-k]的最小值。
考慮用單調隊列來維護決策值sum[i-k]就行啦。
代碼實現:
#include<bits/stdc++.h> using namespace std; #define re register int #define ll long long #define maxn 200009 #define maxm inline ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ll)(ch-'0');ch=getchar();} return x*f; } int sum[maxn],q[maxn]; int n,m,k,ans,tot,mx,head,tail; int main() { // freopen(".in","r",stdin); // freopen(".out","w",stdout); n=read(),m=read(); for(int i=1;i<=n;i++) { int x=read(); sum[i]=sum[i-1]+x; } ans=-0xffffff; head=1,tail=1; for(int i=1;i<=n;i++) { while(head<=tail&&i-q[head]>m) head++; ans=max(ans,sum[i]-sum[q[head]]); while(head<=tail&&sum[q[tail]]>=sum[i]) tail--; q[++tail]=i; } printf("%d\n",ans); fclose(stdin); fclose(stdout); return 0; }
習題報告:
切蛋糕:https://www.luogu.org/problemnew/show/P1714
解題思路:和例題一樣。

#include<bits/stdc++.h> using namespace std; #define re register int #define ll long long #define maxn 500009 #define maxm inline ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ll)(ch-'0');ch=getchar();} return x*f; } int sum[maxn],q[maxn]; int n,m,k,ans,tot,mx,head,tail; int main() { // freopen(".in","r",stdin); // freopen(".out","w",stdout); n=read(),m=read(); for(int i=1;i<=n;i++) { int x=read(); sum[i]=sum[i-1]+x; } head=1,tail=0; for(int i=1;i<=n;i++) { while(head<=tail&&i-q[head]>m) head++; ans=max(ans,sum[i]-sum[q[head]]); while(head<=tail&&sum[q[tail]]>=sum[i]) tail--; q[++tail]=i; } printf("%d\n",ans); fclose(stdin); fclose(stdout); return 0; }
琪露諾:https://www.luogu.org/problemnew/show/P1725
解題思路:用dp[i]表示到達點i時獲得的最大冰凍指數,dp[i]=max{dp[i-j]}+a[i],l<=j<=r<=i.
然后對於求max{dp[i-j]}用單調隊列來優化。

#include<bits/stdc++.h> using namespace std; #define re register int #define ll long long #define maxn 200009 #define maxm inline ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ll)(ch-'0');ch=getchar();} return x*f; } int q[maxn],a[maxn],dp[maxn]; int n,m,k,ans,tot,head,tail,l,r; int main() { // freopen(".in","r",stdin); // freopen(".out","w",stdout); n=read(),l=read(),r=read(); for(int i=0;i<=n;i++) a[i]=read(); head=1,tail=0; for(int i=l;i<=n;i++) { while(head<=tail&&i-q[head]>r) head++; while(head<=tail&&dp[q[tail]]<=dp[i-l]) tail--; q[++tail]=i-l; dp[i]=dp[q[head]]+a[i]; } ans=0; for(int i=n-r+1;i<=n;i++) ans=max(ans,dp[i]); printf("%d\n",ans); fclose(stdin); fclose(stdout); return 0; }
最后推薦一個博客:https://blog.csdn.net/hjf1201/article/details/78729320
里面單調隊列優化DP的基本題型都有,比較齊全。