DSA sheet · Stack · Monotonic stack pattern

Largest Rectangle in Histogram

LeetCode marks this one Hard, but the teacher's promise is that it isn't, once you see the idea. She first builds a brute force (fix a start bar, stretch to the right, keep the minimum height), shows why it gets TLE, explains why two pointers and sliding window don't fit, and then solves it with an increasing monotonic stack plus a zero-height bar at the end.

Why it matters: the area of the best rectangle "standing on" a bar is decided by the nearest smaller bar on its left and the nearest smaller bar on its right. That one fact powers this problem and Maximal Rectangle (problem 9), which reuses this code row by row.

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. The last plate put on is the first one taken off: LIFO (Last In, First Out).

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

We draw a stack left = bottom, right = top. Both append and pop() are O(1).

Safety ruleNever pop() or read st[-1] on an empty stack (IndexError). Write while st and …, and check if not st before peeking.

Monotonic stack: what it is and what it remembers

A monotonic increasing stack keeps its items in increasing order from bottom to top. When a new item comes that is smaller than the top, we pop the top (and maybe more) until the order is fine again, then push the new item.

The useful part is the moment of popping. When item X is popped because a smaller item Y arrived:

So at the moment of the pop, we know both neighbours that "limit" X. That's exactly what this problem needs.

termmeaning
previous smaller (PSE) of bar kthe nearest bar on the left of k that is shorter than k; −1 if none
next smaller (NSE) of bar kthe nearest bar on the right of k that is shorter than k; n if none

We store indexes in the stack, not heights. From an index we can always read the height (heights[idx]), and the index is what we need for the width.


Part A · Brute force: fix the start, stretch right

LeetCode 84

1The question in simple words

You get heights: the heights of bars in a histogram (a bar chart where the bars stand next to each other with no gaps). Every bar is 1 unit wide. Find the area of the largest rectangle you can draw inside the bars.

 6 |          ██
 5 |       ██ ██
 4 |       ██ ██
 3 |       ██ ██    ██
 2 | ██    ██ ██ ██ ██
 1 | ██ ██ ██ ██ ██ ██
   +------------------
      2  1  5  6  2  3   height
      0  1  2  3  4  5   index

A rectangle covering several bars can only be as tall as the shortest of them, because it must fit inside every bar. Bars 5 and 6 together: height min(5, 6) = 5, width 2, area 10. That's the answer here.

 6 |          ██
 5 |       ▓▓ ▓▓
 4 |       ▓▓ ▓▓
 3 |       ▓▓ ▓▓    ██
 2 | ██    ▓▓ ▓▓ ██ ██
 1 | ██ ██ ▓▓ ▓▓ ██ ██          ▓▓ = the best rectangle, 5 × 2 = 10

Area of a rectangle = height × width. Width of bars from index i to index j (both included) = j − i + 1.

2What the constraints tell us

3Intuition: try every starting bar

Stand on a bar and say "my rectangle starts here". Now walk to the right one bar at a time. As you go, the rectangle gets wider by 1 each step, but its height can only stay the same or get lower: it's the minimum of all bars seen so far. At each step, compute the area and keep the best one.

The teacher points out you can do it in either direction: start at a bar and stretch right, or stand at a bar and look back to the left. Both try every pair (start, end). She codes the "stretch right" version.

4Building the logic from her walk-through

She walks through [2, 1, 5, 6, 2, 3] by hand:

Final answer: 10. Notice the two things we keep while stretching: the running minimum height and the width j − i + 1.

Doubt: why does j start at i and not at i + 1?
→ The single bar by itself (width 1) is also a valid rectangle, e.g. the lone 6 gives 6. Starting j at i includes it. And we never need j < i, because those rectangles were already tried when an earlier bar was the start.

5Approach steps

  1. max_area = 0.
  2. For every start i: set h = heights[i].
  3. For every end j from i to n − 1: h = min(h, heights[j]); width = j − i + 1; area = h × width; update max_area.
  4. Return max_area.

6Code (Python)

Brute force: every start, stretch right
class Solution:
    def largestRectangleArea(self, heights):
        n = len(heights)
        max_area = 0
        for i in range(n):                 # the rectangle starts at bar i
            h = heights[i]                 # running minimum height
            for j in range(i, n):          # ...and ends at bar j
                h = min(h, heights[j])     # must fit under every bar
                width = j - i + 1
                max_area = max(max_area, h * width)
        return max_area

7Code line by line

linewhat it means
for i in range(n):Try each bar as the left edge.
h = heights[i]Start the running minimum at the first bar's height.
for j in range(i, n):Move the right edge one bar at a time.
h = min(h, heights[j])The new bar may be shorter; then the whole rectangle has to shrink to it.
width = j - i + 1Number of bars from i to j, both included.
max_area = max(...)Keep the best area seen.

8Dry run: [2, 1, 5, 6, 2, 3]

Each row is one start i. The cells show the area for j = i, i+1, … (running min × width).

i (height)areas as j moves rightbest for this imax_area
0 (2)2×1=2, 1×2=2, 1×3=3, 1×4=4, 1×5=5, 1×6=666
1 (1)1, 2, 3, 4, 556
2 (5)5×1=5, 5×2=10, 2×3=6, 2×4=81010
3 (6)6, 2×2=4, 2×3=6610
4 (2)2, 2×2=4410
5 (3)3310 ✓

9Complexity & remember

Remember the brute forceFix the left edge, stretch right, keep a running min; area = min × (j − i + 1). Correct but O(n²).

Part A½ · Why not two pointers or sliding window?

When optimising, the teacher runs through the patterns she knows: two pointers, sliding window, stack. Two pointers looks tempting because Container With Most Water and Trapping Rain Water also deal with bars and areas.

Why two pointers fails

In Trapping Rain Water, the water above a bar depends on the tallest bar to its left and right, and the smaller of those decides the level. Once we've used a pointer's bar, we move past it and never need it again. Two pointers works because we can safely throw bars away.

Here we can't. Even a short bar can make the biggest rectangle if it's wide enough. Her example: picture 1, 2, 1, 2, 1, 2, … repeated over many bars. The two tall bars 5 and 6 give only 10, but a height-1 rectangle across 13 bars gives 13. So every bar might be the height of the answer, and none can be thrown away early.

Why sliding window fails

A sliding window needs a rule like "grow the window while it's valid, shrink when it breaks". Here there's no such rule: adding a bar can make the area go up or down, and shrinking the window can too. Nothing tells us which end to move.

Why a stack fits

The area always depends on the minimum height, so we need "the nearest smaller bar" on each side. And her stack signal: whenever you're at a point and keep going back (or forward) to find a smaller or larger value, keep those values in a stack instead. That turns O(n²) into O(n).


Part B · Optimal: increasing monotonic stack

1The question (same)

Same input and output as Part A. Only the method changes.

2What the constraints tell us

n up to 10⁵ → we need O(n). Each bar should be pushed once and popped once.

3Intuition: each bar's own rectangle

Flip the brute force around. Instead of "where does the rectangle start?", ask: "if bar k is the shortest bar in the rectangle, how wide can it be?"

It can spread left and right until it hits a bar shorter than itself. Taller bars are fine (the rectangle fits under them). So:

area(k) = heights[k] × (NSE(k) − PSE(k) − 1)

where PSE = index of the previous smaller bar (−1 if none) and NSE = index of the next smaller bar (n if none). The answer is the biggest area(k) over all bars, because the best rectangle has some shortest bar, and that bar's area(k) is at least as big.

PSE and NSE for every bar of [2, 1, 5, 6, 2, 3]

kheightprev smaller (PSE)next smaller (NSE)width = NSE − PSE − 1area
02−1 (none)1 (height 1)1 − (−1) − 1 = 12
11−1 (none)6 (none)6 − (−1) − 1 = 66
251 (height 1)4 (height 2)4 − 1 − 1 = 210
362 (height 5)4 (height 2)4 − 2 − 1 = 16
421 (height 1)6 (none)6 − 1 − 1 = 48
534 (height 2)6 (none)6 − 4 − 1 = 13

The biggest is 10 ✓. The stack finds every PSE and NSE in a single pass, at the moment it pops.

4Building the stack rules, the way the teacher does

Rule 1: keep pushing while bars get taller

Heights 2, 3, 4… keep going up: push them all. We can't finish any of them yet, because we don't know where they end on the right.

Rule 2: a shorter bar arrives → pop and compute

When bar j is shorter than the top, the top bar now knows both its limits:

The bars strictly between i and j are all at least as tall as the popped bar. The width is the count of bars between i and j, with both ends excluded. The teacher derives it from the usual j − i + 1 and then subtracts 2 because neither i nor j is part of the rectangle: j − i + 1 − 2 = j − i − 1.

Doubt: after popping bar 6, I pop bar 5. But 6 is no longer in the stack. Can 5's rectangle still cover 6's spot?
→ Yes. 6 was above 5 in the stack, so it was taller. Anything that was popped from between i and j was taller than the bar we're popping now (that's why it got pushed on top of it). So the 5-high rectangle fits under all of them. The formula j − i − 1 counts those spots even though they're gone from the stack. Width 4 − 1 − 1 = 2, area 5 × 2 = 10.

Rule 3: the stack is empty after the pop → there's no left smaller

Her example: index 0 (height 2) is popped when 1 arrives at j = 1. There's nothing under it. No smaller bar on the left means the rectangle reaches all the way to index 0, so the width is just j (bars 0 … j−1). Here width 1, area 2.

Doubt: is "width = j" really the same formula?
→ Yes. Pretend the left smaller is at index −1 (an invisible wall before the array). Then j − (−1) − 1 = j. Same thing.

Why popping is safe: the stack throws away bars that can't help any more

Once 1 arrives after 2, the 2 can never be the height of a rectangle that reaches further right: the 1 is in the way. Its rectangle is already final, so we compute it and drop it. In the brute force we kept re-checking such bars; the stack drops them right away. That's where the speed comes from. After popping, the stack still goes up from bottom to top: an increasing monotonic stack.

Rule 4: bars left at the end → add a zero-height bar

After the last real bar, the stack may still hold bars (in the example: heights 1, 2, 3). Nobody smaller came to pop them. The teacher's trick: pretend there is one extra bar of height 0 at index n. Every real bar is ≥ 0, and it's popped only when something strictly smaller arrives… a 0 is smaller than every positive bar, so it pops them all and computes their areas with j = n.

Doubt: what about bars of height 0 that are already in the array, or equal heights?
→ We pop only when the new bar is strictly smaller (h < heights[top]). An equal bar is pushed on top of its twin. When a smaller bar finally comes, the later twin is popped first and its width is too short (the twin under it acts as its "left smaller"). But then the earlier twin is popped with the same right edge and the true left smaller, so its area covers the whole stretch. The maximum is still correct. A real 0 bar gives area 0, which never matters. The tests check runs of equal bars and zeros.
Doubt: must I really add 0 to the array?
→ Either way works. The teacher doesn't change the array; she loops i from 0 to n (inclusive) and uses height 0 when i == n. You could also do heights + [0].

5Approach steps

  1. Empty stack of indexes; max_area = 0.
  2. For i from 0 to n (n means the fake 0 bar): h = 0 if i == n else heights[i].
  3. While the stack isn't empty and h < height of the top: pop the top → its height; width = i if the stack is now empty, else i − (new top) − 1; update max_area.
  4. Push i.
  5. Return max_area.

6Code (Python)

Optimal: increasing monotonic stack
class Solution:
    def largestRectangleArea(self, 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]]:   # bar i is the right smaller
                height = heights[st.pop()]       # the bar we finish now
                width = i if not st else i - st[-1] - 1
                max_area = max(max_area, height * width)
            st.append(i)
        return max_area
Her bug on screenHer first run gave a wrong answer: she wrote height = stack.pop(), which is the index, not the height. Fix: heights[st.pop()]. Since the stack stores indexes, every height must be read through heights[...].

7Code line by line

linewhat it means
st = []Indexes of bars whose rectangle isn't finished yet. Their heights go up from bottom to top.
for i in range(n + 1):One extra round for the fake bar at index n.
h = 0 if i == n else heights[i]The fake bar is 0, so it pops everything left.
while st and h < heights[st[-1]]:The current bar is shorter than the top → the top's right limit is i. while, because several bars may end here.
height = heights[st.pop()]Remove the top and read its height.
width = i if not st else i - st[-1] - 1No left smaller → it reaches index 0, width i. Otherwise the left smaller is the new top: count the bars strictly between.
max_area = max(...)Keep the best area.
st.append(i)Bar i waits for its own right smaller. (The fake bar is pushed too, harmlessly.)

8Dry run: [2, 1, 5, 6, 2, 3]

The stack is shown as index(height), left = bottom, right = top.

ihwhat we pop (and why)pushstack aftermax_area
02nothing (empty)0[0(2)]0
111 < 2 → pop 0(2). Stack empty → width = i = 1 → area 21[1(1)]2
255 > 1, nothing2[1(1), 2(5)]2
366 > 5, nothing3[1(1), 2(5), 3(6)]2
422 < 6 → pop 3(6): left = 2, width 4 − 2 − 1 = 1 → 6.
2 < 5 → pop 2(5): left = 1, width 4 − 1 − 1 = 2 → 10.
2 < 1? no, stop
4[1(1), 4(2)]10
533 > 2, nothing5[1(1), 4(2), 5(3)]10
60 (fake)pop 5(3): left 4, width 6 − 4 − 1 = 1 → 3.
pop 4(2): left 1, width 6 − 1 − 1 = 4 → 8.
pop 1(1): empty, width = 6 → 6
6[6(0)]10 ✓
before i = 4
1(1)2(5)3(6)
after i = 4 (6 and 5 popped)
1(1)4(2)
before the fake 0
1(1)4(2)5(3)

The teacher calls the bar 2 at index 4 "magic": when the 0 pops it, its left smaller is index 1 and its right smaller is index 6, so the stack tells us it spans width 4 (indexes 2 to 5) and gives area 8, even though bars 5 and 6 left the stack long ago. And the 1 at the bottom, popped last with an empty stack, spans the whole array: 1 × 6 = 6.

 6 |          ██
 5 |       ██ ██
 4 |       ██ ██
 3 |       ██ ██    ██
 2 | ██    ▓▓ ▓▓ ▓▓ ▓▓          bar 4 (height 2): PSE = 1, NSE = 6
 1 | ██ ██ ▓▓ ▓▓ ▓▓ ▓▓          width 6 − 1 − 1 = 4, area 8

9Complexity & remember

Remember the stack versionPush indexes while heights rise. A shorter bar pops the top: height = heights[popped], right smaller = i, left smaller = new top. width = i if empty else i − top − 1. Loop to n with a fake 0 to flush the stack.

Part C · Revision page

Brute forceMonotonic stack
question askedfor each start, how far right?for each bar as the shortest, how wide?
heightrunning minthe popped bar's height
widthj − i + 1i − top − 1, or i if the stack is empty
end handlingnonefake bar of height 0 at index n
time / spaceO(n²) (TLE for 10⁵) / O(1)O(n) / O(n)
patternfits?why
two pointersnoa short bar can still give the biggest area (wide), so no bar can be thrown away early
sliding windownono grow/shrink rule; area isn't monotonic in the window
monotonic stackyeswe need the nearest smaller on both sides
If you remember only 5 lines 1. A rectangle's height is the shortest bar inside it.
2. Each bar's best rectangle reaches from its previous smaller to its next smaller: area = h × (NSE − PSE − 1).
3. Increasing stack of indexes; a shorter bar pops and finishes the top.
4. Width = i − new top − 1, or i if the stack is empty.
5. Loop i up to n with height 0 at the end to flush the stack.
Mistakes to avoid ✗ using the popped index as the height (her on-screen bug)
✗ forgetting the fake 0, so bars left in the stack are never measured
✗ reading st[-1] for the width when the stack became empty
✗ width j − i + 1 in the stack version (both ends are smaller bars, so they're excluded)
✗ if instead of while for popping
test it yourself (paste under either solution)
s = Solution()
print(s.largestRectangleArea([2, 1, 5, 6, 2, 3]))   # 10
print(s.largestRectangleArea([2, 4]))               # 4
print(s.largestRectangleArea([5]))                  # 5
print(s.largestRectangleArea([1, 2, 3, 4, 5]))      # 9
print(s.largestRectangleArea([3, 3, 3]))            # 9
print(s.largestRectangleArea([2, 0, 2]))            # 2

Based on this video: Largest Rectangle in Histogram | Monotonic Stack