LeetCode 笔记系列 18 Maximal Rectangle [学以致用](最大矩形)
? leetcode之Largest Rectangle in Histogram
標簽:?leetcode面試題最大矩形堆棧單調隊列 2016-07-30 13:47?1325人閱讀?評論(0)?收藏?舉報 ?分類: leetcode(7)? ?數據結構與算法(29)?
版權聲明:本文為博主原創文章,未經博主允許不得轉載。
目錄(?)[+]
問題來源:Largest Rectangle in Histogram?
問題描述:給定一個長度為n的直方圖,我們可以在直方圖高低不同的長方形之間畫一個更大的長方形,求該長方形的最大面積。例如,給定下述直方圖,?
?
我們可以以高度5寬度2畫一個更大的長方形,如下圖,該長方形即是面積最大的長方形。該問題是難度比較大的問題,但是很出名,經常作為面試題出現。最近陳利人老師給出該問題的一個O(n)解法,非常巧妙,并從二維三維角度對問題進行了擴展。我們在陳老師的基礎上,對該問題進行深入分析,給出多種方法,拓展大家的視野。?
1. 解法一
如果在面試的過程中被問到這個題目,除非之前見過,否則一時很難想到解法。我們不妨從最笨的解法入手。在我看來,能把最笨的解法寫出來也是很不錯的,畢竟很多人見到這種題一下就蒙了。?
最笨的解法是什么呢,就是遍歷所有的起始和結束位置,然后計算該區域內長方形的面積,找到最大的。具體實現過程中,從第0個位置開始遍歷起點,選定起點之后,從起點到末尾都可以當做終點,所以總共有O(n2)種可能。在固定了起點之后,后續對終點的遍歷有一個特點:構成長方形的高度都不高于之前所構造長方形的高度,所以長方形的高度即是到當前終點為止的最小值,寬度即是終點位置減去起點位置加1。按照這個思路實現的C++代碼如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
上述代碼可以通過幾乎所有的測試用例,但是當測試用例為0到19999的連續整數時會超時。說明O(n2)的復雜度還是過高,需要進一步優化。下面我們看一下陳利人老師的解法,原文在《很神奇的解法!怎么求柱狀圖中的最大矩形?》。?
新解法的核心在于考慮了直方圖兩個相鄰長方形AB之間的關系。如果前一個長方形A低后一個長方形B高,則A肯定不會是某個大長方形的終點,因為我們可以安全地在A后面添加更高的B,使大長方形的寬度加1。如果A高B低,則A是可能的終點,假設我們就用A當做終點,并且以該長方形的高度當做大長方形的高度,看看可以往前延伸多長。根據上面這兩條性質,我們可以維護一個遞增序列(實際為非遞減,當前后兩個長方形的高度一樣時,前一個長方形同樣也不可能是終點,在此為了解釋方便假定前后高度都不一樣),當B高時就將B的位置添加到序列中,否則就彈出A的位置,并用A的位置作為終點,A的高度作為大長方形的高度計算面積。起點怎么確定呢,由于我們維護的是一個遞增序列,在彈出A之后,序列中A的前一個位置所對應的長方形高度肯定低于A的高度,所以A的前一個長方形的位置加1即是大長方形的起點。因為我們每次都是對序列的末尾進行操作,所以可以用一個棧來維護此遞增序列。大家可以通過下圖仔細體會上面的分析。如果還是不理解,可以閱讀上面提到的原文。?
?
陳老師的博客中給出的是Python代碼,我們將其改寫成C++代碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
上述代碼的核心就是判斷前后兩個長方形的高度,后一個高就添加到堆棧中,否則就彈出計算面積。該代碼提交到leetcode上運行時間是24ms。上述代碼用了STL中的stack,我們也可以用數組代替,此時代碼可以修改如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
由于少了系統調用,上述代碼運行時間降為16ms。需要注意的一點是,stack_index指向的是堆棧頭的上一個位置。?
上面的兩個代碼雖然都能正確運行,但是有一個壞處,破壞了原始的輸入數組。為什么要向原數組中添加一個-1呢?這個也比較容易理解,假如原始數組是遞增的,我們不可能只添加不彈出,添加一個-1就可以彈出所有的元素。此處也可以對代碼進行修改,避免破壞原始數組。方法就是遍歷完所有的元素之后,判斷堆棧是否為空,不為空就彈出并計算面積,比較簡單,請大家自己實現。
2. 解法二
不知道大家對最長回文子串的幾種解法是否了解。如果不考慮最優解法,最長回文子串問題可以有兩種不同的思路:1. 確定頭和尾,判斷該子串是否為回文串;2. 指定回文串的中點,看能往兩側延伸多長。在最大矩形問題中,上面的解法一和回文子串的思路一相似,同理,我們也可以仿照思路二來解決最大矩形問題。我們可以將直方圖的任意一個長方形當做中點,然后以該長方形的高度當做大長方形的高度,看可以往兩側延伸多長。這種思路其實更符合大家的思維方式。很明顯,這種方法的復雜度也是O(n2),提交代碼還是超時,我們對其進行優化。?
2.1 基于堆棧的解法
上面原始解法慢的原因也是沒有考慮直方圖相鄰長方形之間的關系,我們分下面兩種情況考慮,看是否有優化余地。當出現A這種情形時,其實我們可以獲得一些有用的信息,這表明第i個長方形不能再往左擴展(以第i個長方形的高度往兩側擴展),進而我們可以求得left[i](在此left有兩種不同的含義,既可以為向左擴展到的位置,也可以為向左擴展的長度,后續代碼實現按第一種理解方式)。當出現B情形時,表明第i-1個長方形高,第i個長方形可以繼續往左擴展,直到遇到A情形,然后計算left[i]。在實際情況中,由于A情形和B情形隨機出現,所以前后兩個長方形的位置并不一定相鄰。采用數學歸納法我們可以求得所有的left值。可以看出,A情形只往末尾添加元素,B情形只在末尾彈出元素,從而我們可以用一個棧來維護單調遞增(實際為非遞減)隊列。?
?
同理,我們可以通過倒序遍歷數組來計算right。按照上述思路實現的代碼如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
上面代碼看似是兩重循環,但其實每個元素只進入堆棧一次,通過均攤分析可以得到復雜度為O(n)。提交到leetcode上運行時間為16ms。
2.2 基于單調隊列的解法
除了上面巧妙的堆棧解法外,還可以用單調隊列來解決。單調隊列維護數列的下標,隊列內的元素滿足:?
設單調隊列從頭部開始的元素值為xi,則xi<xi+1且axi<axi+1。?
簡單來說單調隊列就是下標對應的元素是嚴格遞增的順序。當然在實際應用過程中,可能不嚴格單調。?
在該問題中,該怎樣應用單調隊列呢?我們還是分兩種情形考慮。考慮方法正好和基于堆棧的方法相反。假設我們維護了遞增的單調隊列(實際為非遞減),當出現B情形時,我們其實可以知道第i-1個長方形不能再往右擴展,從而可以求得right[i-1];當出現A情形時,第i-1個長方形還可以繼續向右擴展。只要高度遞增,就往單調隊列末尾添加元素,否則就計算單調隊列末尾元素的right值。當我們遍歷完所有的元素之后,單調隊列中存在著一個遞增的序列,表示剩余位置可以從隊列頭擴展到隊列尾。依次計算擴展長度。?
?
同理,我們可以通過倒序遍歷數組來計算left。按照上述思路實現的代碼如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
代碼的整體邏輯和基于堆棧的實現類似,只是由首先計算left變為首先求right。復雜度也是O(n),提交到leetcode上運行時間也是16ms。
3. 擴展
該問題非常有趣,可以做很多擴展。例如將長方形的寬度由1變為任意寬度,再或者將二維直方圖變為三維直方圖求最大長方體。針對這兩個擴展的解法,大家可以參考陳利人老師“待字閨中”微信公眾號中的分析。先給出兩個原始鏈接:《舉一反三,從一道面試題說起》和《快看,快看!求3D柱狀圖中的最大長方體有解了》。?
針對第一個擴展,可以很容易采用上面的解法二實現。首先遍歷一遍所有的長方形,累積長方形的寬度得到每個長方形在不同寬度下的起始位置。然后繼續采用解法二中任意一種實現獲得每個長方形往兩側的擴展位置。最后利用計算的起始位置和擴展位置即可計算最大面積,復雜度還是O(n)。
總結
以上是生活随笔為你收集整理的LeetCode 笔记系列 18 Maximal Rectangle [学以致用](最大矩形)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 上海欢乐谷过山车有体重限制吗
- 下一篇: 1995年的5元硬币现在卖值多少钱?