1. 程式人生 > >單調佇列和單調棧

單調佇列和單調棧

目錄

單調棧

什麼是單調棧

單調棧的應用

排隊遞減單調棧

最大長方形遞增單調棧

單調佇列

什麼是單調佇列

單調佇列的應用

單調佇列的基本模板

單調佇列的重要應用DP


單調棧

什麼是單調棧

什麼叫做單調棧?

什麼是單調?

單調也就是序列中的元素是遞增或遞減的,也就是從大到小或者是從小到大的,不會有一個拐點。

單調棧自然就是棧中的元素維護著單調性。

單調棧的應用

排隊遞減單調棧

現在有一些小朋友排隊,一個小朋友可以看到右邊比他矮且沒有比自己高的小朋友遮擋的小朋友(有一點繞),求每個小朋友能看到的小朋友之和。

樣例

輸出

3

我們看到,1號小朋友(1)右邊比他矮的都沒有,所以看到了0個

2號小朋友(5)右邊比他矮的有3號小朋友(2)和5號小朋友(4)

但是5號小朋友被4號小朋友遮住了(9)(因為4號小朋友比2號小朋友高),所以2號小朋友看不到5號小朋友,只能看到1個小朋友(即3號小朋友(2))

依次我們可以得出

3號小朋友看到0個

4號小朋友看到2個

5號小朋友和6號小朋友都看到0個,所以一共看到了1+2=3個

 

解決1.暴力列舉

 

我們每次從i開始,直到迴圈到j比i大或者相等,那麼i號小朋友可以看到j-i-1個小朋友,時間複雜度為O(n^2)

如果我們的N是N<=100000,那這個演算法就不行了!

 

解決2.單調棧

我們發現,我們用了一個迴圈來找到i後面第一個大於等於他的元素,如果我們能一下子就知道,不就少了一個迴圈了嗎

就可以把時間降成O(n)了。

我們想一想,我們的棧如果是遞減的,那麼當一個數進入棧時,一定要把比他大的彈出去,這個時候就可以知道位置了!

1 5 2 9 4 5

第一步

將1放入棧中

第二步

將5放入棧中,由於我們維護的單調遞減棧,如果放入5後,1 5就沒有維護遞減了,所以為了維護遞減,我們把1彈出去,也就可以順便知道比1號同學大於等於的第一個同學就是2號同學了,ans+=(2-1-1) ans=0

第三步

將2放入棧中,發現可以維護遞減,直接放入,棧中的元素就是5 2了

第四步

將9放入棧中,發現不能維護遞減棧,所以依次彈出比9小的數來維護遞減棧,最後我們發現5和2都被彈出去了,也就說明了2號和3號同學右邊第一個比自己大於等於的同學是4號同學 ans+=(4-2-1)+(4-3-1) ans=1

第五步

將4放入棧中,發現可以維持遞減棧,直接放入,序列為9 4

第六步

將5放入棧中,發現不能維持遞減棧了,將比5小的數都彈出去,就把4彈出去了,也就說明了5號同學右邊第一個比自己大於等於的同學是6號同學,ans+=(6-5-1) ans=1,這時序列就是9 5了

最後一步

由於後面沒有同學了,但是棧中卻還有元素,所以我們要結尾,人工的在後面加一個極大值,並且假設是第n+1個同學

ans+=(7-4-1)+(7-6-1)  ans=3了(9代表的是4號同學的身高嘛,這裡減去的自然是編號啦)

恩,我相信你們懂了!!!

​
#include <cstdio>
#include <stack>
using namespace std;

struct node{
    int x, id;
};
int n, x, ans;
stack<node> q;

int main(){
    scanf("%d",&n);
    for(int i = 1; i <= n; i++){
        scanf("%d",&x);
        node h;
        h.x = x;
        h.id = i;
        while(!q.empty()){
            node t = q.top();
            if(t.x < x){
                ans += (i - t.id - 1);
                q.pop();
            }
            else {
                break;
            }
        }
        q.push(h);
    }
    while(!q.empty()){
        node t = q.top();
        q.pop();
        ans += n - t.id;
    }
    printf("%d\n",ans);
    return 0;
}

​


最大長方形遞增單調棧

有一塊草地,寬都為1,長為a[i],先在要找到一個長方形,使得他的面積最大,這個最大的面積是多少呢?

樣例

6

1 5 6 5 4 2

輸出

16

這個圖就很明顯了,對不對,你看啊,最大的肯定不是這個15對不對

因為還有更大的

這個就是16了嘛,他肯定最大啦!

怎麼做,我們想,我們如果以第a[i]為長度,那麼寬應該為多少呢

是不是第i個草左邊能延伸的最大長度加上右邊延伸的最大長度

就比如說,我們以第2塊草地來舉例,他最大隻能延伸到2了,因為第1個草地比他小,所以只能延伸到2

而他的右邊可以延伸到第5塊草地,因為之間的草地都比他自己大,所以肯定能延伸過去,恰好第6塊草地比他小了,所以就不能延伸了

所以第2塊草地可以延伸的寬度就為2-5的長度,即為4

其實也就是說,以第i塊草地為長的長方形,他的寬,就是找左右兩邊第一個比自己小的,從而得到了延伸的最大範圍,就可以知道寬度了

是不是和上面的排隊很像,排隊是求右邊最小的,這道題不過是還要求一個左邊最小的,再用一個單調棧不就行了

恩,第一種方法就是用兩個單調棧

而我們可以只用一個的,遞增棧,遞增怎麼做啊!

我們維護遞增時,是不是左邊的元素肯定比你小,而恰好也就是左邊第一個比你小的,而當你要被踢出去的時候,是不是也一定是後面比你小的元素把你踢出去的,這樣我們不就找到了寬度了嗎?

#include <cstdio>
#include<stack>
#include<iostream>
#define reg register
using namespace std;
 
struct node{
    int id,chang;
};
int n, x, ans;
stack<node> q;
 
inline void read(int &x){
    int f = 1;
    x = 0;
    char s = getchar();
    while(s < '0' || s > '9'){
        if(s == '-'){
            f = -1;
        }
        s = getchar();
    }
    while(s >= '0' && s <= '9'){
        x = (x << 3) + (x << 1) + (s - '0');
        s = getchar();
    }
    x *= f;
}
 
inline void wrtie(int x){
    if(x < 0){
        putchar('-');
        x *= -1;
    }
    if(x > 9){
        wrtie(x / 10);
    }
    putchar((x % 10) + '0');
}
 
int main(){
    node h;
    read(n);
    for(reg int i = 1; i <= n; i++){
        read(h.chang);
        h.id = i;
        while(!q.empty()){
            node t = q.top();
            if(t.chang >= h.chang){
                ans = max(ans, (i - t.id) * t.chang);
                h.id = t.id;
                q.pop();
            }
            else {
                break;
            }
        }
        q.push(h);
    }
    while(!q.empty()){
        ans = max(ans, (n + 1 - q.top().id) * q.top().chang);
        q.pop();
    }
    wrtie(ans);
    return 0;
}

上面我也順便把讀入優化和輸出優化打上了,大家參考參考吧!

單調佇列

什麼是單調佇列

這不是廢話嘛!當然是維護遞減或遞增的佇列啦!

但他和棧不同的是,我們的佇列可以從佇列的頭端出去啊,如果你兩邊都可以進出,就是一個雙向隊列了,用途當然比單調棧要更多的

單調佇列的應用

單調佇列的基本模板

滑動視窗

什麼意思

 

這就是大概的題目意思了

我們上面的單調棧只是一個元素後面比自己大或者小的元素,然而這裡有一個限制,k

怎麼搞!

我們維護的思想是和單調棧一樣的(不知道先看上面的)

而如何來維護連續k個?

如果佇列的頭端的下標和當前要放入的元素下標之間距離大於了K,那麼隊頭就不能要了,所以就要把隊頭彈出去

這裡與單調棧不同的地方也就是這裡了!

#include <cstdio>
#include <list>
#define reg register
using namespace std;
 
struct node
{
    int id, x;
}a[1000005];
int n, k, ans[1000005];
bool flag;
list<node> sheng;
list<node> jiang;
 
inline void read(int &x){
    int f = 1;
    x = 0;
    char s = getchar();
    while(s < '0' || s > '9'){
        if(s == '-'){
            f = -1;
        }
        s = getchar();
    }
    while(s >= '0' && s <= '9'){
        x = (x << 3) + (x << 1) + (s - '0');
        s = getchar();
    }
    x *= f;
}
 
inline void write(int x){
    if(x < 0){
        putchar('-');
        x *= -1;
    }
    if(x > 9){
        write(x / 10);
    }
    putchar((x % 10) + '0');
}
 
int main(){
    read(n);
    read(k);
    for(reg int i = 1; i <= n; i++){
        read(a[i].x);
        a[i].id = i;
        while(!sheng.empty()){
            node t = sheng.front();
            if(i - t.id + 1 > k){
                sheng.pop_front();
            }
            else {
                break;
            }
        }
        while(!sheng.empty()){
            node t = sheng.back();
            if(t.x > a[i].x){
                sheng.pop_back();
            }
            else {
                break;
            }
        }
        sheng.push_back(a[i]);
        while(!jiang.empty()){
            node t = jiang.front();
            if(i - t.id + 1 > k){
                jiang.pop_front();
            }
            else {
                break;
            }
        }
        while(!jiang.empty()){
            node t = jiang.back();
            if(t.x < a[i].x){
                jiang.pop_back();
            }
            else {
                break;
            }
        }
        jiang.push_back(a[i]);
        if(i >= k){
            if(!flag){
                write(sheng.front().x);
                flag = 1;
            }
            else {
                putchar(' ');
                write(sheng.front().x);
            }
            ans[i] = jiang.front().x;
        }
    }
    putchar('\n');
    flag = 0;
    for(reg int i = k; i <= n; i++){
        if(!flag){
            write(ans[i]);
            flag = 1;
        }
        else {
            putchar(' ');
            write(ans[i]);
        }
    }
    return 0;
}

這個也是供大家參考參考的!

單調佇列的重要應用DP

如果我們的單調佇列就是來做這種模板題,也就沒有存在的意義了

那麼這個單調佇列主要是來優化DP的!

例如我們要求連續不超過K的長度的最大子段和

恩,你們都知道,可以用字首和來做對吧?

b[r]-b[l-1]就是l到r子序列的和

b是字首陣列啦,字首和就是1-i的和,比如b[3]=a[1]+a[2]+a[3]

那麼我們輸入的時候就可以預處理了 b[i]=b[i-1]+a[i]

那麼我們要使得長度不超過k,就說明只能減去b[r-k]到b[r-1]的序列了,對不對,因為這樣長度才能是小與等於k的

我們又要使得子序列最大,也就是在b[r-k]到b[r-1]中找到一個最小的嘛

我們直接用單調佇列來優化一下,直接在o(1)的時間複雜度找到最小值,避免了用迴圈來浪費的時間

由於這個還是要大家去自己思考思考的,我就不上程式碼了!

比如我們再來一個例子吧

dp[i][j][k]=max(dp[i][j][k],dp[i][j][j-k]+a[k]);

我們就假設有一道題的狀態轉移方程式上面這樣的,我們要最大,幹嘛不直接知道dp[i][j][j-k]+a[k]中最大的呢,直接和dp[i][j][k]來比較啊,還要用迴圈去找最大值,太浪費時間了,我們直接用單調佇列將dp[i][j][j-k]+a[k]的所有元素維護遞減性,那麼頭元素就一定是最大的了,不用迴圈,直接找到,使得o(n^3)的時間複雜度變為了o(n^2)次方

哇!如此厲害,就是這樣的,有可能大家還不是很理解,只要大家多做幾道題,肯定會明白其中的奧祕的!