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 排版


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM