1. 程式人生 > >LeetCode 84 | 單調棧解決最大矩形問題

LeetCode 84 | 單調棧解決最大矩形問題

本文始發於個人公眾號:**TechFlow**,原創不易,求個關注

今天是LeetCode專題第52篇文章,我們一起來看LeetCode第84題,Largest Rectangle in Histogram(最大矩形面積)。

這道題的官方難度是Hard,點贊3581,反對只有80,通過率在34.7%左右。從通過率上來看,難度其實還可以,並沒有特別大,但是這道題的點贊比很高,說明題目的質量很好。實際上也的確如此,這題非常經典,我個人也非常推薦。建議大家有能力的都做一下本題,一定會很有收穫。

題意

假設我們有一系列寬度相同都為1的矩形豎直地擺放在一起,請問擺放而成的這個圖案所能圍成的最大矩形的面積是多少?

比如上圖當中,我們有6個矩形,它們的寬度都是1。我們能找到的最大矩形應該是中間5和6圍成的矩形:

題目給定一個含有若干個整數的數字,表示這些矩形的高度,要求返回能找到的面積最大的矩形的面積。

樣例

Input: [2,1,5,6,2,3]
Output: 10

區間求最值

拿到手應該能感受到這題的難度,我們一上來的確沒有什麼太好的思路,題目也比較明確,沒有太多可以分析的入手點。所以我們可以先來思考一下最簡單的解法。

最簡單的解法就是找出能夠圍成的所有矩形,然後比較它們之間的面積,得出其中的最大面積。我們很容易可以想到可以遍歷矩形的起始位置,這樣就得到了矩形的寬。至於矩形的長也很簡單,就是選定的這個區間段裡的最低高度。

我們可以做一個小小的思路轉換,假設這些矩形都是木條,我們是要選出木條來製作木桶。那麼根據木桶效應,木桶圍成的水的高度取決於最短的那根木條,同樣圍成矩形的面積的高取決於這些矩形當中最矮的那個。也就是說,當我們確定了區間之後,我們只需要找到區間裡最小的數就可以了。所以這題就轉化成了區間求最值的問題,比如上圖當中,如果我們選擇最後三個矩形,那麼它的高度就是2。

我們假設一共有n個長條矩形可供選擇,那麼我們可以選出的首尾組合就是 ,大概是n的平方量級個區間。對於每個區間,我們需要遍歷它們中的元素獲取最小值,這需要 的遍歷時間,所以整體的複雜度應該在 量級。顯然這是一個非常大的數量級,當n超過1000就很難計算出解了。

這個思路顯然不夠好,我們想要對它進行優化也不容易。比如說如果你學過線段樹這類的資料結構,可能還會想到使用線段樹,我們可以將每次求最小值的查詢優化到 ,但即便如此最終的複雜度也很高。這是因為我們遍歷區間首尾位置就耗費了 ,而這是很難優化的。所以這個思路的極限已經確定了,我們無法做出大的優化。

從這點出發,如果存在更好的解法,那麼一定不是通過這種方式進行的。

逆向思維

上面的一種思路雖然不太可行,但是它提供了一種正向思路。我們搜尋所有的區間,然後通過區間裡的木條確定區圍成矩形的高度,就得到了矩形的面積。

既然這條路走不通,我們能不能反向思考呢?我們假設我們找到了答案,它是區間[a, b]段的木條圍成的矩形,它的高度是h。那麼根據木桶效應,a到b區間段的木條當中一定有一根的長度是h。比如下圖當中[5, 6, 2, 3]如果要圍成矩形,那麼高度只能是2。

既然如此,我們可以尋找以某根木條為短板所能構成的最大矩形。比如上圖當中,如果我們以第一根木條去尋找,就只能找到它本身,所以這個矩形的面積就是1 x 2 = 2。如果以第二根木條為短板去尋找,可以找到整個區間,它對應的面積就是1 x 6 = 6。

因為我們只有n個木條,以每個木條為短板尋找最大矩形,那麼我們一定可以找出最多n個矩形。最終的答案一定在這n個矩形當中,在正向思維當中我們尋找木條區間需要 的複雜度,然而我們尋找短板,只需要 ,也就是說這種思路的搜尋空間更小,只要我們保證搜尋的效率,就可以更快地找到答案。

為了找到每個木條對應的最大矩形,我們需要找到每個短板向左以及向右能夠延伸到的最遠位置。比如上圖例子當中,根據每個木條向右延伸的最遠位置,我們可以得到[0, 5, 3, 3, 5, 5],同樣,我們可以得到每根木條向左延伸的陣列:[0, 0, 2, 3, 2, 5]。有了這兩個陣列之後,我們就可以計算出以每一根木條為短板的最大矩形的面積,在這其中面積最大的那個就是答案。

這個位置我們可以使用單調棧來求,我們用一個有序的棧來維護延伸的位置。舉個例子,我們用從棧底往棧頂遞增的單調棧來維護每根木條向右延伸的位置。當我們遇到一根新的木條時,會彈出棧中所有比它長的值。對於這些值來說,這根新的木條就是它的右邊界。比如[5, 6, 2],一開始讀到5,入棧。接著讀到6,由於6大於棧頂的5,所以6入棧。最後讀到2,由於2比6小,所以6出棧,對於6來說,2的位置就是它的右側邊界。正是由於2比它小,所以它才需要出棧,也說明了2的左側的元素都比6來的大,否則6在之前就應該出棧了。同理,2也是5的右側邊界。

如果你不瞭解單調棧,可以參考一下之前的文章:

LeetCode42題,單調棧、構造法、two pointers,這道Hard題的解法這麼多?

我們把以上的邏輯翻轉,就得到了左側邊界求解的邏輯。左右邊界有了之後,我們只需要乘上它們之間的區間長度就得到了矩形的面積。

接著,我們來寫出程式碼:

class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        n = len(heights)
  # 左側邊界初始化為0
        left_side = [0 for i in range(n)]
        # 右側邊界初始化為n-1
        right_side = [n-1 for _ in range(n)]
        
        stack_left = []
        stack_right = []
        
        for i in range(n):
            h = heights[i]
            # 彈出棧中所有比當前元素小的值
            # 注意,棧記憶體儲的是下標
            while len(stack_right) > 0 and h < heights[stack_right[-1]]:
                tail = stack_right[-1]
                stack_right.pop()
                right_side[tail] = i - 1
            
            # 當前元素入棧
            stack_right.append(i)
            
            # 把座標翻轉,等價於逆向遍歷
            i_ = n - 1 - i
            h = heights[i_]
            
            # 維護單調棧的邏輯同上
            while len(stack_left) > 0 and h < heights[stack_left[-1]]:
                tail = stack_left[-1]
                stack_left.pop()
                left_side[tail] = i_ + 1
                
            # 當前元素入棧
            stack_left.append(i_)
                

        ret = 0
        for i in range(n):
            # 矩形面積等於右側邊界-左側邊界+1 x 高度
            cur = (right_side[i] - left_side[i] + 1) * heights[i]
            ret = max(ret, cur)
        return ret

總結

想要把這道題做出來,單單理清楚題意和單單會單調棧都是沒有用的。既需要理清楚題意,從最簡單的解法出發推匯出優化的方法,也需要深刻理解單調棧這個資料結構,才可以靈活應用。

另外,在程式碼當中需要特別注意邊界的情況。比如初始化時左右邊界的設定,以及可能會出現連續相等元素的情況,這些都需要納入考慮。程式碼雖然看起來簡單,但是隱藏了很多細節,所以只看程式碼是沒用的,最好還是能親自實現一下。

今天的文章到這裡就結束了,如果喜歡本文的話,請來一波素質三連,給我一點支援吧(關注、轉發、點贊)。

本文使用 mdnice 排版

![](https://user-gold-cdn.xitu.io/2020/7/15/173505919c8c3a74?w=258&h=258&f=png&