Problem: 42. Trapping Rain Water (LeetCode Top 100 Liked, Hard).
All four Python solutions pass ruff formatting and lint checks, plus fixed test cases and 3,000 randomized cross-checks.
Table of contents
Open Table of contents
- The problem
- What it’s really asking
- The key observation: how much water sits above one bar?
- Solution 1: brute force (correct first, fast later)
- Solution 2: precomputed maxima (dynamic programming)
- Solution 3: two pointers (down to O(1) space)
- Solution 4: monotonic stack (count water by layers)
- Code conventions
- Comparing the four solutions
- Common pitfalls
- In one sentence
- Appendix: full test code
The problem
You get an array of non-negative integers. Each number is the height of a bar that is 1 unit wide, and the bars stand side by side. After it rains, water collects in the dips between bars. The task is to compute the total amount of trapped water. The full statement is on the LeetCode page linked above.
Example 1
Input: height = [0,1,0,2,1,0,1,3,2,1,2,1]
Output: 6
(The original problem shows this as an elevation map with the 6 units of water drawn in blue.)
Example 2
Input: height = [4,2,0,3,2,5]
Output: 9
Constraints
n == height.length1 <= n <= 2 * 10^40 <= height[i] <= 10^5
What it’s really asking
Take Example 2, height = [4, 2, 0, 3, 2, 5], and draw it (█ is a bar, ~ is trapped water):
5 | █
4 | █ ~ ~ ~ ~ █
3 | █ ~ ~ █ ~ █
2 | █ █ ~ █ █ █
1 | █ █ ~ █ █ █
+------------
0 1 2 3 4 5
Count the ~ cells: 2 above index 1, 4 above index 2, 1 above index 3, and 2 above index 4, for a total of 9.
You can draw Example 1 the same way and count 6 cells. Give it a try.
What makes this problem hard is that the water looks like irregular blobs, and it’s not obvious where to start. The trick is to find one key observation that all four solutions build on.
The key observation: how much water sits above one bar?
Forget the shape of the whole pool and look at a single bar. How much water can sit on top of it?
Imagine standing on the bar at index i. For water to stay there, it needs a wall on both sides:
- Looking left, the tallest bar is
left_max. - Looking right, the tallest bar is
right_max.
The water level is set by the shorter of the two walls, because water spills over the lower side (the same idea as the weakest stave in a barrel). So:
water above index i = min(left_max, right_max) - height[i]
Here left_max and right_max include bar i itself. That way, when bar i is the tallest point, min(...) equals height[i], the result is 0, and it can never go negative.
Checking the formula against the example:
| Index i | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| height | 4 | 2 | 0 | 3 | 2 | 5 |
| left_max | 4 | 4 | 4 | 4 | 4 | 5 |
| right_max | 5 | 5 | 5 | 5 | 5 | 5 |
| min | 4 | 4 | 4 | 4 | 4 | 5 |
| water | 0 | 2 | 4 | 1 | 2 | 0 |
The total is 9, matching the drawing.
The problem is now: how do we quickly find left_max and right_max for every position? The four solutions below are four answers to that question.
Solution 1: brute force (correct first, fast later)
The most direct approach: for each position, scan left for the maximum, then scan right for the maximum.
class Solution:
def trap(self, height: list[int]) -> int:
water = 0
for i, bar in enumerate(height):
# Both maxima include the current bar, so the water is never negative
left_max = max(height[: i + 1])
right_max = max(height[i:])
water += min(left_max, right_max) - bar
return water
- Time: O(n²). Every position scans the whole array.
- Space: O(1), not counting the temporary lists created by slicing.
This times out on LeetCode, but it’s still valuable: it maps directly to the formula, and every later optimization just removes repeated work from it. It’s also a handy reference answer for cross-checking the other solutions.
Solution 2: precomputed maxima (dynamic programming)
Why is brute force slow? Each position rescans the array, yet neighboring positions have almost the same left_max:
left_max[i] = max(left_max[i - 1], height[i])
In other words, one pass from left to right computes every left_max. right_max works the same way, in one pass from right to left.
Python’s standard library already has a tool for this kind of running computation: itertools.accumulate(height, max) yields height[0], then max(height[0], height[1]), then max(previous result, height[2]), and so on, which is exactly the prefix maximum at each position. Do the same on the reversed array and reverse the result, and you get the suffix maximum.
from itertools import accumulate
class Solution:
def trap(self, height: list[int]) -> int:
# left_max[i]: the tallest bar in height[0..i]
left_max = list(accumulate(height, max))
# right_max[i]: the tallest bar in height[i..n-1]
right_max = list(accumulate(reversed(height), max))[::-1]
return sum(
min(left, right) - bar
for left, right, bar in zip(left_max, right_max, height)
)
- Time: O(n). One pass each for the prefix maxima, the suffix maxima, and the sum.
- Space: O(n). Two helper lists.
This is the solution to learn first: the idea is straightforward, it matches the formula line for line, and it’s hard to get wrong. It’s also safe on an empty array: accumulate and zip produce empty sequences, so sum returns 0 without a special case. In an interview, writing this first and then optimizing to two pointers is a natural progression.
Solution 3: two pointers (down to O(1) space)
Solution 2 stores two lists. Can we avoid that?
Look at the formula again. The water depends only on min(left_max, right_max), the shorter of the two. We don’t need both values exactly; we only need to know which side is shorter.
Move two pointers, left and right, inward from both ends, and keep track of:
left_max: the tallest bar at or to the left ofleft;right_max: the tallest bar at or to the right ofright.
At each step, compare left_max and right_max:
- If
left_max < right_max: for positionleft, the tallest bar on its left is exactlyleft_max. The true tallest bar on its right is at leastright_max, becauseright_maxcovers only some of the bars on the right, and the unseen bars in between can only make it taller. So the right wall is definitely taller thanleft_max, the water level atleftis set byleft_max, and we can add its water and moveleftright. - Otherwise: by symmetry, the water level at
rightis set byright_max. Add its water and moverightleft.
In one sentence: whichever side has the shorter maximum already knows its water level, so settle that side first.
class Solution:
def trap(self, height: list[int]) -> int:
left, right = 0, len(height) - 1
left_max = right_max = 0
water = 0
while left < right:
left_max = max(left_max, height[left])
right_max = max(right_max, height[right])
if left_max < right_max:
# The left maximum is shorter: the level at left is settled
water += left_max - height[left]
left += 1
else:
# The right maximum is shorter (or equal): the level at right is settled
water += right_max - height[right]
right -= 1
return water
Walking through [4, 2, 0, 3, 2, 5]:
| Step | left | right | left_max | right_max | Settle | Water added | Total |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 5 | 4 | 5 | left, index 0 | 4 - 4 = 0 | 0 |
| 2 | 1 | 5 | 4 | 5 | left, index 1 | 4 - 2 = 2 | 2 |
| 3 | 2 | 5 | 4 | 5 | left, index 2 | 4 - 0 = 4 | 6 |
| 4 | 3 | 5 | 4 | 5 | left, index 3 | 4 - 3 = 1 | 7 |
| 5 | 4 | 5 | 4 | 5 | left, index 4 | 4 - 2 = 2 | 9 |
left reaches 5 and meets right, the loop ends, and the answer is 9.
Why left < right? Doesn’t the meeting point need to be counted? The pointers always meet at one of the tallest bars overall: the shorter side always moves first, so the tallest bar holds its position until the end. Nothing sits on top of the tallest bar, so there’s nothing to add.
- Time: O(n). Each position is visited once.
- Space: O(1).
This is the optimal solution. The hard part is convincing yourself that looking only at the shorter side is enough, and the bold sentence above is the whole argument.
Solution 4: monotonic stack (count water by layers)
This solution uses a monotonic stack. If that’s new to you, read Monotonic Stacks from Scratch: Starting with the Next Greater Element first, then come back. The rest of this section will be much easier.
A different cut: slice the water into horizontal strips
The first three solutions cut the water vertically: one bar at a time, counting the cells above each. The monotonic stack cuts it horizontally into strips and counts one strip at a time.
Take [4, 2, 0, 3, 2, 5] again and split the 9 cells of water into 4 horizontal strips, labeled A, B, C, and D:
5 | █
4 | █ D D D D █
3 | █ B B █ C █
2 | █ █ A █ █ █
1 | █ █ A █ █ █
+------------
0 1 2 3 4 5
| Strip | Where | Cells |
|---|---|---|
| A | index 2, rows 1 and 2 | 2 |
| B | indices 1 and 2, row 3 | 2 |
| C | index 4, row 3 | 1 |
| D | indices 1 to 4, row 4 | 4 |
That adds up to 9, the same answer as before.
Every strip is enclosed by three bars
Look closely and every strip is enclosed by three bars: a bottom underneath, a left wall, and a right wall.
Take A: the bottom is index 2 (height 0), the left wall is index 1 (height 2), and the right wall is index 3 (height 3).
Once you know those three bars, you can compute the strip:
width = right wall index - left wall index - 1
depth = min(left wall height, right wall height) - bottom height
cells = width × depth
All four strips:
| Strip | Bottom | Left wall | Right wall | Width | Depth | Cells |
|---|---|---|---|---|---|---|
| A | index 2 (height 0) | index 1 (height 2) | index 3 (height 3) | 3 - 1 - 1 = 1 | min(2, 3) - 0 = 2 | 2 |
| B | index 1 (height 2) | index 0 (height 4) | index 3 (height 3) | 3 - 0 - 1 = 2 | min(4, 3) - 2 = 1 | 2 |
| C | index 4 (height 2) | index 3 (height 3) | index 5 (height 5) | 5 - 3 - 1 = 1 | min(3, 5) - 2 = 1 | 1 |
| D | index 3 (height 3) | index 0 (height 4) | index 5 (height 5) | 5 - 0 - 1 = 4 | min(4, 5) - 3 = 1 | 4 |
Notice that B’s bottom is index 1 (height 2), not the lower index 2. The part below B was already counted in A, so B only covers the layer between heights 2 and 3. “Bottom” here means the bar that the floor of this strip rests on.
How do we find the three bars? With a monotonic stack
Look at the three bars of each strip again, and a pattern appears:
- Right wall: the first bar to the right of the bottom that is taller than it;
- Left wall: the first bar to the left of the bottom that is not shorter than it.
For example, A’s bottom is index 2 (height 0). Looking right, the first bar taller than 0 is index 3. Looking left, the first bar not shorter than 0 is index 1.
Those two “first bars” are exactly what the end of Monotonic Stacks from Scratch describes: when an element is popped, the element that popped it is the first greater one to its right, and the new top after the pop is the first one to its left that isn’t smaller.
So the method is: push the bars one at a time, from left to right, onto a monotonically decreasing stack. Whenever the incoming bar is taller than the top:
- Pop the top. It’s the bottom.
- The new top after the pop is the left wall.
- The incoming bar is the right wall.
- With all three bars known, compute the strip.
Why does the stack decrease? For the same reason as the waiting area in the monotonic stack post: the stack holds bars that haven’t found a right wall yet. If a bar had a shorter bar to its left on the stack, that shorter bar would already have found its right wall and been popped.
Step by step
The stack is written left to right, with the bottom on the left and the top on the right, and each entry as index(height).
Step 1: index 0 (height 4) arrives
- The stack is empty, so push it.
- Stack:
[0(4)]
Step 2: index 1 (height 2) arrives
- 2 isn’t taller than the top 0(4), so push it.
- Stack:
[0(4), 1(2)]
Step 3: index 2 (height 0) arrives
- 0 isn’t taller than the top 1(2), so push it.
- Stack:
[0(4), 1(2), 2(0)]
So far the bars keep getting shorter, none of them has a right wall, and no water can be counted yet. They all wait on the stack.
Step 4: index 3 (height 3) arrives
- 3 is taller than the top 2(0):
- Pop 2(0). It’s the bottom.
- The new top 1(2) is the left wall, and index 3 is the right wall.
- Strip A: width 3 - 1 - 1 = 1, depth min(2, 3) - 0 = 2, +2.
- 3 is also taller than the new top 1(2):
- Pop 1(2). It’s the bottom.
- The new top 0(4) is the left wall, and index 3 is the right wall.
- Strip B: width 3 - 0 - 1 = 2, depth min(4, 3) - 2 = 1, +2.
- 3 isn’t taller than the top 0(4). Stop and push index 3.
- Stack:
[0(4), 3(3)], total 4.
The water counted so far (. marks water not counted yet):
5 | █
4 | █ . . . . █
3 | █ B B █ . █
2 | █ █ A █ █ █
1 | █ █ A █ █ █
+------------
0 1 2 3 4 5
Step 5: index 4 (height 2) arrives
- 2 isn’t taller than the top 3(3), so push it.
- Stack:
[0(4), 3(3), 4(2)], total still 4.
Step 6: index 5 (height 5) arrives
- 5 is taller than the top 4(2):
- Pop 4(2). It’s the bottom.
- The new top 3(3) is the left wall, and index 5 is the right wall.
- Strip C: width 5 - 3 - 1 = 1, depth min(3, 5) - 2 = 1, +1.
- 5 is also taller than the new top 3(3):
- Pop 3(3). It’s the bottom.
- The new top 0(4) is the left wall, and index 5 is the right wall.
- Strip D: width 5 - 0 - 1 = 4, depth min(4, 5) - 3 = 1, +4.
- 5 is also taller than the new top 0(4):
- Pop 0(4). It’s the bottom.
- But the stack is now empty, so there’s no left wall. With nothing on the left to hold it, water would run off, so there’s nothing to count. Stop.
- Push index 5.
- Stack:
[5(5)], total 9.
Every bar has been processed, and all four strips are counted:
5 | █
4 | █ D D D D █
3 | █ B B █ C █
2 | █ █ A █ █ █
1 | █ █ A █ █ █
+------------
0 1 2 3 4 5
The answer is 9.
In code
class Solution:
def trap(self, height: list[int]) -> int:
stack: list[int] = [] # Indices whose heights decrease from bottom to top
water = 0
for right, bar in enumerate(height):
# Taller than the top: the top becomes a dip bottom, settle one layer
while stack and bar > height[stack[-1]]:
bottom = stack.pop()
if not stack:
break # No left wall, so the water would run off
left = stack[-1]
width = right - left - 1
depth = min(height[left], bar) - height[bottom]
water += width * depth
stack.append(right)
return water
Matching the code to the steps above:
| Code | In the steps |
|---|---|
for right, bar in enumerate(height) | index right (height bar) arrives; it’s a potential right wall |
while stack and bar > height[stack[-1]] | the incoming bar is taller than the top, so a strip can be settled |
bottom = stack.pop() | pop the top; it’s the bottom |
if not stack: break | the stack is empty after the pop, so there’s no left wall; stop (the last pop in step 6) |
left = stack[-1] | the new top is the left wall |
width, depth, water += ... | compute this strip |
stack.append(right) | push the incoming bar to wait for its own right wall |
- Time: O(n). Each index is pushed and popped at most once.
- Space: O(n). In the worst case (bars that only get shorter), every index is on the stack.
A monotonic stack isn’t better than two pointers here, but the idea transfers. Finding the first greater (or smaller) element on each side is the standard use of a monotonic stack, and 739. Daily Temperatures and 84. Largest Rectangle in Histogram follow the same pattern. The latter is also on the Top 100 Liked list.
Code conventions
The four solutions follow the same conventions, so you can read them side by side:
- Names say what they mean: the total is always
water, a single bar’s height isbar, the maxima on each side areleft_max/right_max, and the dip bottom, left wall, and right wall in the stack solution arebottom,left, andright, withwidthanddepthfor the layer. No opaque names likeh,res, orans. enumerategives the index and the value together, instead offor i in range(len(height))followed byheight[i].- Blank lines separate the logic: setup, main loop, and return are separated, and inside a loop, updating state and settling water are kept apart.
- Comments explain why, such as why the maxima include the current bar or why an empty stack means stop, rather than restating the code.
- Type hints use the built-in Python 3.9+ syntax
list[int]instead of importingListfromtyping. - All code passes
ruff formatandruff check.
Comparing the four solutions
| Solution | Time | Space | Idea | When to use it |
|---|---|---|---|---|
| Brute force | O(n²) | O(1) | By column, scan for both maxima each time | Understanding the problem, cross-checking |
| Precomputed maxima | O(n) | O(n) | By column, compute both maxima up front | Learn this first, hard to get wrong |
| Two pointers | O(n) | O(1) | By column, settle only the shorter side | Optimal, great in interviews |
| Monotonic stack | O(n) | O(n) | By layer, settle a layer whenever a dip appears | Practicing stacks, transfers to other problems |
Common pitfalls
- Empty arrays: if you initialize prefix lists by index (for example
left_max[0] = height[0]), an empty array goes out of bounds and needs a check. All four versions here handle empty input naturally: the brute-force and stack loops never run, the two-pointer condition0 < -1is false, andaccumulateyields nothing. - Negative water:
left_maxandright_maxmust include the current bar, somin(...) - height[i]is at least 0. - An empty stack after popping: the dip has no left wall and the water runs off, so
breakimmediately instead of computing anything. >or>=in the stack comparison: use>. An equal-height bar is simply pushed; the layer between it and the previous equal bar would have depth 0 and doesn’t change the result.
In one sentence
The water above a position depends on the shorter of the tallest bars on its left and right. Brute force, precomputed maxima, and two pointers are different ways to find those two maxima; the monotonic stack takes a different angle and adds up the water one layer at a time.
Appendix: full test code
The four solutions rewritten as plain functions. The script runs the fixed cases first, then cross-checks 3,000 random inputs against the brute-force solution. The random numbers come from a dedicated random.Random(seed) instance, so results are reproducible and the global random state is left alone:
"""Cross-check the solutions, using brute force as the reference."""
import random
from collections.abc import Callable
from itertools import accumulate
def trap_brute_force(height: list[int]) -> int:
water = 0
for i, bar in enumerate(height):
left_max = max(height[: i + 1])
right_max = max(height[i:])
water += min(left_max, right_max) - bar
return water
def trap_prefix_max(height: list[int]) -> int:
left_max = list(accumulate(height, max))
right_max = list(accumulate(reversed(height), max))[::-1]
return sum(
min(left, right) - bar for left, right, bar in zip(left_max, right_max, height)
)
def trap_two_pointers(height: list[int]) -> int:
left, right = 0, len(height) - 1
left_max = right_max = 0
water = 0
while left < right:
left_max = max(left_max, height[left])
right_max = max(right_max, height[right])
if left_max < right_max:
water += left_max - height[left]
left += 1
else:
water += right_max - height[right]
right -= 1
return water
def trap_monotonic_stack(height: list[int]) -> int:
stack: list[int] = []
water = 0
for right, bar in enumerate(height):
while stack and bar > height[stack[-1]]:
bottom = stack.pop()
if not stack:
break
left = stack[-1]
width = right - left - 1
depth = min(height[left], bar) - height[bottom]
water += width * depth
stack.append(right)
return water
SOLUTIONS: list[Callable[[list[int]], int]] = [
trap_brute_force,
trap_prefix_max,
trap_two_pointers,
trap_monotonic_stack,
]
FIXED_CASES: list[tuple[list[int], int]] = [
([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1], 6),
([4, 2, 0, 3, 2, 5], 9),
([], 0),
([5], 0),
([1, 2, 3], 0),
([3, 2, 1], 0),
([3, 0, 3], 3),
([5, 1, 1, 1, 5], 12),
]
def check(solve: Callable[[list[int]], int], height: list[int], expected: int) -> None:
actual = solve(height)
assert actual == expected, (
f"{solve.__name__}({height}) returned {actual}, expected {expected}"
)
def check_fixed_cases() -> None:
for height, expected in FIXED_CASES:
for solve in SOLUTIONS:
check(solve, height, expected)
def check_random_cases(rounds: int = 3000, seed: int = 0) -> None:
rng = random.Random(seed)
for _ in range(rounds):
height = [rng.randint(0, 8) for _ in range(rng.randint(0, 25))]
expected = trap_brute_force(height)
for solve in SOLUTIONS[1:]:
check(solve, height, expected)
def main() -> None:
check_fixed_cases()
check_random_cases()
print("all passed")
if __name__ == "__main__":
main()