單調佇列&單調棧專題訓練(不斷更新中~)
練這個專題的原因是被多校賽第三場 Problem A. Ascending Rating 虐了, 看別人部落格發現只有一句解釋:單調佇列的應用 。
這才決定要把單調佇列單調棧摸熟(感謝多校賽虐我千百遍)
1、單調佇列
給出下標為1 -- n 的陣列 ,從左到右掃(也可以從右向左), 對於當前元素i , 從隊尾不斷地把小於a[i]的出隊(規則自己定)
維護一個從隊頭到隊尾遞減的佇列。多用於維護區間內最大值, 或者區間內遞增序列包含的元素個數。
題意:
給1-n陣列,每次移動長度m的視窗,求視窗第一個元素 到 視窗內最大元素之間遞增序列個數, 和最大元素的值(具體題意不是這樣的 ,只不過求得上訴資料再處理一下就得到答案了)
思路:
本題考慮的是對於 i 而言,[i,i+m-1]區間對 i位置 的影響, 因此此題從右往左掃。滿足區間長度為m 後才開始計算值
維護一個長度為m的嚴格遞減單調佇列, 佇列裡的內容就是當前元素a[i] 向後掃的 嚴格大於a[i]的元素。且隊首元素就是區間m內的最大值
程式碼:
#include <iostream> #include <cstdio> #include <map> #include <cmath> #include <queue> #include <cstring> using namespace std; const int maxn = 1e7+5; typedef long long ll; ll a[maxn], qq[maxn]; int main() { int T; scanf("%d",&T); while(T--) { ll n,m,k,p,q,r,mod; scanf("%lld%lld%lld%lld%lld%lld%lld",&n,&m,&k,&p,&q,&r,&mod); ll i=1; for(; i<=k; i++) scanf("%lld",&a[i]); while(i<=n) { a[i] = (p*a[i-1]+q*i+r)%mod; i++; } ll tail = 0,head = 1; // head 隊頭 tail隊尾,因為是 ++tail = i 所以tail置0 ll ansa = 0,ansb = 0; for(ll i=n;i;i--) { while(tail>=head && a[qq[tail]] <= a[i]) // 當前元素 ,把隊尾的小弱雞全踢出去 tail--; qq[++tail] = i; if(i<=n-m+1) // 後面區間長度 m { while(tail>=head && qq[head]>i+m-1) // 超出區間m了 head++; ansa += i^a[qq[head]]; ansb += i^(tail-head+1); } } printf("%lld %lld\n",ansa,ansb); } return 0; }
2)淹沒木板
問題描述
地上從左到右豎立著 n 塊木板,從 1 到 n 依次編號,如下圖所示。我們知道每塊木板的高度,在第 n 塊木板右側豎立著一塊高度無限大的木板,現對每塊木板依次做如下的操作:對於第 i 塊木板,我們從其右側開始倒水,直到水的高度等於第 i 塊木板的高度,倒入的水會淹沒 ai 塊木板(如果木板左右兩側水的高度大於等於木板高度即視為木板被淹沒),求 n 次操作後,所有 ai 的和是多少。如圖上所示,在第 4 塊木板右側倒水,可以淹沒第 5 塊和第 6 塊一共 2 塊木板,a4 = 2。
思路:
這是個簡單題,有多種思路, 從左向右掃, 把隊尾元素小於當前元素的彈出隊,1)則剩餘在佇列內的,就是當前能對其的貢獻,ans += tail - head +1 。 2)彈出隊的,當前點對他產生價值,ans += i - qq[tail]-1
程式碼:第一種
#include <iostream>
#include <cstdio>
#include <map>
#include <cmath>
#include <queue>
#include <cstring>
using namespace std;
const int maxn = 1e7+5;
typedef long long ll;
int a[maxn], qq[maxn];
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
int n;
scanf("%d",&n);
int head = 1, tail = 0;
for(int i=1; i<=n; i++)
scanf("%d",&a[i]);
int ans = 0;
for(int i=1; i<=n; i++)
{
while(head <= tail && a[qq[tail]] < a[i])
tail --;
ans += tail - head + 1;
qq[++tail] = i;
}
printf("%d\n",ans);
}
return 0;
}
3)單調佇列優化DP
經過幾個月辛勤的工作,FJ決定讓奶牛放假。假期可以在1…N天內任意選擇一段(需要連續),每一天都有一個享受指數W。但是奶牛的要求非常苛刻,假期不能短於P天,否則奶牛不能得到足夠的休息;假期也不能超過Q天,否則奶牛會玩的膩煩。FJ想知道奶牛們能獲得的最大享受指數。
Input
第一行:N,P,Q.
第二行:N個數字,中間用一個空格隔開,每個數都在longint範圍內。
Output
一個整數,奶牛們能獲得的最大享受指數。
Sample Input
5 2 4
-9 -4 -3 8 -6
Sample Output
5
Data Constraint
50% 1≤N≤10000
100% 1≤N≤100000
1<=p<=q<=n
思路:
求區間和,那麼就用字首和思想 ,令sum【r】 = sun[r-1]+a[r] ,那麼區間[l , r]的和就等於 sum[r] - sum[l - 1] 。我們令R固定,L的取值範圍是 R-Q+1 <= L <= R-P+1 ,則狀態轉移方程為 DP[R] = DP [J] + a[R] ( R-Q <= L <= R-P max(DP[J]) )使用單調佇列維護區間範圍 [ R-Q , R-P] 內的sum[J]陣列最小值。
程式碼:
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
const int maxn = 10050;
const int inf = 0x3f3f3f3f;
int dp[maxn], a[maxn], qq[maxn];
int main()
{
int n, p ,q ;
scanf("%d%d%d",&n,&p,&q);
a[0] = 0;
for(int i=1; i<=n; i++)
{ int t;
scanf("%d",&t);
a[i] = a[i-1] + t; // 字首和
}
int head = 1, tail = 0;
int ans = -inf;
for(int i=p; i<=n; i++)
{
while(head <= tail && a[i-p] <= a[qq[tail]])
tail--;
while(head <= tail && qq[head]<i-q)
head++;
qq[++tail] = i-p;
ans = max(ans, a[i] - a[qq[head]]);
}
printf("%d\n",ans);
return 0;
}
2、單調棧
多用於找到當前點左邊(右邊) 第一個大於當前值(小於)的位置。
例如希望找到左邊第一個比當前大的元素, 加入一個元素之前,對棧頂進行操作,遇到比他小的 ,出棧, 否則就是我們要的結果
還是淹沒木板那題
發現做法和單調佇列很像, 陣列模擬僅僅是踢出元素方式不同, 不過在其他應用裡 這兩種資料結構還是有不一樣的奇效
程式碼:
include <iostream>
#include <cstdio>
#include <map>
#include <cmath>
#include <queue>
#include <cstring>
using namespace std;
const int maxn = 1e7+5;
typedef long long ll;
const int inf = 0x3f3f3f3f;
int a[maxn], qq[maxn];
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++)
scanf("%d",&a[i]);
int ans = 0;
int top = 0, bottom = 1;
a[n+1] = inf; // n+1 個板子,無窮大
for(int i=1; i<=n+1; i++)
{
while(top >= bottom && a[qq[top]] < a[i])
{
ans += i - qq[top] - 1; // 比如第三個板子 8 此時棧頂是 第二個板子5 則貢獻是 3-2-1 = 0
top--;
}
qq[++top] = i;
}
printf("%d\n",ans);
}
return 0;
}