1. 程式人生 > >單調佇列&單調棧專題訓練(不斷更新中~)

單調佇列&單調棧專題訓練(不斷更新中~)

練這個專題的原因是被多校賽第三場 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;
}