LeetCode #85 Hard

Maximal Rectangle

Maximal Rectangle: given a binary matrix, find the largest rectangle containing only 1s and return its area.

Constraints
  • rows == matrix.length
  • cols == matrix[i].length
  • 1 <= rows, cols <= 200
  • matrix[i][j] is '0' or '1'.
monotonic-stackmatrixdp
Open on LeetCode ↗
02

Intuition

Maximal rectangle finds the largest rectangle of 1s in a binary matrix. Considering every possible rectangle is far too slow, and the efficient solution comes from recognising a problem already solved. Process the matrix row by row, and at each row ask what the largest rectangle ending at that row is. If, for each column, the count of consecutive 1s reaching upward is known, that row of counts is a histogram: - Each row becomes a histogram of column heights, and the answer is the largest rectangle in that histogram — reducing this Hard problem to Largest Rectangle in Histogram. The heights update incrementally. For each cell, a 1 increments the height carried from the row above, and a 0 resets it to zero. That reset is essential — a zero breaks the column, so no rectangle can span it. Then run the histogram routine on each row and keep the largest result across all rows. The histogram subroutine uses a monotonic increasing stack. Bars are pushed while heights increase; when a shorter bar arrives, taller bars are popped and each popped bar's rectangle is measured — its width running from the previous stacked index to the current one. Appending a sentinel height of 0 after the last bar flushes the stack, ensuring every remaining bar is measured. Without it, bars left on the stack are never evaluated and the answer comes out short on rows that end tall. The total cost is O(rows × cols): each row's histogram is built in O(cols) and processed in O(cols), since every bar is pushed and popped at most once.

How to spot this pattern

Each row becomes a histogram: the height at a column is how many consecutive 1s sit above it. Then Largest Rectangle in Histogram runs once per row. Reducing a 2-D problem to a known 1-D one, row by row, is the transferable move.

03

Approach

Try it first

Before reading on: price up what the direct approach costs here, then ask what ordering you can maintain so the answer is always at one end. Aim for O(m*n) time and O(n) space.

1

Reduce rows to histograms

For each row, treat the consecutive 1s reaching upward in each column as bar heights. The answer becomes the largest rectangle in that histogram — a problem already solved.

2

Update heights incrementally

A 1 increments the height from the row above; a 0 resets it to zero. The reset is essential, since a zero breaks the column and no rectangle spans it.

3

Run the histogram routine per row

Compute the largest rectangle for each row's histogram and keep the maximum across all rows. Every rectangle has some bottom row, so none is missed.

4

Use a monotonic increasing stack

Push bars while heights increase. When a shorter bar arrives, pop the taller ones — each popped bar's rectangle spans from the previous stacked index to the current.

5

Append a sentinel zero

Add a height of 0 after the last bar to flush the stack. Without it, bars remaining on the stack are never measured and rows ending tall report too small.

6

Cost of the approach

Each row's histogram is built and processed in O(cols), with every bar pushed and popped once, giving O(rows × cols) time and O(cols) space.

04

Solution & live demo

▶1class Solution:
▶2 def maximalRectangle(self, matrix):
▶3 if not matrix:
▶4 return 0
▶5 n = len(matrix[0])
▶6 heights = [0] * n
▶7 best = 0
▶8 for row in matrix:
▶9 for j in range(n):
▶10 heights[j] = heights[j] + 1 if row[j] in (1, '1') else 0
▶11 st = []
▶12 for j in range(n + 1):
▶13 h = heights[j] if j < n else 0
▶14 while st and heights[st[-1]] >= h:
▶15 ht = heights[st.pop()]
▶16 left = st[-1] if st else -1
▶17 best = max(best, ht * (j - left - 1))
▶18 st.append(j)
▶19 return best
05

Common pitfalls

Not resetting the height on a zero

✗ Wrong
if row[j] == '1': heights[j] += 1
✓ Right
heights[j] = heights[j] + 1 if row[j] in (1, '1') else 0

A zero breaks the vertical run, so the accumulated height above it can no longer support a rectangle at this row. Leaving the old value lets rectangles span straight through obstacles.

Omitting the sentinel column

✗ Wrong
for j in range(n):
✓ Right
for j in range(n + 1):
    h = heights[j] if j < n else 0

Bars still on the stack when the scan ends never get measured. A virtual zero-height column at the end forces every remaining bar to pop and be evaluated.

Computing the width from the popped index

✗ Wrong
best = max(best, ht * (j - st[-1]))
✓ Right
left = st[-1] if st else -1
best = max(best, ht * (j - left - 1))

The rectangle spans from just after the new stack top to just before j, giving a width of j - left - 1. Using the popped index measures only part of the span, and an empty stack means the bar extends all the way to the left edge.

06

Edge cases

Empty matrix

Return 0 before any processing.

All zeros

The heights array stays flat at zero and 0 is returned.

All ones

The answer is the full area, m * n.

Single row

It degenerates to plain largest-rectangle-in-histogram on a 0/1 array.

07

Complexity

Time
O(m*n)
Space
O(n)
One histogram pass per row; the naive enumeration is O(m^2 * n^2).