DSA sheet · Stack · Monotonic stack pattern

Maximal Rectangle

This is the last question of the monotonic stack pattern, and the teacher says up front that its prerequisite is Largest Rectangle in Histogram (problem 6). She first writes a brute force that tries every rectangle (corner to corner) and checks it cell by cell, shows the huge complexity and the TLE, explains why a graph approach (BFS/DFS) isn't the right tool, and then shows the real trick: treat each row as the floor of a histogram, and run the histogram solution once per row.

Why it matters: it's a perfect example of reducing a new problem to one you already solved. A 2D matrix looks scary; broken into rows, it's just problem 6 again.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

What is a stack?

A stack is a pile of plates: you only touch the top. Last in, first out: LIFO.

operationmeaningPython (list as a stack)
pushput on topst.append(x)
popremove the top and get itst.pop()
peeklook at the topst[-1]
empty?nothing insidenot st

Drawn left = bottom, right = top. Always check that the stack is not empty before pop() or st[-1].

Monotonic increasing stack (from problem 6)

We keep bar indexes in the stack so that their heights increase from bottom to top. When a shorter bar arrives, we pop the taller ones. At the moment a bar is popped:

The biggest rectangle whose shortest bar is the popped bar spans everything between PSE and NSE: width = NSE − PSE − 1 (or NSE if there's no PSE). A fake bar of height 0 at the end pops whatever is left.

the histogram helper we'll reuse (from problem 6)
def largest_rectangle_area(heights):
    n = len(heights)
    max_area = 0
    st = []                                  # indexes, heights increasing
    for i in range(n + 1):                   # i == n is the fake 0 bar
        h = 0 if i == n else heights[i]
        while st and h < heights[st[-1]]:
            height = heights[st.pop()]
            width = i if not st else i - st[-1] - 1
            max_area = max(max_area, height * width)
        st.append(i)
    return max_area

If any of this feels new, read problem 6 first; the teacher insists on it too.


Part A · Brute force: every pair of corners, check every cell

LeetCode 85

1The question in simple words

You get a grid (matrix) of "1"s and "0"s. Find the biggest rectangle that contains only 1s, and return its area (the number of cells in it).

        col: 0 1 2 3 4
  row 0:     1 0 1 0 0
  row 1:     1 0 1 1 1
  row 2:     1 1 1 1 1
  row 3:     1 0 0 1 0          answer = 6  (rows 1–2, cols 2–4)

The teacher points at several valid rectangles of all-1s, with areas 2, 4, 5 and 6; the biggest is 6. A block that would include even one 0 is not allowed: for example, extending the red block down into row 3 hits zeros, so it's invalid.

On LeetCode the cells are the characters "1" and "0", not numbers, so we compare with "1".

2What the constraints tell us

3Intuition: a rectangle is fixed by two corners

Any rectangle in a grid is decided by its top-left cell (r1, c1) and its bottom-right cell (r2, c2). So the simplest idea is: try every top-left, try every bottom-right below and to the right of it, and check whether every cell inside is 1. If yes, compute the area and keep the biggest.

Two conditions make a valid answer: (1) it must be a rectangle (not an L-shape or any other shape), which the two-corner idea guarantees, and (2) every cell inside must be 1.

4Building the logic from examples

Area from two corners: why "+1"?

Start with the smallest case: (r1, c1) = (r2, c2) = (0, 0). That's one cell, area 1. The formula has to give 1 here, not 0.

The teacher's explanation: on a number line, the distance from point 1 to point 1 is 1 − 1 = 0, because we measure between points. In a grid we count cells, and the cells from r1 to r2 (both included) number r2 − r1 + 1.

area = (r2 − r1 + 1) × (c2 − c1 + 1)

For (0,0) → (0,0): 1 × 1 = 1 ✓. For (1,2) → (2,4): 2 × 3 = 6 ✓.

Growing the bottom-right corner

Fix the top-left at (0, 0). Move the bottom-right: (0,0) is valid (area 1). (0,1) includes a 0 → invalid. Then (1,0): cells (0,0) and (1,0) are both 1 → valid, area 2. Then (1,1) → the block from (0,0) to (1,1) includes zeros → invalid. And so on. For each pair we re-check all cells inside with two more loops.

Doubt: if the top-left cell itself is 0, should we try any bottom-right at all?
→ No. Every rectangle starting there contains that 0, so all are invalid. The teacher skips such starts with continue. She also notes that once a 0 blocks the way, going further in that direction is pointless; that's a small speed-up, but it's still the brute force.

5Approach steps

  1. For every top-left (r1, c1): if that cell is 0, skip it.
  2. For every bottom-right (r2, c2) with r2 ≥ r1 and c2 ≥ c1:
  3. Check every cell in rows r1..r2, columns c1..c2. If any is 0, this rectangle is invalid.
  4. If it's valid: area = (r2 − r1 + 1) × (c2 − c1 + 1); update the maximum.
  5. Return the maximum.

6Code (Python)

Brute force: all corner pairs + full check
class Solution:
    def maximalRectangle(self, matrix):
        rows, cols = len(matrix), len(matrix[0])
        max_area = 0
        for r1 in range(rows):                       # top-left corner
            for c1 in range(cols):
                if matrix[r1][c1] == "0":            # can't start on a 0
                    continue
                for r2 in range(r1, rows):           # bottom-right corner
                    for c2 in range(c1, cols):
                        if self.allOnes(matrix, r1, c1, r2, c2):
                            area = (r2 - r1 + 1) * (c2 - c1 + 1)
                            max_area = max(max_area, area)
        return max_area

    def allOnes(self, matrix, r1, c1, r2, c2):
        for r in range(r1, r2 + 1):
            for c in range(c1, c2 + 1):
                if matrix[r][c] == "0":
                    return False                     # one 0 spoils it
        return True

7Code line by line

linewhat it means
for r1 … for c1 …Pick the top-left corner: R × C choices.
if matrix[r1][c1] == "0": continueA rectangle can't start on a 0.
for r2 in range(r1, rows) / c2 in range(c1, cols)Pick the bottom-right corner, never above or left of the top-left. Starting at r1 and c1 includes the 1-cell rectangle.
self.allOnes(...)Walk over every cell inside; return False at the first 0.
area = (r2 - r1 + 1) * (c2 - c1 + 1)Count of rows × count of columns (cells, so +1).

8Dry run: a few rectangles from the example

top-leftbottom-rightcells insidevalid?areamax so far
(0,0)(0,0)1yes11
(0,0)(0,1)1 0no–1
(0,0)(3,0)1 1 1 1 (column 0)yes4 × 1 = 44
(1,2)(1,4)1 1 1yes1 × 3 = 34
(1,2)(2,4)1 1 1 / 1 1 1yes2 × 3 = 66
(1,2)(3,4)… row 3 has 0 0 1 0 …no–6
(2,0)(2,4)1 1 1 1 1yes1 × 5 = 56

After all pairs, the answer is 6 ✓.

9Complexity & remember

Remember the brute forceTwo corners fix a rectangle. Check every cell inside. Area = (r2 − r1 + 1)(c2 − c1 + 1). Six nested loops → hopeless for 200 × 200.

Part B · Optimal: one histogram per row + monotonic stack

1The question (same)

Same grid, same answer. We change how we look at the grid.

2What the constraints tell us

R, C ≤ 200. If each row costs O(C) after O(C) preparation, the total is O(R·C) = 4 × 10⁴. Tiny.

3Intuition: first, why not a graph?

The teacher admits that the first time she saw this problem she thought of graphs. A 2D grid can be seen as a graph (each cell is a node, neighbours are joined), and "Max Area of Island" uses BFS/DFS to measure connected groups of 1s.

But BFS/DFS finds a connected blob of any shape. In the example, all the 1s touch each other, so BFS would report the whole blob, which is not a rectangle. You'd need extra logic to check rectangle shapes, and it would be slower. So she drops the graph idea.

Then why a stack? Nothing in the grid obviously says "go back and find a smaller value". The trick is to stop seeing a matrix and look at it row by row.

4Building the logic: turn each row into a histogram

Just row 0

Look only at row 0: 1 0 1 0 0. Think of each 1 as a block of height 1 standing on the floor. That's a histogram with heights [1, 0, 1, 0, 0]. The largest rectangle is 1.

Rows 0–1, with row 1 as the floor

Now stand on row 1 and look up. In each column, count how many 1s are stacked on top of each other, ending at row 1. Column 0 has 1 and 1 → height 2. Column 1 has a 0 at row 1 → height 0. Column 2 → 2. Columns 3 and 4 have a 1 only in row 1 → 1. Heights: [2, 0, 2, 1, 1]. Largest rectangle: the three bars 2, 1, 1 at columns 2–4 give height 1 × width 3 = 3 (and single bars give 2).

The teacher's point: problem 6 also just took an array like [2, 0, 2, 1, 1]; the blocks were only a picture. So if we can turn the 2D grid into a 1D heights array for each row, we can reuse problem 6 directly.

Rows 0–2, floor = row 2

Heights become [3, 1, 3, 2, 2]. The largest rectangle here is height 2 × width 3 = 6. That's our answer.

Floor = row 3, and the rule for zeros

Row 3 is 1 0 0 1 0. You might think "just keep adding", but that would be wrong. A 1 counts only if it's directly on top of other 1s with no gap. If the floor cell is 0, nothing above it can reach the floor, so that column's height drops back to 0. Heights: [4, 0, 0, 3, 0]. Largest: 4. That's less than 6.

floor rowrow cellsheightslargest rectangle in that histogram
01 0 1 0 0[1, 0, 1, 0, 0]1
11 0 1 1 1[2, 0, 2, 1, 1]3
21 1 1 1 1[3, 1, 3, 2, 2]6
31 0 0 1 0[4, 0, 0, 3, 0]4

The answer is the biggest of the four: 6.

floor = row 0
 1 | ██    ██
   +---------------
      1  0  1  0  0
floor = row 1
 2 | ██    ██
 1 | ██    ██ ██ ██
   +---------------
      2  0  2  1  1
floor = row 2
 3 | ██    ██
 2 | ██    ▓▓ ▓▓ ▓▓
 1 | ██ ██ ▓▓ ▓▓ ▓▓
   +---------------
      3  1  3  2  2
floor = row 3
 4 | ██
 3 | ██       ██
 2 | ██       ██
 1 | ██       ██
   +---------------
      4  0  0  3  0

▓▓ = the best rectangle (2 × 3 = 6). In the grid it's rows 1–2, columns 2–4.

Doubt: the teacher calls this a "prefix sum". Is it?
→ It's like one, going down each column, with one big change: it resets to 0 at every 0. So it's really "how many 1s in a row, going up from here". The update is just: heights[j] = heights[j] + 1 if the cell is 1, else heights[j] = 0.
Pencil fix: for a 0 cell, the height must be set to 0, not just "left alone".
→ The teacher says that for a 0 "we don't add anything". If the code only adds 1 for a 1 and never resets, a column keeps its old height across a 0. Example: a single column 1, 0, 1. The real answer is 1, but without the reset the last row's height would be 2 and the answer would be 2. In the main example the mistake happens to give 6 anyway, which is why it's easy to miss. Always write the else: heights[j] = 0. The tests check this.
Doubt: why does this find every rectangle?
→ Every all-1 rectangle has a bottom row. When that row is the floor, every column of the rectangle has height at least the rectangle's height (the 1s stack up from the floor with no gap). So the histogram for that floor contains the rectangle, and problem 6 finds something at least as big.

5Approach steps

  1. heights = [0] × cols; max_area = 0.
  2. For each row i, for each column j: if the cell is "1", heights[j] += 1, else heights[j] = 0.
  3. After finishing the row, run the histogram helper on heights and update max_area.
  4. Return max_area.

6Code (Python)

Optimal: row histograms + monotonic stack
class Solution:
    def maximalRectangle(self, matrix):
        rows, cols = len(matrix), len(matrix[0])
        heights = [0] * cols          # 1s stacked up to the current row
        max_area = 0
        for i in range(rows):
            for j in range(cols):
                if matrix[i][j] == "1":
                    heights[j] += 1   # one more block on this column
                else:
                    heights[j] = 0    # a 0 breaks the column
            max_area = max(max_area, self.largestRectangleArea(heights))
        return max_area

    def largestRectangleArea(self, heights):     # problem 6, unchanged
        n = len(heights)
        max_area = 0
        st = []
        for i in range(n + 1):
            h = 0 if i == n else heights[i]
            while st and h < heights[st[-1]]:
                height = heights[st.pop()]
                width = i if not st else i - st[-1] - 1
                max_area = max(max_area, height * width)
            st.append(i)
        return max_area

7Code line by line

linewhat it means
heights = [0] * colsOne bar per column. We reuse the same list for every row and just update it.
for i in range(rows):Each row becomes the floor once.
if matrix[i][j] == "1": heights[j] += 1The column's tower grows by one block.
else: heights[j] = 0A 0 on the floor: nothing in this column reaches the floor.
self.largestRectangleArea(heights)Called after the inner loop, once per row: the best rectangle whose bottom is this row.
max_area = max(...)Keep the best over all floors.
largestRectangleAreaExactly problem 6: increasing stack of indexes, pop on a shorter bar, width i or i − top − 1, fake 0 at the end.

8Dry run: the stack on floor row 2, heights [3, 1, 3, 2, 2]

Previous smaller and next smaller for every bar

kheightPSENSEwidth = NSE − PSE − 1area
03−1113
11−15 (none)55
231313
3215 (none; bar 4 is equal, not smaller)36
421 (bar 3 is equal, not smaller)5 (none)36

The stack, step by step

ihwhat we pop (and why)pushstack afterbest in this row
03nothing (empty)0[0(3)]0
111 < 3 → pop 0(3), stack empty → width 1 → 31[1(1)]3
233 > 1, nothing2[1(1), 2(3)]3
322 < 3 → pop 2(3), left 1 → width 3 − 1 − 1 = 1 → 3. 2 < 1? no3[1(1), 3(2)]3
422 < 2? no (equal stays)4[1(1), 3(2), 4(2)]3
50 (fake)pop 4(2): left 3 → width 5 − 3 − 1 = 1 → 2 (too short, its twin covers it).
pop 3(2): left 1 → width 5 − 1 − 1 = 3 → 6.
pop 1(1): empty → width 5 → 5
5[5(0)]6
after i = 3
1(1)3(2)
before the fake 0
1(1)3(2)4(2)
after popping 4(2)
1(1)3(2) → 6

The whole matrix, row by row

row 010100helper → 1, max = 1
row 120211helper → 3, max = 3
row 231322helper → 6, max = 6
row 340030helper → 4, max stays 6 ✓

9Complexity & remember

Pencil correction: in the video the complexity is given as R × C × C time and R × C space. That's an over-count. The helper is called once per row (after the inner loop, not inside it), so the time is R × (C + C) = O(R·C). And the heights list has C cells and is overwritten for each row, so the extra space is O(C). Even the bigger estimate (8 × 10⁶) passes easily, which is why the submission was fast.
Remember Maximal RectangleEach row is the floor of a histogram. 1 → height + 1, 0 → height = 0. Run problem 6 on the heights after each row; take the max.

Part C · Revision page

Brute forceRow histograms + stack
ideaevery (top-left, bottom-right) pair, check all cellseach row is a histogram floor; reuse problem 6
area(r2 − r1 + 1) × (c2 − c1 + 1)height × width from the stack
timeO((R·C)³) → TLEO(R·C)
spaceO(1)O(C)
ideafits?why
graph BFS/DFS (Max Area of Island)nofinds connected blobs of any shape; checking for rectangles needs extra work and is slower
row histograms + monotonic stackyesevery rectangle has a bottom row; that row's histogram contains it
If you remember only 5 lines 1. A rectangle is fixed by two corners; the brute force checks every pair → far too slow.
2. Look row by row: each row is the floor of a histogram.
3. heights[j] = heights[j] + 1 on a "1", 0 on a "0".
4. After each row, run Largest Rectangle in Histogram on heights.
5. The answer is the max over all rows; time O(R·C).
Mistakes to avoid ✗ not resetting the height to 0 on a "0"
✗ comparing with the number 1 when the cells are the strings "1"/"0"
✗ calling the helper inside the inner column loop (wasted work)
✗ in the helper: using the popped index as the height, or forgetting the fake 0
✗ forgetting "+1" in the brute-force area (cells, not points)
test it yourself (paste under either solution)
m = [["1","0","1","0","0"],
     ["1","0","1","1","1"],
     ["1","1","1","1","1"],
     ["1","0","0","1","0"]]
s = Solution()
print(s.maximalRectangle(m))                 # 6
print(s.maximalRectangle([["0"]]))           # 0
print(s.maximalRectangle([["1"]]))           # 1
print(s.maximalRectangle([["1"],["0"],["1"]]))   # 1
print(s.maximalRectangle([["1","1"],["1","1"]]))  # 4

Based on this video: Maximal Rectangle | Monotonic Stack