Largest Rectangle in Histogram
Find the area of the largest rectangle that fits inside a histogram. The O(n) stack solution treats each bar as the height of a maximal rectangle, using a monotonic increasing stack to find the left and right boundaries in a single pass. Extends directly to 'Maximal Rectangle in Binary Matrix'.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- The code (single-pass with sentinel)
- Watch it happen, frame by frame
- Extension: Maximal Rectangle in Binary Matrix (LC #85)
- Common traps
- Check yourself
- Practice problems
Before we start
This is one of the most celebrated stack problems in competitive programming. It looks like a geometry puzzle but the core insight is pure monotonic stack from Chapter 5.01 — applied twice (left boundary and right boundary) simultaneously. Mastering this problem unlocks the matrix variant as a free bonus. By the end you will be able to:
- Explain what "this bar as the height" means and how that frames the problem.
- Write the O(n) single-pass stack solution from memory.
- Extend the solution to Maximal Rectangle in a Binary Matrix (LC #85).
Picture this first (no code yet)
A real-life story
You are looking at a city skyline: buildings of different heights stand side by side. You want to place the largest billboard possible that is fully hidden behind the buildings — it must not stick out above any building it covers.
For each building, ask: "if I use this building's height as my billboard height, how far left and right can I extend before I hit a shorter building?" The width is (right boundary − left boundary − 1). Area = height × width. The answer is the maximum area over all buildings.
The question "how far left/right before a shorter building" is exactly the previous-smaller and next-smaller queries — solved by the monotonic stack from Chapter 5.01.
The actual problem
Largest Rectangle in Histogram (LC #84):
Given an array
heightsof non-negative integers representing histogram bar widths of 1, find the area of the largest rectangle that can be formed within the histogram.
heights = [2, 1, 5, 6, 2, 3]→ answer is 10 (bars at index 2 and 3, height 5, width 2).
First, the slow way (so you feel the pain)
O(n²) brute force: for each pair (i, j), the rectangle height is min(heights[i..j]) and width is j - i + 1. Enumerate all pairs and track the max area.
n = len(heights)
max_area = 0
for i in range(n):
min_h = heights[i]
for j in range(i, n):
min_h = min(min_h, heights[j])
max_area = max(max_area, min_h * (j - i + 1))
For n = 100,000: 10¹⁰ operations. At 10⁸ ops/sec, that is 100 seconds. The stack solution does it in O(n).
The turning point
Pause & think
For each bar i with height h = heights[i], the widest rectangle using exactly height h extends:
- Left: to the first bar shorter than
hon the left (call its indexL). - Right: to the first bar shorter than
hon the right (call its indexR).
Width = R - L - 1. Area = h × (R - L - 1).
This is previous-smaller (L) and next-smaller (R) — the same queries from Chapter 5.01. The monotonic stack computes both in a single pass by processing elements as they become "popped" (when a new shorter bar is found).
When does a bar i get popped from a monotonic increasing stack? Exactly when a shorter bar j arrives — making j the right smaller boundary of i. At that moment, the new stack top (before i was pushed) is the left smaller boundary of i.
The one idea to remember
The entire pattern in one sentence
Maintain a monotonic increasing stack of bar indices; when a bar is popped (because a shorter bar arrived), calculate its maximum rectangle using the arriving bar as the right boundary and the new stack top as the left boundary.
The code (single-pass with sentinel)
def largestRectangleArea(heights):
heights = heights + [0] # sentinel: forces all remaining bars to pop at the end
stack = [-1] # sentinel: gives a clean left boundary for leftmost bars
max_area = 0
for i, h in enumerate(heights):
while stack[-1] != -1 and heights[stack[-1]] >= h:
height = heights[stack.pop()] # this bar's height
width = i - stack[-1] - 1 # right = i, left = new stack top + 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
Line-by-line narration:
heights + [0]— appending a zero sentinel ensures every bar eventually gets popped (a height-0 bar is shorter than everything). Without it, bars remaining in the stack at loop's end wouldn't be processed.stack = [-1]— index −1 is a left sentinel. When we pop the leftmost bar,stack[-1]is −1, makingwidth = i - (-1) - 1 = i, which spans from index 0 to i−1 correctly.while heights[stack[-1]] >= h:— pop all bars taller than or equal to the arriving bar (if equal, we pop too — it is safe; those bars would form at most the same rectangle with the current bar extending it).height = heights[stack.pop()]— the popped bar's height is the rectangle's height.width = i - stack[-1] - 1— after popping,stack[-1]is the new top = the previous-smaller index. The width spans fromstack[-1] + 1toi - 1, sowidth = i - stack[-1] - 1.stack.append(i)— push current bar's index (stack stays increasing in height).
Watch it happen, frame by frame
heights = [2, 1, 5, 6, 2, 3] → appended sentinel: [2, 1, 5, 6, 2, 3, 0]
stack=[-1]
i=0, h=2: stack top=-1 (sentinel). Push 0. stack=[-1, 0]
i=1, h=1: heights[0]=2 >= 1 → pop 0.
height=2, width=1-(-1)-1=1. area=2. max=2.
stack top=-1. Push 1. stack=[-1, 1]
i=2, h=5: 1<5 → push 2. stack=[-1,1,2]
i=3, h=6: 5<6 → push 3. stack=[-1,1,2,3]
i=4, h=2: heights[3]=6>=2 → pop 3. height=6, width=4-2-1=1. area=6. max=6.
heights[2]=5>=2 → pop 2. height=5, width=4-1-1=2. area=10. max=10. ✅
heights[1]=1 < 2 → stop. Push 4. stack=[-1,1,4]
i=5, h=3: heights[4]=2<3 → push 5. stack=[-1,1,4,5]
i=6, h=0 (sentinel):
heights[5]=3>=0 → pop 5. height=3, width=6-4-1=1. area=3.
heights[4]=2>=0 → pop 4. height=2, width=6-1-1=4. area=8.
heights[1]=1>=0 → pop 1. height=1, width=6-(-1)-1=6. area=6.
stack=[-1]. Top=-1, stop. Push 6. (doesn't matter)
max_area = 10 ✅
Extension: Maximal Rectangle in Binary Matrix (LC #85)
Given an
m × nbinary matrix, find the area of the largest rectangle containing only 1s.
Reduction: treat each row as the "ground" and compute a histogram of consecutive 1s above each cell. Then run LC #84 on each row's histogram.
def maximalRectangle(matrix):
if not matrix or not matrix[0]:
return 0
n = len(matrix[0])
heights = [0] * n
max_area = 0
for row in matrix:
# update histogram
for j in range(n):
heights[j] = heights[j] + 1 if row[j] == '1' else 0
# largest rectangle for this row's histogram
max_area = max(max_area, largestRectangleArea(heights))
return max_area
For an m × n matrix: O(m × n) time — one histogram update per cell, one stack pass per row.
Common traps
Watch out for these
- Forgetting the zero sentinel at the end of
heights. Without it, bars still in the stack after the loop never get processed. You'd have to add a second cleanup loop — the sentinel avoids that elegantly. - Forgetting the
-1sentinel at the bottom of the stack. Withoutstack[-1] = -1, when you pop the leftmost bar,stackbecomes empty andstack[-1]crashes with IndexError. The sentinel eliminates this edge case. - Computing width as
i - stack[-1]instead ofi - stack[-1] - 1. After popping bark, the rectangle extends from indexstack[-1] + 1toi - 1(inclusive), so width =(i - 1) - (stack[-1] + 1) + 1 = i - stack[-1] - 1. Off-by-one here is the most common mistake. - Using
>instead of>=in the pop condition. Equal heights: bar at index 2 and 3 both have height 5. When bar 3 is popped by a shorter bar, computing only bar 3's rectangle is fine — bar 2's rectangle (which is wider) will be computed when bar 2 is popped. Using>=or>both work, but>=is cleaner: it pops the earlier equal bar first, which avoids a subtle width error in some edge cases.
Remember this forever
Largest Rectangle in Histogram
def largestRectangleArea(heights):
heights = heights + [0] # sentinel forces all pops
stack = [-1] # sentinel for left boundary
max_area = 0
for i, h in enumerate(heights):
while stack[-1] != -1 and heights[stack[-1]] >= h:
ht = heights[stack.pop()]
wd = i - stack[-1] - 1
max_area = max(max_area, ht * wd)
stack.append(i)
return max_area
Width formula: i - stack[-1] - 1 (right boundary − left sentinel − 1).
Two sentinels: [0] at heights end, [-1] at stack bottom.
Extension: build histogram row by row → LC #85 Maximal Rectangle.
Check yourself
When bar `k` is popped from the stack, why is `stack[-1]` (the new top) the left boundary?
The stack maintains an increasing sequence of bar heights. When bar k is pushed, all bars taller than k have already been popped — meaning the bars currently below k in the stack are all shorter than k. The new top after popping k is the rightmost bar that is strictly shorter than heights[k]. Since it's shorter, the rectangle of height heights[k] cannot extend past it to the left. So stack[-1] index + 1 is where the rectangle of height heights[k] starts. Left start = stack[-1] + 1, right end = i - 1 (the bar that triggered the pop is shorter, so we stop before it), width = i - stack[-1] - 1.
What is the time complexity of the single-pass stack solution, and why?
O(n). Each bar index is pushed onto the stack exactly once and popped at most once. The while loop across all iterations does at most n total pops (each pop corresponds to one earlier push). So the total work across all iterations is O(n) pushes + O(n) pops = O(n). The single pass is O(n), and the sentinel ensures no second pass is needed.
For the Maximal Rectangle matrix problem, why do we reset `heights[j] = 0` when `matrix[row][j] == '0'`?
The histogram for each row represents the number of consecutive 1s above and including that cell in column j. If the current cell is '0', there are no consecutive 1s ending here — the streak is broken. Setting heights[j] = 0 correctly resets the bar height to zero, meaning this column contributes no height to the histogram for this row. If we didn't reset, a '0' cell would inherit the height from the previous row, incorrectly counting 1s that aren't actually consecutive.
Practice problems
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Largest Rectangle in Histogram | Hard | Two sentinels; width = i - stack[-1] - 1 | LC #84 |
| Maximal Rectangle | Hard | Build histogram per row → LC #84 per row | LC #85 |
| Maximal Square | Medium | DP (not stack); dp[i][j] = min(left, top, diag) + 1 | LC #221 |
| Trapping Rain Water | Medium | Use stack (or two-pointer) for water levels | LC #42 |
Next up: Stack-based Expression Evaluation — how stacks handle operator precedence and parentheses to evaluate arithmetic expressions in O(n), covering Reverse Polish Notation and Basic Calculator.