1. 程式人生 > >[LeetCode] 84. Largest Rectangle in Histogram

[LeetCode] 84. Largest Rectangle in Histogram

rectangle 搜索 進入 clas 組成 ger simple gin cti

1. 題目描述

技術分享圖片

2. 樣例輸入

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

Output: 10

3. 解題報告

解法一:暴力搜索法

我們自然而然就會想到的最樸素的解法就是用雙重嵌套for循環來暴力搜索最優解,第一層for循環遍歷每一個bar,第二層for循環遍歷以上一層for循環中每一個bar為最右端再向左看的所有可能的矩形,在此過程中記錄最大矩形的值即可。

代碼如下:

 1   public static int largestRectangleArea(int[] heights) {
 2         int
res = 0; 3 int curHeight = 0; 4 5 for (int i = 0; i < heights.length; ++i) { 6 curHeight = heights[i]; 7 for (int j = i; j >= 0; --j) { 8 curHeight = Math.min(curHeight, heights[j]); 9 res = Math.max(res, curHeight * (i - j + 1));
10 } 11 } 12 13 return res; 14 }

運行結果如下:

技術分享圖片

顯然,暴力搜索效率很低,其時間復雜度為O(n2),只打敗了10%的人,這種成績是不可接受的,我們要考慮優化算法。

解法二:暴力搜索法 + 剪枝

暴力搜索法存在很多重復計算,經過簡單觀察,可以發現,其實我們沒有必要在每一個bar都回頭向左遍歷一遍所有可能的矩形,我們只需要在局部峰值處回頭向左遍歷即可。所謂局部峰值,即當heights[i]>heights[i+1]時,我們就認為heights[i]為局部峰值。這樣做的原因是因為非局部峰值處的情況後面的局部峰值都可以包括。舉個例子,觀察直方圖[2,1,5,6,2,3],局部峰值為[2,6,3],非局部峰值5所能包含的矩形在局部峰值6處都可以包括並且可以加上6本身的一步分組成更大的矩形,所以就沒必要計算非局部峰值處的情況了,讀者畫個圖將更有助於理解。

代碼如下:

 1   public static int largestRectangleArea(int[] heights) {
 2         int res = 0;
 3         int curHeight = 0;
 4 
 5         for (int i = 0; i < heights.length; ++i) {
 6             if (i < heights.length - 1 && heights[i] <= heights[i + 1]) {
 7                 continue;
 8             }
 9             curHeight = heights[i];
10             for (int j = i; j >= 0; --j) {
11                 curHeight = Math.min(curHeight, heights[j]);
12                 res = Math.max(res, curHeight * (i - j + 1));
13             }
14         }
15 
16         return res;
17     }

運行結果如下:

技術分享圖片

由上圖可見,速度有了顯著提升,打敗了90%的人也算是差強人意,這效果雖好但理論上仍然是O(n2) 的復雜度,我們接下來尋求時間復雜度更低的解法。

解法三:分治法

其實對於優化暴力搜索法,最容易想到的方法並不是解法二中的剪枝法,那是需要一些靈感的。按照解題套路,接下來我們應該嘗試是否有時間復雜度為O(nlgn)的解法,一看到lgn,我們應該本能反應到是否可以嘗試一下分治法。使用分治法解決此題的思路還是很清晰的:對於一段給定數量的bar,從中間的bar一分為二,含有最大矩形面積的區域要麽在中間bar的左側,要麽在中間bar的右側,要麽是橫跨中間bar。對於橫跨中間bar的情況,可以在O(n)時間內解決,因此,遞歸式為 T(n)=2T(n/2)+O(n),時間復雜度為O(nlgn)。

代碼如下:

 1     public static int maxArea(int[] heights, int l, int h) {
 2         if (l == h) return heights[l];
 3 
 4         int m = l + (h - l) / 2;
 5         int res = maxArea(heights, l, m);
 6         res = Math.max(res, maxArea(heights, m + 1, h));
 7         res = Math.max(res, combineArea(heights, l, m, h));
 8 
 9         return res;
10     }
11 
12     public static int combineArea(int[] heights, int l, int m, int h) {
13         int res = 0;
14         int i = m, j = m + 1;
15         int curHeight = Math.min(heights[i], heights[j]);
16         while (i >= l && j <= h) {
17             curHeight = Math.min(curHeight, Math.min(heights[i], heights[j]));
18             res = Math.max(res, (j - i + 1) * curHeight);
19             if (i == l) {
20                 ++j;
21             } else if (j == h) {
22                 --i;
23             } else {
24                 if (heights[i - 1] > heights[j + 1]) {
25                     --i;
26                 } else {
27                     ++j;
28                 }
29             }
30         }
31 
32         return res;
33     }
34 
35     public static int largestRectangleArea(int[] heights) {
36         if (heights.length == 0) return 0;
37         else return maxArea(heights, 0, heights.length - 1);
38     }

運行結果如下:

技術分享圖片

速度基本符合預期,但是否還存在O(n)的解法呢?

解法四:單調棧法

所謂單調棧,就是棧內只存放單調遞增或單調遞減的序列,以單調增棧為例,若待入棧元素比棧頂元素大,則入棧;反之,則彈出棧頂元素,進行相應處理。使用單調棧可以找到元素向左遍歷第一個比他小的元素,也可以找到元素向左遍歷第一個比他大的元素。單調棧的維護是 O(n) 級的時間復雜度,因為所有元素只會進入棧一次,並且出棧後再也不會進棧了。

使用單調棧解決這道題的核心思想和解法二的剪枝法有異曲同工之妙,但要更加精巧的多,更詳細的解釋可看這裏。

代碼如下:

 1 public static int largestRectangleArea(int[] heights) {
 2         int res = 0;
 3         Stack<Integer> s = new Stack<>();
 4 
 5         for (int i = 0; i < heights.length; ++i) {
 6             while (!s.isEmpty() && heights[i] < heights[s.peek()]) {
 7                 res = Math.max(res, heights[s.pop()] * (s.isEmpty() ? i : i - s.peek() - 1));
 8             }
 9             s.push(i);
10         }
11 
12         while (!s.isEmpty()) {
13             res = Math.max(res, heights[s.pop()] * (s.isEmpty() ? heights.length : heights.length - s.peek() - 1));
14         }
15 
16         return res;
17     }

運行結果如下:

技術分享圖片

速度比前面慢的主要原因是stack的使用,如果數據集能夠更大一些,才能夠體現出O(n)解法的優勢。

參考資料:

1. Simple Divide and Conquer AC solution without Segment Tree

2. AC clean Java solution using stack

3. [LeetCode] Largest Rectangle in Histogram 直方圖中最大的矩形

2019-05-07 於清華園

[LeetCode] 84. Largest Rectangle in Histogram