單調棧&單調隊列入門

單調隊列比較難理解代碼,所以自己加了點註釋

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
struct node
{
	int x, y;
}v[101]; //x表示值,y表示位置 可以理解爲下標
int a[] = { 0,6 ,4, 10, 10 ,8 ,6, 4, 2, 12 ,14 }, n = 10, m = 3, 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--;//1 此處如果是有比當前更小的數--
		v[++tail].x = a[i], v[tail].y = i;//2  此處++結合註釋1部分,如果是有比當前更小的數,則替換,否走移到下一個位置
		while (v[head].y<i - m + 1) head++;//如果當前頭部的下標有更新,則不用移動頭部,如果當前頭部下標沒更新,並且不在範圍內,則移動頭部++
		mn[i - m + 1] = v[head].x; //把頭部的最大數放入結果數組
		// 道理同上,當然了 要把已經超出範圍的從head開始排出
		//  然後每個隊首則是目前m個數的最小值
	}
}
int main()
{
	int i;
	getmin();
	for (i = 1; i <= n - m + 1; i++)
	{
		 printf(" %d", mn[i]);
	}


	getchar();
	getchar();
	getchar();
	return 0;
}

下面是原文地址:https://www.cnblogs.com/tham/p/8038828.html

單調棧&單調隊列入門

單調隊列是什麼呢?可以直接從問題開始來展開。
Poj 2823
給定一個數列,從左至右輸出每個長度爲m的數列段內的最小數和最大數。
數列長度:N<=106m<=NN<=106,m<=N

解法①

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

解法②

還有一種想法是維護一個BST,然後for循環從左到右,依次加入到BST裏面,如果某個數超出了k的範圍,就從BST中刪除。
因爲每個數只insert一次,最多erase一次,所以複雜度是O(NlogN)O(NlogN)的,已經很不錯了。
但是106106級別的極限數據,這種做法會被卡掉的,況且維護一個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個數中的最大值了。答案是可以,這就要用到單調隊列。
對於單調隊列,我們這樣子來定義:

  • 1、維護區間最值
  • 2、去除冗雜狀態 如上題,區間中的兩個元素a[i],a[j](假設現在再求最大值)
    若 j>i且a[j]>=a[i] ,a[j]比a[i]還大而且還在後面(目前a[j]留在隊列肯定比a[i]有用,因爲你是往後推, 核心思想 !!!)
  • 3、保持隊列單調,最大值是單調遞減序列,最小值反之
  • 4、最優選擇在隊首

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

簡單舉例應用
數列爲: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),優於堆或線段樹等數據結構。

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

#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。

解法①

暴力求解,複雜度是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²) 。

解法②

單調棧來求解的話,複雜度是O(n)
結合單調棧的性質:使用單調棧可以找到元素向左遍歷第一個比他小的元素,也可以找到元素向左遍歷第一個比他大的元素。
顧名思義,單調棧就是棧內元素單調遞增或者單調遞減的棧,這一點和單調隊列很相似,但是單調棧只能在棧頂操作。
單調棧有以下兩個性質:
1、若是單調遞增棧,則從棧頂到棧底的元素是嚴格遞增的。若是單調遞減棧,則從棧頂到棧底的元素是嚴格遞減的。
2、越靠近棧頂的元素越後進棧。
單調棧與單調隊列不同的地方在於棧只能在棧頂操作,因此一般在應用單調棧的地方不限定棧的大小,否則可能會造成元素無法進棧。
元素進棧過程:對於單調遞增棧,若當前進棧元素爲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)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;
}

發佈了44 篇原創文章 · 獲贊 6 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章