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)解法的优势。
解法五:记录边界法
构造两个数组left[]和right[],left[i]记录从第i个bar向左看高度不低于它的连续的最远的bar的下标,right[i]是向右看。举个例子,有[4,7,8,9,5,6,2],以5为例,其向左看只能到7,向右看只能到6。如此,计算最大矩形面积,只需要从(right[i]-left[i]+1)*heights[i]中挑一个最大的即可。在代码实现中,运用了和kmp算法类似的"jump"的技巧。
代码如下:
1 public int largestRectangleArea(int[] heights) { 2 // validate input 3 if(heights == null || heights.length == 0) { 4 return 0; 5 } 6 7 // init 8 int n = heights.length; 9 int[] left = new int[n]; 10 int[] right = new int[n]; 11 int result = 0; 12 13 // build left 14 left[0] = 0; 15 for(int i = 1; i < n; i++) { 16 int currentLeft = i-1; 17 while(currentLeft >= 0 && heights[currentLeft] >= heights[i]) { 18 currentLeft = left[currentLeft]-1; 19 } 20 21 left[i] = currentLeft+1; 22 } 23 24 // build right 25 right[n-1] = n-1; 26 for(int i = n-2; i >= 0; i--) { 27 int currentRight = i+1; 28 while(currentRight < n && heights[i] <= heights[currentRight]) { 29 currentRight = right[currentRight]+1; 30 } 31 32 right[i] = currentRight-1; 33 } 34 35 // compare height 36 for(int i = 0; i < n; i++) { 37 result = Math.max(result, (right[i]-left[i]+1)*heights[i]); 38 } 39 40 // return 41 return result; 42 }
运行结果如下:
参考资料:
1. Simple Divide and Conquer AC solution without Segment Tree
2. AC clean Java solution using stack
3. [LeetCode] Largest Rectangle in Histogram 直方图中最大的矩形
4. Java O(n) left/right arrays solution, 4ms beats 96%
2019-05-07 于清华园