話說博主在寫 Max Chunks To Make Sorted II 這篇帖子的解法四時,寫到使用單調棧Monotone Stack的解法時,突然腦中觸電一般,想起了之前曾經在此貼 LeetCode All in One 題目講解匯總(持續更新中...) 的留言區中說要寫單調棧的總結帖,當時答應了要寫,就去 LeetCode 上看標記為 Stack 的題,可是發現有好多題,而且很多用的不是單調棧,於是博主一個一個的看了起來,但是無奈太多了,一直沒有時間全部看完,就一直沒有動筆寫。雖說時間就像那啥,擠擠總會有的,但是這不一個恍惚,半年就過去了,如果博主再不開始寫,等回過神來,絕對又是半年。於是,博主決定改變策略,不去看所有題的,而是好壞不多想,直接動筆先寫個大概,留到以后慢慢補充完整吧。
好,廢話不多說,來說單調棧吧。所謂的單調棧 Monotone Stack,就是棧內元素都是單調遞增或者單調遞減的,有時候需要嚴格的單調遞增或遞減,根據題目的具體情況來看吧。關於單調棧,這個帖子講的不錯,而且舉了個排隊的例子來類比。那么,博主也舉個生動的例子來說明吧:比如有一天,某家店在發 free food,很多人在排隊,於是你也趕過去湊熱鬧。但是由於來晚了,隊伍已經很長了,想着不然就插個隊啥的。但發現排在隊伍最前面的都是一些有紋身的大佬,惹不起,只能贊美道,小豬佩奇身上紋,來世還做社會人。於是往隊伍后面走,發現是一群小屁孩,直接全部攆走,然后排在了社會大佬們的后面。那么這就是一個單調遞減的棧,按實力遞減。由於棧元素是后進先出的,所以上面的例子正確的檢查順序應該是從隊尾往前遍歷,小屁孩都攆走,直到遇到大佬停止,然后排在大佬后面(假設這個隊列已經事先按實力遞減排好了)。
明白了單調棧的加入元素的過程后,我們來看看它的性質,以及為啥要用單調棧。單調棧的一大優勢就是線性的時間復雜度,所有的元素只會進棧一次,而且一旦出棧后就不會再進來了。
單調遞增棧可以找到左起第一個比當前數字小的元素。比如數組 [2 1 4 6 5],剛開始2入棧,數字1入棧的時候,發現棧頂元素2比較大,將2移出棧,此時1入棧。那么2和1都沒左起比自身小的數字。然后數字4入棧的時候,棧頂元素1小於4,於是1就是4左起第一個小的數字。此時棧里有1和4,然后數字6入棧的時候,棧頂元素4小於6,於是4就是6左起第一個小的數字。此時棧里有1,4,6,然后數字5入棧的時候,棧頂元素6大於5,將6移除,此時新的棧頂元素4小於5,那么4就是5左起的第一個小的數字,最終棧內數字為 1,4,5。
單調遞減棧可以找到左起第一個比當前數字大的元素。這里就不舉例說明了,同樣的道理,大家可以自行驗證一下。
性質搞懂了后,下面來看一下應用,什么樣的場景下適合使用單調棧呢?可以看下 Max Chunks To Make Sorted II 這篇帖子的解法四,但這道題並不是單調棧的最典型應用,只能說能想到用單調棧確實牛b,但一般情況下是不容易想到的。我們來看一些特別適合用單調棧來做的題目吧。
首推 Trapping Rain Water 這道題,雖然博主開始也沒有注意到可以使用單調棧來做。但實際上是一道相當合適的題,來復習一下題目:
For example,
Given [0,1,0,2,1,0,1,3,2,1,2,1]
, return 6
.
給了邊界的高度(黑色部分),讓求能裝的水量(藍色部分)。 為啥能用單調棧來做呢?我們先來考慮一下,什么情況下可以裝下水呢,是不是必須兩邊高,中間低呢?我們對低窪的地方感興趣,就可以使用一個單調遞減棧,將遞減的邊界存進去,一旦發現當前的數字大於棧頂元素了,那么就有可能會有能裝水的地方產生。此時我們當前的數字是右邊界,我們從棧中至少需要有兩個數字,才能形成一個坑槽,先取出的那個最小的數字,就是坑槽的最低點,再次取出的數字就是左邊界,我們比較左右邊界,取其中較小的值為裝水的邊界,然后此高度減去水槽最低點的高度,乘以左右邊界間的距離就是裝水量了。由於需要知道左右邊界的位置,所以我們雖然維護的是遞減棧,但是棧中數字並不是存遞減的高度,而是遞減的高度的坐標。這應該屬於單調棧的高級應用了,可能並不是那么直接就能想出正確的解法。
再來看一道 Largest Rectangle in Histogram,這道求直方圖中的最大矩陣的題,也是非常適合用單調棧來做的,來復習一下題目:
For example,
Given height = [2,1,5,6,2,3]
,
return 10
.
我們可以看到,直方圖矩形面積要最大的話,需要盡可能的使得連續的矩形多,並且最低一塊的高度要高。有點像木桶原理一樣,總是最低的那塊板子決定桶的裝水量。那么既然需要用單調棧來做,首先要考慮到底用遞增棧,還是用遞減棧來做。我們想啊,遞增棧是維護遞增的順序,當遇到小於棧頂元素的數就開始處理,而遞減棧正好相反,維護遞減的順序,當遇到大於棧頂元素的數開始處理。那么根據這道題的特點,我們需要按從高板子到低板子的順序處理,先處理最高的板子,寬度為1,然后再處理旁邊矮一些的板子,此時長度為2,因為之前的高板子可組成矮板子的矩形 ,因此我們需要一個遞增棧,當遇到大的數字直接進棧,而當遇到小於棧頂元素的數字時,就要取出棧頂元素進行處理了,那取出的順序就是從高板子到矮板子了,於是乎遇到的較小的數字只是一個觸發,表示現在需要開始計算矩形面積了,為了使得最后一塊板子也被處理,這里用了個小trick,在高度數組最后面加上一個0,這樣原先的最后一個板子也可以被處理了。由於棧頂元素是矩形的高度,那么關鍵就是求出來寬度,那么跟之前那道 Trapping Rain Water 一樣,單調棧中不能放高度,而是需要放坐標。由於我們先取出棧中最高的板子,那么就可以先算出長度為1的矩形面積了,然后再取下一個板子,此時根據矮板子的高度算長度為2的矩形面積,以此類推,直到數字大於棧頂元素為止,再次進棧,巧妙的一比!
初步來總結一下單調棧吧,單調棧其實是一個看似原理簡單,但是可以變得很難的解法。線性的時間復雜度是其最大的優勢,每個數字只進棧並處理一次,而解決問題的核心就在處理這塊,當前數字如果破壞了單調性,就會觸發處理棧頂元素的操作,而觸發數字有時候是解決問題的一部分,比如在 Trapping Rain Water 中作為右邊界。有時候僅僅觸發作用,比如在 Largest Rectangle in Histogram 中是為了開始處理棧頂元素,如果僅作為觸發,可能還需要在數組末尾增加了一個專門用於觸發的數字。另外需要注意的是,雖然是遞增或遞減棧,但里面實際存的數字並不一定是遞增或遞減的,因為我們可以存坐標,而這些坐標帶入數組中才會得到遞增或遞減的數。所以對於玩數組的題,如果相互之間關聯很大,那么就可以考慮考慮單調棧能否解題。
應用實例:
Largest Rectangle in Histogram
相關帖子:
LeetCode Binary Search Summary 二分搜索法小結
參考資料:
https://zhuanlan.zhihu.com/p/26465701
https://chuckliu.me/#!/posts/585a2cb4f33c18149026f0be
https://blog.csdn.net/liujian20150808/article/details/50752861