Largest Rectangle in Histogram
Given an array of integers heights representing the histogram's bar heights where the width of each bar is 1, return the area of the largest rectangle in the histogram.
- 1 <= heights.length <= 10⁵
- 0 <= heights[i] <= 10⁴
heights = [2,1,5,6,2,3]10heights = [4, 2, 0, 3, 2, 5]6Finding the area of the largest rectangle in a histogram is a challenge of boundaries. The area of any rectangle is governed by the formula:
Area = height × width
In a histogram where every bar has a width of 1, if we fix a specific bar as the "height", its maximum width is determined by how far it can expand to the left and right before hitting a bar shorter than itself.
Width = (Right Boundary - Left Boundary - 1)
The bottleneck in a naive approach is that the shortest bar in any group always dictates the height for the entire range.
We pick every possible pair of bars as the left and right boundaries. For every pair, we find the shortest bar between them to determine the height.
max_area = 0
n = len(heights)
for i in range(n):
min_h = heights[i]
for j in range(i, n):
# Update minimum height for the current range
min_h = min(min_h, heights[j])
max_area = max(max_area, min_h * (j - i + 1))
return max_areaTo speed this up, we can use extra memory to pre-calculate the boundaries for every single bar. For each bar $i$, we want to know:
1. Left Boundary: The index of the first bar to the left that is strictly shorter than $heights[i]$.
2. Right Boundary: The index of the first bar to the right that is strictly shorter than $heights[i]$.
If we store these in two arrays, left_smaller and right_smaller, we can calculate the area for every bar in a single pass.
# Pre-calculate boundaries (using two passes)
# Once we have left_smaller and right_smaller:
for i in range(n):
width = right_smaller[i] - left_smaller[i] - 1
max_area = max(max_area, heights[i] * width)To optimize even further and avoid multiple passes, we use a Monotonic Stack. By maintaining a stack of indices in increasing order of height, we can resolve the boundaries of multiple bars in a single pass.
The strategy works as follows:
- The Discovery: We scan the histogram from left to right. If the current bar is shorter than the bar at the top of our stack, we have found a "Right Wall" for that top bar.
- The Calculation: We pop the top index (the height). The "Left Wall" is the new top of the stack (the nearest smaller bar to its left). The width is then calculated as the distance between these two walls.
- The Sentinel: We add a virtual bar of height 0 at the very end. This ensures that every remaining bar in the stack is forced out and its area is calculated before the process ends.
# Monotonic Stack (O(N) Time, O(N) Space)
heights.append(0) # Sentinel to flush the stack
stack = [-1] # Virtual left wall
max_area = 0
for i in range(len(heights)):
# While current bar is a 'Right Wall' for the stack top
while len(stack) > 1 and heights[i] < heights[stack[-1]]:
h = heights[stack.pop()]
w = i - stack[-1] - 1
max_area = max(max_area, h * w)
stack.append(i)
return max_areaBy resolving each bar exactly once, we transform a nested search into a perfect linear scan. The stack acts as a memory of "unsolved" heights, waiting for their boundaries to be discovered.
Monotonic Stack boundary technique
Mental Model
- The Limiter: Each bar `i` extends as long as neighboring bars are *taller*.
- Boundary Finding: Use a Monotonic Stack to find the nearest smaller element on both sides in O(N).
O(N) Complexity
The brute force approach takes O(N²). Monotonic Stack allows us to find the best boundaries for every single bar in linear time!