Largest Rectangle in Histogram

Stack, Dynamic Programming, Divide and Conquer

Hard

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.

Example

Given height = [2,1,5,6,2,3],
return 10.

Analysis

Stack

https://www.jiuzhang.com/solution/largest-rectangle-in-histogram Linear search using a stack of incomplete subproblems

We process the elements in left-to-right order and maintain a stack of information about started but yet unfinished subhistograms. Whenever a new element arrives it is subjected to the following rules. If the stack is empty we open a new subproblem by pushing the element onto the stack. Otherwise we compare it to the element on top of the stack. If the new one is greater we again push it. If the new one is equal we skip it. In all these cases, we continue with the next new element.
If the new one is less, we finish the topmost subproblem by updating the maximum area w.r.t. the element at the top of the stack. Then, we discard the element at the top, and repeat the procedure keeping the current new element. This way, all subproblems are finished until the stack becomes empty, or its top element is less than or equal to the new element, leading to the actions described above. If all elements have been processed, and the stack is not yet empty, we finish the remaining subproblems by updating the maximum area w.r.t. to the elements at the top.
For the update w.r.t. an element, we find the largest rectangle that includes that element. Observe that an update of the maximum area is carried out for all elements except for those skipped. If an element is skipped, however, it has the same largest rectangle as the element on top of the stack at that time that will be updated later.
The height of the largest rectangle is, of course, the value of the element. At the time of the update, we know how far the largest rectangle extends to the right of the element, because then, for the first time, a new element with smaller height arrived. The information, how far the largest rectangle extends to the left of the element, is available if we store it on the stack, too.
We therefore revise the procedure described above. If a new element is pushed immediately, either because the stack is empty or it is greater than the top element of the stack, the largest rectangle containing it extends to the left no farther than the current element. If it is pushed after several elements have been popped off the stack, because it is less than these elements, the largest rectangle containing it extends to the left as far as that of the most recently popped element.
Every element is pushed and popped at most once and in every step of the procedure at least one element is pushed or popped. Since the amount of work for the decisions and the update is constant, the complexity of the algorithm is O(n) by amortized analysis.

(*Favorite) Memoization - Dynamic Programming

Thanks to solution shared by @anton4 on LeetCode forum: https://leetcode.com/problems/largest-rectangle-in-histogram/discuss/28902/5ms-O(n)-Java-solution-explained-(beats-96)

Basic idea is:

For any bar i the maximum rectangle is of width r - l - 1 where
r - is the rightmost index of the bar to the right with height h[r] >= h[i], and
l - is the leftmost index of the bar to the left which height h[l] >= h[i]
Then the area for the bar i is:
h[i] * (r - l - 1)

The main trick is how to effectively calculate leftBound[] and rightBound[] arrays.

• leftBound[] stores the leftmost bar's index for each bar i, which maintains height[l] >= height[i]
• rightBound[] stores the rightmost bar's index for each bar i, which maintains height[r] >= height[i]

Trivial Solution

The trivial solution is to use O(n^2) solution and for each i element first find its left/right neighbour in the second inner loop just iterating back or forward.

Optimization using Memoization

The only line change shifts this algorithm from O(n^2) to O(n) complexity: we don't need to rescan each item to the left - we can reuse results of previous calculations and "jump" through indices in quick manner:

while (p >= 0 && height[p] >= height[i]) {
p = leftBound[p];
}

Solution

Naive Implementation - TLE

public class Solution {
/**
* @param height: A list of integer
* @return: The area of largest rectangle in the histogram
*/
public int largestRectangleArea(int[] height) {
int maxArea = 0;
int[] min = new int[height.length];
for (int i = 0; i < height.length; i++) {
for (int j = i; j < height.length; j++) {
if (i == j) {
min[j] = height[j];
} else {
if (height[j] < min[j - 1]) {
min[j] = height[j];
} else {
min[j] = min[j - 1];
}
}
int tempArea = min[j] * (j - i + 1);
if (tempArea > maxArea) {
maxArea = tempArea;
}
}
}

return maxArea;
}
}

Pruning - AC (9 ms, faster than 89.45%)

public class Solution {
/**
* @param height: A list of integer
* @return: The area of largest rectangle in the histogram
*/
public int largestRectangleArea(int[] height) {
int maxV = 0;
for(int i =0; i< height.length; i++)
{
if(i+1 < height.length && height[i] <= height[i+1]) {
// if not peak node, skip it
continue;
}
int minV = height[i];
for(int j =i; j>=0; j--)
{
minV = Math.min(minV, height[j]);
int area = minV*(i-j+1);
if(area > maxV)
maxV = area;
}
}
return maxV;
}
}

O(n) Linear search using a stack of incomplete subproblems

public class Solution {
public int largestRectangleArea(int[] height) {
if (height == null || height.length == 0) {
return 0;
}

Stack<Integer> stack = new Stack<Integer>();
int max = 0;
for (int i = 0; i <= height.length; i++) {
int current = (i == height.length) ? -1 : height[i];
while (!stack.isEmpty() && current <= height[stack.peek()]) {
int h = height[stack.pop()];
int w = stack.isEmpty() ? i : i - stack.peek() - 1;
max = Math.max(max, h * w);
}
stack.push(i);
}

return max;
}
}

*(Preferred) Another Stack implementation

(notice how boundary is handled: while (!stack.isEmpty() && (i == a.length || a[stack.peek()] > a[i])))

public int largestRectangleArea(int[] a) {
LinkedList < Integer > stack = new LinkedList < > ();
int max = 0;

for (int i = 0; i <= a.length; i++) {
while (!stack.isEmpty() && (i == a.length || a[stack.peek()] > a[i])) {
int height = a[stack.pop()];
int width = (!stack.isEmpty()) ? i - stack.peek() - 1 : i;
max = Math.max(max, height * width);
}

stack.push(i);

}

return max;
}

Another Implementation of Linear search using a stack of incomplete subproblems

public class Solution {
/**
* @param height: A list of integer
* @return: The area of largest rectangle in the histogram
*/
public int largestRectangleArea(int[] height) {
Stack < Integer > stack = new Stack < Integer > ();
int i = 0;
int maxArea = 0;
int[] h = new int[height.length + 1];
// add an 0, so it would calculate the last height
h = Arrays.copyOf(height, height.length + 1);
while (i < h.length) {
if (stack.isEmpty() || h[stack.peek()] <= h[i]) {
stack.push(i++);
} else {
int t = stack.pop();
maxArea = Math.max(maxArea, h[t] * (stack.isEmpty() ? i : i - stack.peek() - 1));
}
}
return maxArea;
}
}

Naive Implementation - calculate area by getting left bound and right bound for heights[i] (not reusing leftBound[] rightBound[]) - AC - (287 ms, faster than 16.64%)

Also check the follow-up solution for optimization using memoization

class Solution {
public int largestRectangleArea(int[] heights) {
if (heights == null || heights.length == 0) {
return 0;
}
if (heights.length == 1) {
return heights;
}
int maxArea = 0;
int[] leftBound = new int[heights.length];
int[] rightBound = new int[heights.length];
leftBound = -1;
rightBound[heights.length - 1] = heights.length;
for (int i = 1; i < heights.length; i++) {
int l = i - 1;
while (l >= 0 && heights[l] >= heights[i]) {
l--;
}
leftBound[i] = l;
}
for (int i = heights.length - 2; i >= 0; i--) {
int r = i + 1;
while (r < heights.length && heights[r] >= heights[i]) {
r++;
}
rightBound[i] = r;
}
for (int i = 0; i < heights.length; i++) {
maxArea = Math.max(heights[i] * (rightBound[i] - leftBound[i] - 1), maxArea);
}
return maxArea;
}
}

*Memoization by reusing the stored leftBound[], rightBound[]
AC - 3ms faster than 96.33%

class Solution {
public int largestRectangleArea(int[] heights) {
if (heights == null || heights.length == 0) {
return 0;
}
if (heights.length == 1) {
return heights;
}
int maxArea = 0;
int[] leftBound = new int[heights.length];
int[] rightBound = new int[heights.length];
leftBound = -1;
rightBound[heights.length - 1] = heights.length;
for (int i = 1; i < heights.length; i++) {
int l = i - 1;
while (l >= 0 && heights[l] >= heights[i]) {
l = leftBound[l];
}
leftBound[i] = l;
}
for (int i = heights.length - 2; i >= 0; i--) {
int r = i + 1;
while (r < heights.length && heights[r] >= heights[i]) {
r = rightBound[r];
}
rightBound[i] = r;
}
for (int i = 0; i < heights.length; i++) {
maxArea = Math.max(heights[i] * (rightBound[i] - leftBound[i] - 1), maxArea);
}
return maxArea;
}
}