單調棧、單調隊列詳解


http://www.cnblogs.com/tham/p/8038828.html

首先看一個問題。

給定一個數列,從左至右輸出每個長度為\(k\)的數列段內的最小數和最大數(第一行輸出每個區間最小值,第二行輸出最大值)。
數列長度:\(N \leq 10^6\)\(k \leq N\)

解法①

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

解法②

核心思想:去除無用的狀態,保存有用的狀態。考慮這樣一個問題:

現在區間中有兩個元素a[i], a[j],且滿足\(i < j,\, a[j]\geq a[i]\)(假設要求區間內最大值)
就是說a[j]比a[i]還大而且還在后面,那么a[j]肯定比a[i]有用(因為區間是往后推,找到的最大值有可能是a[i],但不可能是a[j])。因此a[i]對於求這段區間的最值是沒用的了。
現在我們維護一個單調遞減隊列,並從隊尾入隊、隊頭出隊(隊內元素序號遞增)。每次入隊a[j]時,從隊尾往前找到第一個大於a[j]的元素,並把a[j]插入到它后面。也就是說小於a[j]的元素不要了,因為它們對求出區間內的最大值來說是無用的。隨着區間后移,再把隊頭超出區間范圍的元素彈出。

單調隊列實現的大致過程:

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,3)已經超出范圍所以排掉:(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>
#define reg register
const int MAX=7000001;
int n,k,a[MAX];
int Min[MAX],Max[MAX];
struct node{
    int x,id; //x:值 id:位置(原序列中的下標)
}v[MAX];
void get_min(){
	int head=1,tail=0; //默認起始位置為1 因為插入是v[++tail]故初始化為0
	for(reg int i=0;i<k-1;++i){ //根據題目 前m-1個先直接進入隊列
		while(head<=tail && v[tail].x>=a[i]) --tail;
	    v[++tail].x=a[i], v[tail].id=i;
	}
	for(reg int i=k-1;i<n;++i){
		while(head<=tail && v[tail].x>=a[i]) --tail;
		v[++tail].x=a[i], v[tail].id=i;
		while(v[head].id<i-k+1) ++head;
		Min[i-k+1]=v[head].x;
		//道理同上,當然了,要把已經超出范圍的從head開始排出
        //然后每個隊首則是目前k個數的最小值

	}
}
void get_max(){ //最大值同最小值的道理,只不過是維護的是遞減隊列
	int head=1,tail=0;
	for(reg int i=0;i<k-1;++i){
		while(head<=tail && v[tail].x<=a[i]) --tail;
		v[++tail].x=a[i], v[tail].id=i;
	}
	for(reg int i=k-1;i<n;++i){
		while(head<=tail && v[tail].x<=a[i]) --tail;
		v[++tail].x=a[i], v[tail].id=i;
		while(v[head].id<i-k+1) ++head;
		Max[i-k+1]=v[head].x;
	}
}
void output(){
    printf("%d",Min[0]);
	for(reg int i=1;i<n-k+1;++i) printf(" %d",Min[i]);
	printf("\n%d",Max[0]);
	for(reg int i=1;i<n-k+1;++i) printf(" %d",Max[i]);
	printf("\n");
}
int main(){
	scanf("%d%d",&n,&k);
	for(reg int i=0;i<n;++i) scanf("%d",&a[i]);
	get_min(); get_max(); output();
	return 0;
}

關於單調棧的一道題目

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

解法①

暴力求解,復雜度是\(O(n^2)\)
例如現在存在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^2)\)

解法②

單調棧來求解的話,復雜度是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)\)的時間內迎刃而解了。
從左往右將木板節點壓棧,遇到比棧頂木板高的木板就將當前棧頂木板出棧並計算淹沒的木板數,如此循環直到棧頂木板高度比當前木板高或者棧為空,然后將此木板壓棧。木板全都壓棧完成后,棧內剩余的木板都是右側沒有比它們更高的木板的,所以一個個出棧並計算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;
}

也可設a[n+1]=INF,可以省去最后出隊的while。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM