單調隊列
概念
顧名思義,單調隊列就是在隊列的基礎上,維護一個單調的序列。
性質
- 隊列中的元素其對應在原來的序列中的順序必須是單調遞增的。
- 隊列中元素的大小必須是單調遞(增/減/自定義)。
先來一道模板題來感受一下單調隊列的應用:
模板題:滑動窗口
題目
【題目描述】
有一個長為 n 的序列 a,以及一個大小為 k 的窗口。現在這個從左邊開始向右滑動,每次滑動一個單位,求出每次滑動后窗口中的最大值和最小值。
例如:
【輸入格式】
輸入一共有兩行,第一行有兩個正整數 n,k。 第二行 n 個整數,表示序列 a
【輸出格式】
輸出共兩行,第一行為每次窗口滑動的最小值
第二行為每次窗口滑動的最大值
【輸入樣例】
8 3 1 3 -1 -3 5 3 6 7
【輸出樣例】
-1 -3 -3 -3 3 3 3 3 5 5 6 7
【數據規模】
對於 50% 的數據,1≤n≤105;
對於 100% 的數據,1≤k≤n≤106,ai∈[−231,231)。
解析
單調隊列模板題。
對於最小值來說,我們維護一個單調遞增隊列,
這是因為我們要讓隊列的頭為該區間的最小值,那么后一個數要比頭大,
因為是單調的,所以每一個進來的數,都應該比隊列中的數大,所以是單調遞增隊列。
題目中還有一個限制條件,那便是窗口大小為k,所以我們要時刻維護隊列中的數的下標大於當前下標減去k,
如果不滿足該條件,就從隊列頭刪去該數,可見單調隊列是個雙端隊列,這也便是為什么不用棧的原因。
具體實現時,我們令head表示隊列頭+1,tail表示隊列尾,
那么問題來了,為什么head要+1呢?
試想一下,如果head不+1,那么當head=tail時,隊列中到底是沒有數還是有1個數呢?顯然無法判斷。
所以我們令head的值+1,當head<=tail時,隊列中便是有值的,如果head>tail,隊列便為空。
我們用樣例來模擬一下單調隊列,以求最小值為例:
- i=1,隊列為空,1進隊,[1]
- i=2,3比1大,滿足單調性,3進隊,[1,3]
- i=3,-1比3小,破壞單調性,3出隊,-1比1小,1出隊,隊列為空,-1進隊[-1],此時i>=k,輸出隊頭,即-1
- i=4,-3比-1小,-1出隊,隊列為空,-3進隊[-3],輸出-3
- i=5,5比-3大,5進隊,[-3,5],輸出-3
- i=6,3比5小,5出隊,3比-3大,3進隊,[-3,3],輸出-3
- i=7,-3下標為4,i-4=3,大於等於k,-3已不在區間中,-3出隊,6比3大,6進隊,[3,6],輸出3
- i=8,7比6大,7進隊,[3,6,7],輸出3
這樣最小值便求完了,最大值同理,只需在判斷時改變符號即可。
Code

#include <algorithm> #include <iostream> #include <cstring> #include <string> #include <cstdio> #include <cmath> using namespace std; const int N=1e6+500; int n,k,a[N],q[N],head=1,tail;//head要+1 int main() { scanf("%d %d",&n,&k); for(int i=1;i<=n;i++) { //求最小值 scanf("%d",&a[i]); while(head<=tail&&q[head]<=i-k) head++;//隊頭顯然是最早進入的,如果隊頭的下標大於i-k,該數便不在區間內了,從隊頭刪除 while(head<=tail&&a[q[tail]]>=a[i]) tail--;//當前數破壞了單調性,從隊尾刪除,直至隊中數小於當前數 q[++tail]=i;//當前元素進隊 if(i>=k) printf("%d ",a[q[head]]);//輸出每個區間最小值 } printf("\n"); head=1,tail=0; for(int i=1;i<=n;i++) { //求最大值 while(head<=tail&&q[head]<=i-k) head++; while(head<=tail&&a[q[tail]]<=a[i]) tail--; q[++tail]=i;//當前元素進隊 if(i>=k) printf("%d ",a[q[head]]); } return 0; }
學廢單調隊列了嗎?
再來幾道例題練練手吧!
例題一:切蛋糕
題目
【題目描述】
今天是小Z的生日,同學們為他帶來了一塊蛋糕。這塊蛋糕是一個長方體,被用不同色彩分成了N個相同的小塊,每小塊都有對應的幸運值。
小Z作為壽星,自然希望吃到的第一塊蛋糕的幸運值總和最大,但小Z最多又只能吃M小塊(M≤N)的蛋糕。
吃東西自然就不想思考了,於是小Z把這個任務扔給了學OI的你,請你幫他從這N小塊中找出連續的k塊蛋糕(k≤M),使得其上的幸運值最大。
【輸入格式】
輸入文件cake.in的第一行是兩個整數N,M。分別代表共有N小塊蛋糕,小Z最多只能吃M小塊。
第二行用空格隔開的N個整數,第i個整數Pi代表第i小塊蛋糕的幸運值。
【輸出格式】
輸出文件cake.out只有一行,一個整數,為小Z能夠得到的最大幸運值。
【輸入樣例】
6 3 1 -2 3 -4 5 -6
【輸出樣例】
5
【數據規模】
對20%的數據,N≤100。
對100%的數據,N≤500000,|Pi|≤500。 答案保證在2^31-1之內。
解析
因為蛋糕是連續的,所以不難聯想到前綴和,令sum[i]表示從第1塊到第i塊蛋糕的幸運值之和。
於是很自然的想到了暴力:從1到n枚舉i,從i-M+1到i枚舉j,那么最大幸運值maxn=max(maxn,sum[i]-sum[j-1])
但是這樣顯然會超時,考慮優化。
對於每一個i來說,實際上我們只需要找到最小的sum[j-1]即可,所以我們可以用單調遞增隊列來維護最小的sum[j-1]的值,
那么這不就是一個滑動窗口么?數列為sum[1]~sum[n],區間長度為1~M,求每個區間的最小值,
唯一不同的就是區間長度不是一個定值,而是1~M,但這也不難辦,依舊只需保證隊列長度不超過M即可。
Code

#include <algorithm> #include <iostream> #include <cstring> #include <string> #include <cstdio> #include <cmath> #include <queue> using namespace std; inline int read()//快讀 { int num=0,w=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') w=-1; ch=getchar(); } while(ch>='0'&&ch<='9') { num=(num<<1)+(num<<3)+ch-'0'; ch=getchar(); } return num*w; } const int N=500100; int n,m,p[N],sum[N],q[N],head=1,tail,maxn; int main() { n=read(),m=read(); for(int i=1;i<=n;i++) { p[i]=read(),sum[i]=sum[i-1]+p[i],maxn=max(maxn,p[i]);//可能只有一個 /*for(int j=i-m+1;j<=i;j++) 40分 maxn=max(maxn,sum[i]-sum[j-1]);*/ while(head<=tail&&i-q[head]>m) head++; maxn=max(maxn,sum[i]-sum[q[head]]); while(head<=tail&&sum[q[tail]]>=sum[i]) tail--; q[++tail]=i; } printf("%d",maxn); return 0; }
例題二:Flowerpot S
題目
【題目描述】
老板需要你幫忙澆花。給出N滴水的坐標,y表示水滴的高度,x表示它下落到x軸的位置。
每滴水以每秒1個單位長度的速度下落。你需要把花盆放在x軸上的某個位置,使得從被花盆接着的第1滴水開始,到被花盆接着的最后1滴水結束,之間的時間差至少為D。
我們認為,只要水滴落到x軸上,與花盆的邊沿對齊,就認為被接住。給出N滴水的坐標和D的大小,請算出最小的花盆的寬度W。
【輸入格式】
第一行2個整數 N 和 D。
第2.. N+1行每行2個整數,表示水滴的坐標(x,y)。
【輸出格式】
僅一行1個整數,表示最小的花盆的寬度。如果無法構造出足夠寬的花盆,使得在D單位的時間接住滿足要求的水滴,則輸出-1。
【輸入樣例】
4 5 6 3 2 4 4 10 12 15
【輸出樣例】
2
【樣例解釋】
有4滴水, (6,3), (2,4), (4,10), (12,15).水滴必須用至少5秒時間落入花盆。花盆的寬度為2是必須且足夠的。把花盆放在x=4..6的位置,它可以接到1和3水滴, 之間的時間差為10-3 = 7滿足條件。
【數據規模】
40%的數據:1 ≤ N ≤ 1000,1 ≤ D ≤ 2000;
100%的數據:1 ≤ N ≤ 100000,1 ≤ D ≤ 1000000,0≤x,y≤10^6。
解析
先設想一下,如果我們已經知道了W,那么怎么求時間差是否大於等於D?
顯然,我們只需判斷每個長為W的區間,最早落下的和最晚落下的水滴的時間差是否大於等於D即可。
也就只需要知道最小的y與最大的y即可,這不就是滑動窗口嗎?
將水滴按照x從小到大排序,水滴序列便滿足單調隊列的性質1:序列為順序
於是問題就成了:給定一個序列(y值),區間長度為W,求每個區間的最大值和最小值。
分別用維護最大值的單調遞減隊列和維護最小值的單調遞增隊列即可。
時間差的問題解決了,那么W怎么知道?
二分!
令l=0,因為這題寬度為實際寬度-1,例如樣例中的4~6實際寬度為3,輸出卻是2,而對於3~3這樣實際寬度為1的,應輸出0,故l從0開始
r=最大的x+1,如果連W=最大的x時都無法滿足題意時,l便會等於r,即l=最大的x+1,這樣我們只需在二分完后判斷l是否等於最大的x+1,就可以知道有無答案了。
Code

#include <algorithm> #include <iostream> #include <cstring> #include <string> #include <cstdio> #include <cmath> #include <queue> using namespace std; inline int read()//快讀 { int num=0; char ch=getchar(); while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9') { num=(num<<1)+(num<<3)+ch-'0'; ch=getchar(); } return num; } const int N=100100; struct rec{ int x,y; }a[N]; int n,d,minq[N],maxq[N]; bool cmp(rec p,rec q) { return p.x<q.x; } bool check(int w) { int minh=1,mint=0,maxh=1,maxt=0; for(int i=1;i<=n;i++) { while(minh<=mint&&a[i].x-a[minq[minh]].x>w) minh++;//最小值 while(maxh<=maxt&&a[i].x-a[maxq[maxh]].x>w) maxh++;//最大值 while(minh<=mint&&a[minq[mint]].y>=a[i].y) mint--; while(maxh<=maxt&&a[maxq[maxt]].y<=a[i].y) maxt--; minq[++mint]=i,maxq[++maxt]=i; if(a[maxq[maxh]].y-a[minq[minh]].y>=d) return true;//符合題意 } return false; } int main() { n=read(),d=read(); for(int i=1;i<=n;i++) a[i].x=read(),a[i].y=read(); sort(a+1,a+n+1,cmp);//按x從小到大排序 int l=0,r=a[n].x+1;//+1是為了讓l能夠比a[n].x大,令下面判斷是否應輸出-1 while(l<r)//二分查找最小的W { int mid=(l+r)>>1; if(check(mid)) r=mid; else l=mid+1; } if(l==a[n].x+1) printf("-1"); else printf("%d",l); return 0; }
例題三:圍欄
題目
【題目描述】
有N塊木板從左到右排成一行,有M個工匠對這些木板進行粉刷,每塊木板至多被粉刷一次。
第 i 個木匠要么不粉刷,要么粉刷包含木板 Si 的,長度不超過 Li 的連續的一段木板,每粉刷一塊可以得到 Pi 的報酬。
不同工匠的Si不同。
請問如何安排能使工匠們獲得的總報酬最多。
【輸入格式】
第一行包含兩個整數N和M。
接下來M行,每行包含三個整數Li,Pi,Si。
【輸出格式】
輸出一個整數,表示結果。
【輸入樣例】
8 4 3 2 2 3 2 3 3 3 5 1 1 7
【輸出樣例】
17
【數據規模】
1≤N≤16000,
1≤M≤100,
1≤Pi≤10000
解析
顯然,這是一道動態規划題。
令f[i][j]表示前i個工匠粉刷前j塊木板(可以有空着不刷的木板),工匠們能獲得的最多報酬。
狀態轉移方程:f[i][j]=
- f[i-1][j] 即第i個工匠不刷
- f[i][j-1] 即第j塊木板不刷
- f[i-1][k]+Pi*(j-k) 其中 j-Li ≤k ≤Si-1&&j ≥Si 即第i個工匠刷第k+1到第j塊木板
事實上,對於第3種轉移方式可以改寫成f[i][j]=Pi*j+max(f[i-1][k]-Pi*k)
當j增大時,對於k1與k2(k1<k2<Si-1)來說,顯然k1會更早離開[j-Li,Si-1]這個區間,
與此同時,如果f[i-1][k1]-Pi*k1≤f[i-1][k2]-Pi*k2,那么k2顯然比k1更優,即k1是無用的決策,應排除出候選集合。
所以我們可以維護一個k單調遞增,f[i-1][k]-Pi*k單調遞減的隊列,該隊列維護方式如下:
- 當j變大時,從隊頭將小於j-Li的決策出隊
- 新的決策入隊時,在隊尾將破壞f[i-1][k]-Pi*k的單調性的決策出隊,然后將新的決策入隊
那么對於每種情況,最優決策即為隊頭。
需要注意的是,如果Si-Li小於0,那么k應該從0開始,畢竟k不能為負數。
Code

#include <algorithm> #include <iostream> #include <cstring> #include <string> #include <cstdio> #include <cmath> #include <queue> using namespace std; inline int read()//快讀 { int num=0; char ch=getchar(); while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9') { num=(num<<1)+(num<<3)+ch-'0'; ch=getchar(); } return num; } const int N=16500,M=150; struct rec{ int l,p,s; }a[M]; int n,m,f[M][N],q[N],head,tail; /*f[i][j]表示前i個工匠粉刷前j塊木板(可以有空着不刷的木板),工匠們能獲得的最多報酬。 f[i][j]=max(f[i][j],以下三種情況) 1、f[i-1][j] 第i個工匠不刷 2、f[i][j-1] 第j塊木板不刷 3、f[i-1][k]+Pi*(j-k) 其中 j-Li ≤k ≤Si-1,j ≥Si 第i個工匠刷第k+1到第j塊木板 3可以改為f[i][j]=Pi*j+max(f[i-1][k]-Pi*k) */ bool cmp(rec x,rec y) { return x.s<y.s; } int calc(int i,int k) { return f[i-1][k]-a[i].p*k; } int main() { n=read(),m=read(); for(int i=1;i<=m;i++) a[i].l=read(),a[i].p=read(),a[i].s=read(); sort(a+1,a+m+1,cmp);//按Si從小到大排序,令序列為順序 for(int i=1;i<=n;i++) { head=1,tail=0; for(int k=max(0,a[i].s-a[i].l);k<=a[i].s-1;k++)//Si-Li<0時,k從0開始 { while(head<=tail&&calc(i,q[tail])<=calc(i,k)) tail--; q[++tail]=k; } for(int j=1;j<=n;j++) { f[i][j]=max(f[i-1][j],f[i][j-1]);//第一種轉移方式和第二種轉移方式 if(j>=a[i].s) { while(head<=tail&&q[head]<j-a[i].l) head++; if(head<=tail) f[i][j]=max(f[i][j],calc(i,q[head])+a[i].p*j);//第三種轉移方式 } } } printf("%d",f[m][n]); return 0; }
例題四:理想的正方形
題目
【題目描述】
有一個a*b的整數組成的矩陣,現請你從中找出一個n*n的正方形區域,使得該區域所有數中的最大值和最小值的差最小。
【輸入格式】
第一行為3個整數,分別表示a,b,n的值
第二行至第a+1行每行為b個非負整數,表示矩陣中相應位置上的數。每行相鄰兩數之間用一空格分隔。
【輸出格式】
僅一個整數,為a*b矩陣中所有“n*n正方形區域中的最大整數和最小整數的差值”的最小值。
【輸入樣例】
5 4 2 1 2 5 6 0 17 16 0 16 17 2 1 2 10 2 1 1 2 2 2
【輸出樣例】
1
【數據規模】
(1)矩陣中的所有數都不超過1,000,000,000
(2)20%的數據2<=a,b<=100,n<=a,n<=b,n<=10
(3)100%的數據2<=a,b<=1000,n<=a,n<=b,n<=100
解析
二維的單調隊列題目。
考慮如何求每個正方形的最大值:
枚舉每一行,求出每個長度為n的區間的最大值,這不正是滑動窗口嗎?
將求出來的maxx[][]作為一個新的矩形,然后從1到b-n+1枚舉列,
依舊是滑動窗口求出每列的maxx[][]最大值,記錄為maxy[][],
又形成了一個新的矩形,這便是每個正方形的最大值。
不理解的可以看下面的圖,以樣例為例。
初始矩形:
maxx[][]矩形:
maxy[][]矩形:
最小值同理。
最后只需從1到a-n+1枚舉i,從1到b-n+1枚舉j,求出最小的maxy[i][j]-miny[i][j]即可。
Code

#include <algorithm> #include <iostream> #include <cstring> #include <string> #include <cstdio> #include <cmath> #include <queue> using namespace std; inline int read()//快讀 { int num=0; char ch=getchar(); while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9') { num=(num<<1)+(num<<3)+ch-'0'; ch=getchar(); } return num; } const int N=1010; int a,b,n,s[N][N],head,tail,q[N],maxx[N][N],maxy[N][N],minx[N][N],miny[N][N],minn=0x7f7f7f7f; int main() { a=read(),b=read(),n=read(); for(int i=1;i<=a;i++) for(int j=1;j<=b;j++) s[i][j]=read(); for(int i=1;i<=a;i++)//每一行的每個區間的最大值 { head=1,tail=0; for(int j=1;j<=b;j++) { while(head<=tail&&q[head]<=j-n) head++; while(head<=tail&&s[i][q[tail]]<=s[i][j]) tail--; q[++tail]=j; if(j>=n) maxx[i][j-n+1]=s[i][q[head]]; } } for(int i=1;i<=b-n+1;i++)//每個正方形的最大值 { head=1,tail=0; for(int j=1;j<=a;j++) { while(head<=tail&&q[head]<=j-n) head++; while(head<=tail&&maxx[q[tail]][i]<=maxx[j][i]) tail--; q[++tail]=j; if(j>=n) maxy[j-n+1][i]=maxx[q[head]][i]; } } for(int i=1;i<=a;i++)//每一行每個區間的最小值 { head=1,tail=0; for(int j=1;j<=b;j++) { while(head<=tail&&q[head]<=j-n) head++; while(head<=tail&&s[i][q[tail]]>=s[i][j]) tail--; q[++tail]=j; if(j>=n) minx[i][j-n+1]=s[i][q[head]]; } } for(int i=1;i<=b-n+1;i++)//每個正方形的最小值 { head=1,tail=0; for(int j=1;j<=a;j++) { while(head<=tail&&q[head]<=j-n) head++; while(head<=tail&&minx[q[tail]][i]>=minx[j][i]) tail--; q[++tail]=j; if(j>=n) miny[j-n+1][i]=minx[q[head]][i]; } } for(int i=1;i<=a-n+1;i++) for(int j=1;j<=b-n+1;j++) minn=min(minn,maxy[i][j]-miny[i][j]);//每個正方形的最大值減去最小值 printf("%d",minn); return 0; }