單調棧 單調佇列詳解

2022-03-24 16:23:45 字數 4412 閱讀 3846

首先看乙個問題。

給定乙個數列,從左至右輸出每個長度為\(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#define reg register

const int max=7000001;

int n,k,a[max];

int min[max],max[max];

struct nodev[max];

void get_min()

for(reg int i=k-1;i=a[i]) --tail;

v[++tail].x=a[i], v[tail].id=i;

while(v[head].id關於單調棧的一道題目

問題描述

地上從左到右豎立著 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()

stack.push(temp);

}//現在棧中的木板右側沒有比它高的木板,用最右側無限高的木板減

while(!stack.empty())

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

單調棧,單調佇列

大多數借鑑了 單調佇列是什麼呢?可以直接從問題開始來展開。poj 2823 給定乙個數列,從左至右輸出每個長度為m的數列段內的最小數和最大數。數列長度 n 106,m n 我們知道,解法 在暴力列舉的過程中,有乙個地方是重複比較了,就是在找當前的f i 的時候,i的前面其它m 1個數在算f i 1 ...

單調棧 單調佇列

單調棧 單調佇列是在棧和佇列的基礎上加上單調結構的資料結構。如果乙個元素入棧或入隊,他會檢查之前的元素,如果之前的元素不可能是答案的解,那麼就彈出元素,使得當前元素入棧或入隊。leetcode 239 滑動視窗最大值 此題是單調佇列,每次遇到乙個元素,一直從隊尾彈出,直到隊尾元素大於該元素為止。還需...

單調棧 單調佇列

啊學完了來寫個總結吧 顧名思義,單調,就是指色彩單一某乙個容器裡面的元素都是遞增或遞減的,而單調棧和單調佇列就是這個容器。單調棧 單調棧模板 其他的我就不說了,講下為什麼單調棧是從後往前掃瞄 當我們在判斷乙個數後面第乙個比它大的數時,前提是後面的數已經被處理了,所以我們要從後往前掃瞄。我做了兩種做法...