話說博主在寫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中是為了開始處理棧頂元素,如果僅作為觸發,可能還需要在陣列末尾增加了一個專門用於觸發的數字。另外需要注意的是,雖然是遞增或遞減棧,但裡面實際存的數字並不一定是遞增或遞減的,因為我們可以存座標,而這些座標帶入陣列中才會得到遞增或遞減的數。所以對於玩陣列的題,如果相互之間關聯很大,那麼就可以考慮考慮單調棧能否解題。
相關帖子:
LeetCode Binary Search Summary 二分搜尋法小結
參考資料:
https://zhuanlan.zhihu.com/p/26465701
https://chuckliu.me/#!/posts/585a2cb4f33c18149026f0be
https://blog.csdn.net/liujian20150808/article/details/50752861