https://leetcode.com/problems/largest-rectangle-in-histogram/
Given n non-negative integers representing the histogram's bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.
Above is a histogram where width of each bar is 1, given height = [2,1,5,6,2,3]
.
The largest rectangle is shown in the shaded area, which has area = 10
unit.
For example,
Given height = [2,1,5,6,2,3]
,
return 10
.
解题思路:
这是比较难的一道道题,但是O(n^2)的解法很容易写出。遍历每个histogram,对其往左右两个方向拓展,直到遇到数组边缘或者比它小的数字,这样便找出了此处的窗口宽度。于是此处的面积,就是窗口宽度乘以该histogram的高度。遍历结束之后,全局最大的面积也就出来了。
但是这个解法在数据量大的时候会TLE,这个结果是可以预见的。所以,本题一定不是要O(n^2)的解法,应该是O(n)。直觉上像是动态规划的题目,但是想了半天都没找到解法。
看到stack的tag,想到利用height的递增性,但是还是想不出解法。好吧,只能瞄了瞄大神的解法。
思路是,始终维护一个单调递增的stack。一旦当前height比栈顶小,就弹出栈顶,直到当前height进栈后,是递增的。那么,弹出栈顶的时候,如何计算当前面积?
我们令当前准备弹出的位置为hereIndex,计算以当前hereIndex为高度的矩形的面积。
那么之前栈内,hereIndex往右一定都比index高,所以往右的都是宽度(rightIndex - hereIndex)。从hereIndex往左,是不是一定都比index矮?不一定,比如上图,1、5、6就是的。但是,1、5、6、2、3,2进来的时候,5、6被弹出了,所以只有1、2、3。计算2的宽度的时候,我们看到从1到2之间的5、6都是可以算在2的宽度里的。所以往左的宽度就是hereIndex - stack.peek()。
因为此时hereIndex已经被弹出来了,所以这里的stack.peel()其实就是在栈内的hereIndex的前一个坐标。
我们看到,(rightIndex - hereIndex) + (hereIndex - stack.peek())其实就是rightIndex - stack.peek()。所以宽度就是栈内hereIndex左右坐标相减。
但是,如果index是栈内的最后一个元素,弹出后,栈内为空。index是全局最短的元素,从左边的0一直到右边都是它的宽度,所以此时宽度是rightIndex + 1。
为什么不是rightIndex - hereIndex + 1?考虑这个情况,[2,1,2]。最后栈内只有[1,2],计算到1的时候,以1为高度的矩形宽度显然为3,而不是2。这里我们可以感觉到,只要栈内是空的,那么1往左的高度一定都是大于1的,所以一定要算上,否则栈就不会是空的。
最后,整个遍历结束后,还要对栈内剩余的元素进行一遍同样的操作。
public class Solution { public int largestRectangleArea(int[] height) { int result = 0, current = 0; Stack<Integer> stack = new Stack<Integer>(); for(int i = 0; i < height.length; i++) { if(stack.empty() || height[stack.peek()] <= height[i]) { stack.push(i); } else { int rightIndex = stack.peek(); while(!stack.empty() && height[stack.peek()] > height[i]) { int hereIndex = stack.pop(); if(!stack.empty()) { // current = height[hereIndex] * ((rightIndex - hereIndex) + (hereIndex - stack.peek())); current = height[hereIndex] * (rightIndex - stack.peek()); } else { // current = height[hereIndex] * ((rightIndex - hereIndex + 1) + hereIndex); current = height[hereIndex] * (rightIndex + 1); } result = Math.max(result, current); } stack.push(i); } } if(!stack.empty()) { int rightIndex = stack.peek(); while(!stack.empty()) { int hereIndex = stack.pop(); if(!stack.empty()) { current = height[hereIndex] * (rightIndex - stack.peek()); } else { current = height[hereIndex] * (rightIndex + 1); } result = Math.max(result, current); } } return result; } }
这道题在思路上比较类似 Longest Valid Parentheses,都是维护了一个储存index的stack。对于栈内的任意一个index,栈顶元素是其窗口的右边界,而index左侧的元素则是窗口的左边界。面积的计算发生在栈顶元素弹出的时候。
因为每个元素都只被弹出一次,也仅仅计算一次,所以时间复杂度为O(n)。
这道题非常巧妙,也比较难。但是我们可以看出,stack这个数据结构并非像LIFO表面上这么简单。利用这个性质,加上单调性,本题可以得到解决。
//20181022
class Solution { public int largestRectangleArea(int[] heights) { int max = 0; Stack<Integer> stack = new Stack<Integer>(); for (int i = 0; i < heights.length; i++) { while (!stack.empty() && heights[i] < heights[stack.peek()]) { int hereIndex = stack.pop(); int hereArea = stack.empty() ? heights[hereIndex] * i : heights[hereIndex] * (i - 1 - stack.peek()); max = Math.max(max, hereArea); } stack.push(i); } if (!stack.empty()) { int rightIndex = stack.peek(); while (!stack.empty()) { int hereIndex = stack.pop(); int hereArea = stack.empty() ? heights[hereIndex] * (rightIndex + 1): heights[hereIndex] * (rightIndex - stack.peek()); max = Math.max(max, hereArea); } } return max; } }