單調棧&單調隊列入門

單調隊列是什麼呢?能夠直接從問題開始來展開。
Poj 2823
給定一個數列,從左至右輸出每一個長度爲m的數列段內的最小數和最大數。
數列長度:\(N <=10^6 ,m<=N\)node

解法①

很直觀的一種解法,那就是從數列的開頭,將窗放上去,而後找到這最開始的k個數的最大值,而後窗最後移一個單元,繼續找到k個數中的最大值。
Alt text
這種方法每求一個f(i),都要進行k-1次的比較,複雜度爲$ O(Nk) $。
顯然,若是暴力時間複雜度爲 $ O(Nm) $ 不超時就怪了。算法

解法②

還有一種想法是維護一個BST,而後for循環從左到右,依次加入到BST裏面,若是某個數超出了k的範圍,就從BST中刪除。
由於每一個數只insert一次,最多erase一次,因此複雜度是\(O(NlogN)\)的,已經很不錯了。
可是\(10^6\)級別的極限數據,這種作法會被卡掉的,何況維護一個BST的代碼也比較麻煩。數組

void getans() {
    BST tree;

    for(int i=1,j=1;i<=N;++i) {
        tree.insert(a[i]);
        while(j<=i-k) {
            tree.erase(a[j]);
            --j;
        }
        cout<<tree.max()<<endl;
    }
}

解法③

咱們知道,解法①在暴力枚舉的過程當中,有一個地方是重複比較了,就是在找當前的f(i)的時候,i的前面其它m-1個數在算f(i-1)的時候咱們就比較過了。
當你一個個往下找時,每一次都是少一個而後多一個,若是少的不是最大值,而後再問新加進來的,看起來很省時間對吧,那麼若是少了的是最大值呢?第二個最大值是什麼??
那麼咱們能不能保存上一次的結果呢?固然主要是i的前k-1個數中的最大值了。答案是能夠,這就要用到單調隊列。
對於單調隊列,咱們這樣子來定義: 數據結構

  • 一、維護區間最值
  • 二、去除冗雜狀態 如上題,區間中的兩個元素a[i],a[j](假設如今再求最大值)
    若 j>i且a[j]>=a[i] ,a[j]比a[i]還大並且還在後面(目前a[j]留在隊列確定比a[i]有用,由於你是日後推, 核心思想 !!!)
  • 三、保持隊列單調,最大值是單調遞減序列,最小值反之
  • 四、最優選擇在隊首

單調隊列實現的大體過程:
一、維護隊首(對於上題就是若是隊首已是當前元素的m個以前,則隊首就應該被刪了,head++)
二、在隊尾插入(每插入一個就要從隊尾開始往前去除冗雜狀態,保持單調性)spa

簡單舉例應用
數列爲:6 4 10 10 8 6 4 2 12 14
N=10,K=3;
那麼咱們構造一個長度爲3的單調遞減隊列:
首先,那6和它的位置0放入隊列中,咱們用(6,0)表示,每一步插入元素時隊列中的元素以下
插入6:(6,0);
插入4:(6,0),(4,1);
插入10:(10,2);
插入第二個10,保留後面那個:(10,3);
插入8:(10,3),(8,4);
插入6:(10,3),(8,4),(6,5);
插入4,以前的10已經超出範圍因此排掉:(8,4),(6,5),(4,6);
插入2,同理:(6,5),(4,6),(2,7);
插入12:(12,8);
插入14:(14,9);
那麼f(i)就是第i步時隊列當中的首元素:6,6,10,10,10,10,8,6,12,14
同理,最小值也能夠用單調隊列來作。調試

單調隊列的時間複雜度是O(N),由於每一個數只會進隊和出隊一次,因此這個算法的效率仍是很高的。
注意:建議直接用數組模擬單調隊列,由於系統自帶容器不方便並且不易調試,同時,每一個數只會進去一次,因此,數組絕對不會爆,空間也是S(N),優於堆或線段樹等數據結構。code

更重要的:單調是一種思想,當咱們解決問題的時候發現有許多冗雜無用的狀態時,咱們能夠採用單調思想,用單調隊列或相似於單調隊列的方法去除冗雜狀態,保存咱們想要的狀態,blog

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
struct node
{
    int x,y;
}v[1010000]; //x表示值,y表示位置 能夠理解爲下標
int a[1010000],n,m,mx[1010000],mn[1010000];
void getmin()
{
    int i,head=1,tail=0;// 默認起始位置爲1 由於插入是v[++tail]故初始化爲0
    for(i=1;i<m;i++)
    {
        while(head<=tail && v[tail].x>=a[i]) tail--;
        v[++tail].x=a[i],v[tail].y=i;
                // 根據題目 前m-1個先直接進入隊列
    }
    for(;i<=n;i++)
    {
        while(head<=tail && v[tail].x>=a[i]) tail--;
        v[++tail].x=a[i],v[tail].y=i;
        while(v[head].y<i-m+1) head++;
        mn[i-m+1]=v[head].x;
               // 道理同上,固然了 要把已經超出範圍的從head開始排出
               //  而後每一個隊首則是目前m個數的最小值
    }
}
void getmax() //最大值同最小值的道理,只不過是維護的是遞減隊列
{
    int i,head=1,tail=0;
    for(i=1;i<m;i++)
    {
        while(head<=tail && v[tail].x<=a[i]) tail--;
        v[++tail].x=a[i],v[tail].y=i;
    }
    for(;i<=n;i++)
    {
        while(head<=tail && v[tail].x<=a[i]) tail--;
        v[++tail].x=a[i],v[tail].y=i;
        while(v[head].y<i-m+1) head++;
        mx[i-m+1]=v[head].x;
    }
}
int main()
{
    int i,j;
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;i++)scanf("%d",&a[i]);
    getmin();
    getmax();
    for(i=1;i<=n-m+1;i++)
    {
        if(i==1)printf("%d",mn[i]);
        else printf(" %d",mn[i]);
    }
    printf("\n");
    for(i=1;i<=n-m+1;i++)
    {
        if(i==1)printf("%d",mx[i]);
        else printf(" %d",mx[i]);
    }
    printf("\n");
    return 0;
}

這就是單調隊列,單調棧和單調隊列區別不大,都是每次push的時候在棧頂要維護單調性。隊列

關於單調棧的一道題目

問題描述
地上從左到右豎立着 n 塊木板,從 1 到 n 依次編號,以下圖所示。咱們知道每塊木板的高度,在第 n 塊木板右側豎立着一塊高度無限大的木板,現對每塊木板依次作以下的操做:對於第 i 塊木板,咱們從其右側開始倒水,直到水的高度等於第 i 塊木板的高度,倒入的水會淹沒 ai 塊木板(若是木板左右兩側水的高度大於等於木板高度即視爲木板被淹沒),求 n 次操做後,全部 ai 的和是多少。如圖上所示,在第 4 塊木板右側倒水,能夠淹沒第 5 塊和第 6 塊一共 2 塊木板,a4 = 2。
ci

解法①

暴力求解,複雜度是O(n²)
例如如今存在5塊木板
每塊木板從左至右高分別爲
10,5,8,12,6
從第一塊木板(高度爲10)右側開始倒水,當水到達第四塊木板(高度爲12)時,能夠淹沒第一塊木板
即第一塊木板至第四塊木板之間的木板數量,即4-1-1 = 2,a1 = 2;
也就是說:尋找在第 i 個木板右邊第一個比它大的木板j,ai 就等於木板 i 和木板 j 之間的木板數
同理獲得
a2=0
a3=0
a4=1
a5=0
sum = a1 + a2 +a3 +a4 +a5 = 3
因而,問題就變成了尋找在第 i 個數右邊第一個比它大的數。能夠暴力求解,從 1 循環到 n,對每塊木板再往右循環一遍,這樣的時間複雜度是\(O(n²)\)

解法②

單調棧來求解的話,複雜度是O(n)
結合單調棧的性質:使用單調棧能夠找到元素向左遍歷第一個比他小的元素,也能夠找到元素向左遍歷第一個比他大的元素。
顧名思義,單調棧就是棧內元素單調遞增或者單調遞減的棧,這一點和單調隊列很類似,可是單調棧只能在棧頂操做。
單調棧有如下兩個性質:
一、如果單調遞增棧,則從棧頂到棧底的元素是嚴格遞增的。如果單調遞減棧,則從棧頂到棧底的元素是嚴格遞減的。
二、越靠近棧頂的元素越後進棧。
單調棧與單調隊列不一樣的地方在於棧只能在棧頂操做,所以通常在應用單調棧的地方不限定棧的大小,不然可能會形成元素沒法進棧。
元素進棧過程:對於單調遞增棧,若當前進棧元素爲e,從棧頂開始遍歷元素,把小於e或者等於e的元素彈出棧,直接遇到一個大於e的元素或者棧爲空爲止,而後再把e壓入棧中。對於單調遞減棧,則每次彈出的是大於e或者等於e的元素。

數據模擬木板倒水單調棧的入棧計算過程
思路:尋找比棧頂高的木板i,找到就出棧,不是就把木板i入棧,給出循環計數樣例 10,5,8,12,6
從左往右掃描
棧爲空,10入棧 棧:10 此時棧頂是10,也就是說要尋找比10大的木板
5比10小,5入棧 棧:5,10 此時棧頂是5,也就是說要尋找比5大的木板
8比5大,5出棧 棧:10
這個時候,第二個高度爲5的木板右邊比它高的木板已經找到了,是第三個木板8,因此5出棧,計算a2 = 3-2-1 = 0
8比10小,8入棧 棧:8,10 此時棧頂是8,也就是說要尋找比8大的木板
12比8大,8出棧 棧:10
第三個高度爲8的木板右邊比它高的木板已經找到了,是第四個木板12,8出棧,計算a3 = 4-3-1 = 0
12比10大,10出棧 棧:空
第一個高度爲10的木板右邊比它高的木板已經找到了,是第四個木板12,因此10出棧,計算a1 = 4-1-1 = 2
棧爲空,12入棧 棧:12 此時棧頂是12,也就是說要尋找比12大的木板
6比12小,6入棧 棧:6,12 此時棧頂是6,也就是說要尋找比6大的木板
掃描完成結束
最後棧的結構是:6,12 棧頂爲6
因爲最右端豎立着一塊高度無限大的木板,即存在第六塊木板高度爲無窮,因此剩餘兩塊木板的算法以下 a5 = 6-5-1 =0
a4 = 6-4-1 = 1
sum = a1 + a2 +a3 +a4 +a5 = 3
所以本題能夠在\(O(n)\)的時間內迎刃而解了。
從左往右將木板節點壓棧,遇到比棧頂木板高的木板就將當前棧頂木板出棧並計算淹沒的木板數,如此循環直到棧頂木板高度比當前木板高或者棧爲空,而後將此木板壓棧。木板全都壓棧完成後,棧內剩餘的木板都是右側沒有比它們更高的木板的,因此一個個出棧並計算ai=n+1-temp_id-1(用最右邊無限高的木板減)

//從左往右解木板倒水
int main() {
    int n,ans=0;
    cin>>n;
    Stack<Node> stack(n);
    Node temp;
    for(int i=1;i<=n;i++){
        cin>>temp.height;
        temp.id=i;
        //遇到了右側第一個比棧頂元素大的元素,計算並出棧
        while(!stack.empty()&&stack.top().height<=temp.height){
            ans=ans+i-stack.top().id-1;
            stack.pop();
        }
        stack.push(temp);
    }
    //如今棧中的木板右側沒有比它高的木板,用最右側無限高的木板減
    while(!stack.empty()){
        ans=ans+n+1-stack.top().id-1;
        stack.pop();
    }
    cout<<ans<<endl;
    return 0;
}
相關文章
相關標籤/搜索