演算法學習筆記 16 單調棧與單調佇列

2021-10-18 22:56:28 字數 3911 閱讀 2683

單調棧和單調佇列都是一類演算法思想,是棧和佇列在演算法裡很常見的應用。

單調棧演算法一般用模擬棧或者程式語言給實現的棧都可以,因為只要求在棧頂入棧和退棧,以及取棧頂元素。

單調佇列演算法很多時候用手寫的模擬佇列比較方便,因為很多時候需要雙口出隊的佇列,主要是在隊尾也有刪除元素的需求。而模擬佇列都是游標移動來限定佇列中的所有元素,所以用模擬佇列很自然的可以做到雙端佇列的操作。

單調棧是棧中的元素按照乙個單調性來排列,每次入棧乙個元素x

xx時,如果加入這個元素會使得單調性被破壞,就不停的彈出棧頂元素,直到可以加入x

xx而不破壞單調性(極端情況下就是把棧裡的所有元素都彈出,空棧放個x

xx就一定是單調的了)。

單調棧乙個最常見的應用就是在給定的序列上求每乙個數左邊(或者右邊)離它最近的比它小(或者大)的數。

例如,要求每個數左邊離它最近的比它小的數,那麼可以考慮在從左到右遍歷的過程中維護乙個從棧底到棧頂元素值單調增加的棧(下圖中藍色部分)。每當遍歷到乙個新元素(下圖中綠色部分),都從棧頂不斷彈出比它大的元素(圖中紅框框起來的兩個元素),那麼留下的棧頂就是要求的左側第乙個比它小的元素值,然後再把這個新元素入棧即可。

試想為什麼≥

\geq

≥新元素的都可以淘汰掉,因為要求的是對每個元素求左側第乙個比它小的數,那麼既然有圖上綠色的元素a[i

]a[i]

a[i]

要加到棧中,棧裡面的元素又都是在a[i

]a[i]

a[i]

的左邊,然後還比a[i

]a[i]

a[i]

大或者一樣大,就不可能代替a[i

]a[i]

a[i]

成為可能的解。

因為如果在後面遍歷求解的過程中,紅框框起來的元素是滿足「比某個數小」這個要求的,那麼綠色的更小的a[i

]a[i]

a[i]

也一定是滿足這個要求的,然後a[i

]a[i]

a[i]

還離得更近,就不可能輪到紅框框起來的這兩個元素,所以不可能作為解的就可以直接剔除掉了。

模板題:單調棧。

這題是求左側最近的比它小的數,屬於「左側第乙個」這類問題,因為讀入資料的時候是從左往右讀的,單調棧解決這個問題也是從左往右,所以其實可以邊讀邊解決。這裡是為了理解的清楚才存到陣列a裡,實際上是可以不用存的。

#include

using

namespace std;

const

int n =

1e5+10;

// 存序列

int a[n]

;// 模擬棧:模擬棧陣列、棧頂指標(在陣列中呈現位置是尾巴,所以用tail的縮寫了)

int stk[n]

, tt;

intmain()

cout << endl;

return0;

}

單調佇列的應用更廣泛,乙個經典應用就是求滑動視窗裡的最大(或者最小)值。

試想求滑動視窗最大值的問題,視窗其實就是乙個容量固定好的佇列,在從左到右滑動的過程中,從右側吸入元素(所以右邊是隊尾),從左側吐出元素(所以左邊是隊頭)。

按照第3部分總結的思想來思考這個問題。

試想來了乙個元素v

vv之後,滑動視窗裡哪些值是永遠不可能成為答案的了?因為越靠左的越老,來了的這個v

vv是最新的,所以目前的這些元素中,它能在視窗裡活的最久,如果視窗裡還有≤

v\leq v

≤v的元素,那就是「又老又差」的元素,是沒法熬到v

vv死掉而自己還沒死掉從而成為最大值的,所以就沒有機會作為答案,因此≤

v\leq v

≤v的元素要全部刪除掉。

知道了這件事,即使不繼續去思考這個單調佇列的單調性是什麼樣的,也能寫出正確的**了,因為只需要在入隊之前,從隊尾把所有≤

v\leq v

≤v的都刪掉就行了。稍微思考一下就知道,因為≤

v\leq v

≤v的都刪掉了,所以左邊佇列裡剩下的都

v> v

>

v的,每次遇到新元素v

vv都如此,所以這個單調佇列從左到右是單調遞減的。

然後,滑動視窗裡的最大值顯然就是圖上剩餘藍色的第乙個元素,也就是單調佇列隊頭的元素。

模板題:滑動視窗。

總結起來有四步:

把該滑出的滑出(由於每次移動一步,最多也就滑出乙個,所以乙個if就行了,不需要用while

在入隊前,看看隊尾元素和新元素是不是破壞的單調性(也可以從「又老又差」這個角度去思考),不斷從隊尾刪除(這裡要用while了,因為可能刪多個)

新元素入隊

如果視窗已經達到k

kk這麼大了才需要輸出結果(滑動視窗最值,即單調佇列隊頭)

這裡解釋一下第四步,是因為滑動視窗的大小是固定的k

kk,所以第乙個視窗的形成其實不是一開始就能形成的,因為一開始只加了乙個元素進來,要加夠k

kk個元素才能形成視窗:

所以要i≥k

−1i \geq k - 1

i≥k−

1的時候(k

kk是視窗大小)才能形成視窗,只有在這些時候才要把滑動視窗的最值(即單調佇列的隊頭表示的值)輸出。

另外,由於單調佇列的優化,導致在已經形成視窗的時候,遍歷到i

ii的時候隊頭也不一定是需要滑出去的,所以這裡在單調佇列裡存的是元素在陣列中的下標而不是值,如果下標已經比視窗裡實際第乙個元素的下標i−k

−1i - k - 1

i−k−

1還要小,那麼就說明該把它滑出去了。

#include

using

namespace std;

const

int n =

1e6+10;

// 存序列每個數的值

int a[n]

;// 模擬佇列:模擬佇列陣列、隊頭指標、隊尾指標

int q[n]

, hh, tt =-1

;int

main()

cout << endl;

// 重置佇列

hh =

0, tt =-1

;// 同理計算滑動視窗最大值

for(

int i =

0; i < n; i ++

) cout << endl;

return0;

}

不管是單調棧還是單調佇列,優化的地方都是從元素進入的地方刪除,因為元素要放置的地方才是單調性可能被破壞的地方。

如果不清楚單調性應該設定成什麼樣的,那麼可以針對問題思考一下,來了乙個元素之後,棧(或者佇列)裡哪些元素是永遠不可能成為解的了?這些元素有什麼特徵?

如果知道了單調性是怎樣的,但是不知道和入隊位置元素的判斷條件該怎麼寫,只要遵守乙個原則:會因為來了新元素,而破壞單調性的舊元素要通通刪除。

在單調棧問題裡,入棧前的棧頂元素是所求的答案;在單調佇列的滑動視窗應用裡,入隊後的隊頭元素是所求的答案。不過還要注意這裡存的直接就是答案,還是答案的索引(特別是滑動視窗問題,需要用索引來判斷該不該滑出去了),然後要做必要的轉換。

學習筆記 單調佇列與單調棧

乙個具有單調性的棧。插入乙個元素時,如果直接插入不滿足單調性,就一直彈出,直到插入後滿足單調為止。luogu p1886 loj p10175 meaning of the problem 給你乙個數列 a 多組長度為 k 的區間的最大值與最小值。solution 一道非常經典的單調佇列題。以最大值...

單調棧演算法筆記

定義 單調棧就是棧內元素遞增或者單調遞減的棧,並且只能在棧頂操作。單調棧的維護是o n 的時間複雜度,所有元素只會進進棧一次 性質 單調棧裡面的元素具有單調性 元素加入棧前會把棧頂破壞單調性的元素刪除 使用單調棧可以找到元素向左遍歷的第乙個比他小的元素 單增棧 也可以找到元素向左遍歷第乙個比他大的元...

演算法之單調棧與單調佇列

單調佇列顧名思義就是具有單一單調性的佇列。給定乙個數列,從左至右輸出每個長度為m的數列段內的最小數和最大數。數列長度 n 106,m n 數列為 6 4 10 10 8 6 4 2 12 14,求長度為3的數列段內的最大數,使用單調遞減棧。1 6,0 入隊,此時隊列為 6,0 2 4,1 入隊,此時...