Davide Spataro

Trapping Rain Water

hard · Two Pointers · 30 min read
array two-pointers dp monotonic-stack prefix-max amortized

Introduction

The problem is deceptively simple. A non-negative integer array describes the silhouette of a series of concrete pillars of unit width. Pour water from above and count the volume that stays trapped between them.

There’s an obvious solution that runs in O(n2)O(n^2). There’s also a much faster answer with constant extra memory, and reaching it requires the right move at the right time. This chapter walks the four approaches an interviewer might want to see — a brute force, a dynamic-programming refinement, a two-pointer sweep, and a monotonic-stack reframing — and the lessons each one teaches transfer well beyond this single problem.

The interview signal isn’t writing the brute force. It’s the explicit move from “recompute” to “precompute” to “eliminate.”

If you’re new to histogram problems, read linearly — each approach builds on the previous one. If you’ve coded the optimal answer before, jump to its correctness invariant — that’s where candidates stumble when pressed to prove the algorithm.

What the interviewer is actually testing

  1. Articulating the per-column structure. A candidate who dives straight into code without naming what determines the water level at each column usually writes wrong code. Say the relationship out loud first.
  2. Seeing past the brute force. O(n2)O(n^2) is the obvious solution. The signal is whether you recognize that the helper data the brute force recomputes can be precomputed, and then further eliminated.
  3. A correctness invariant. The optimal approach is short but its correctness is non-obvious. Stating the invariant explicitly is the senior signal.
  4. Knowing the alternatives exist. A monotonic-stack reframing is another lens on the same problem. Mentioning it shows you see histogram problems as a family.

Problem statement

Figure · what the problem looks likeGray bars are the elevation map for the canonical input. Blue cells are the water that pools between the bars after rain. Total: 6 units.
01234567891011
Problem

Given a non-negative integer array HH of size nn representing an elevation map where each bar has width 11, compute how many units of water can be trapped between the bars after rain.

Water can pool above a column whenever there are taller bars on both sides; it pools no higher than the lower of those two surrounding walls (otherwise it would spill over).

Example 1
Input H = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
Output 6
Three separate pockets — between bars 1–3 (1 unit), between bars 3–7 (4 units), between bars 7–10 (1 unit). Total 6.
Example 2
Input H = [3, 2, 0, 0, 4, 4, 1, 2, 5, 4, 0, 2, 1]
Output 14
Larger pockets with a tall 5-column in the middle. Total of 14 units across the array.
Example 3
Input H = [5, 4, 3, 2, 1]
Output 0
Strictly decreasing — every bar's right-hand ceiling is lower than itself. No water can be trapped.

Clarifying questions — and why each one matters

Clarification questions

Q Are the heights always non-negative integers?
A Per the standard problem, yes. If negatives are allowed the formula still works (water is above the base, clamped at zero) but the semantics become less physical; confirm before assuming.
Q Can the input be empty or have fewer than three bars?
A With 0, 1, or 2 bars, no water can be trapped — there isn't a valid 'middle' to pool water above. Guard once at the top.
Q Is the array mutable?
A Doesn't matter for any of the approaches here; all of them treat height as read-only. Worth asking if the interviewer expects in-place tricks.
Q What's the upper bound on n and on H[i]?
A n up to 10⁵ is typical; heights to 10⁴ or so. These bounds permit O(n) solutions comfortably. If n is enormous but the array is sparse, we'd reach for a different representation — ask.
Q Do you want the total water, or a per-column array?
A Default is the scalar total. If the interviewer asks for the per-column profile, use the prefix-max approach — it already computes each column's contribution; just return the array instead of summing.

A mental framework — the column-by-column formula

Visualize the histogram one column at a time. For column i, water can only settle in the region between height[i] and whatever ceiling the surrounding bars create. The ceiling is the lower of the two tallest bars on either side — because water spills over the shorter side.

That gives the one formula that drives the entire chapter:

water(i)=max(0,  min(Li,Ri)H[i])\text{water}(i) = \max\Big(0,\; \min\big(L_i, R_i\big) - H[i]\Big)

where LiL_i and RiR_i are the max of HH in the prefix and suffix including ii. The total is iwater(i)\sum_i \text{water}(i).

Every approach below is a different strategy for computing or avoiding computing LL and RR:

  • Brute force recomputes LiL_i and RiR_i from scratch on every column.
  • Prefix-max arrays precompute both in linear time, then scan.
  • Two-pointer sweep eliminates the RR array by observing that we only need one bound at a time.
  • Monotonic stack reframes the sum as a sequence of rectangles discovered on-the-fly.

Discussion

Approach 01

Brute force — recompute maxes at every column

For each inner column, scan the entire prefix and suffix for the max. Apply the formula. Correct, O(n²), your starting line.

Time O(n²) Space O(1)

The interview narration

Say this out loud:

“For each column i in the interior, the water above it is min(leftMax, rightMax) − height[i], clamped at zero. Brute force: for every i, linear-scan the prefix and suffix to find those two maxes. O(n2)O(n^2) total, O(1)O(1) space. Correct but slow — I’ll use it as a baseline.”

How it works, step by step

The outer loop ranges over interior columns (ii from 11 to n2n-2); the first and last columns can never trap water because one of their sides has no bars to bound the water.

For each ii:

  1. Compute leftMax = max(H[0..i-1]) via std::max_element. Cost: O(i)O(i).
  2. Compute rightMax = max(H[i+1..n-1]) similarly. Cost: O(ni1)O(n - i - 1).
  3. Apply the formula: if min(leftMax, rightMax) > H[i], add the difference to the running total.
  4. Otherwise the current bar is at least as tall as one of the bounds — no water.

The outer loop runs n2n - 2 times; each iteration does O(n)O(n) work for the two max scans. Total: O(n2)O(n^2) time, O(1)O(1) extra space.

The code

brute-force.cpp
#include <algorithm>
#include <vector>
int trap(const std::vector<int>& height) {
const int n = static_cast<int>(height.size());
if (n < 3) return 0;
int total = 0;
for (int i = 1; i < n - 1; ++i) {
const int leftMax = *std::max_element(height.begin(), height.begin() + i);
const int rightMax = *std::max_element(height.begin() + i + 1, height.end());
const int level = std::min(leftMax, rightMax);
if (level > height[i]) total += level - height[i];
}
return total;
}

One loop, two std::max_element calls per iteration. The `std::max(0, ...)` clamp is the formula in one line.

The std::max_element calls return iterators; dereferencing gives the actual max value. This is more idiomatic than hand-rolling two inner loops — interviewers notice.

Why it’s rejected

n=105n = 10^5 gives 101010^{10} operations, which is roughly a minute of wall time. Production code would never ship this, and no interviewer accepts it as the final answer. But it establishes the formula clearly, and every faster approach is a way to avoid recomputing the maxes.

What it gets right

  • The formula is correct and direct. Every other approach builds on this same per-column sum; the brute force just computes it the slowest possible way.
  • O(1) extra space. No auxiliary arrays or stacks. Until the next approach, this is the space-optimal baseline.
  • Easy to verify on small inputs. If you’re ever unsure whether your optimized code is correct, run both on a 10-element input and compare.
Approach 02

Prefix-max arrays — precompute both bounds

Two linear passes compute leftMax and rightMax for every index. A third pass sums the formula. O(n) time, O(n) space.

Time O(n) Space O(n) dpprefix-max

The insight

The O(n2)O(n^2) in the brute force came from recomputing the same maxes over and over. Both leftMax[i] and rightMax[i] have overlapping structure — each one extends the previous:

Li=max(Li1,Hi),Ri=max(Ri+1,Hi)L_i = \max(L_{i-1}, H_i), \quad R_i = \max(R_{i+1}, H_i)

That’s a one-line DP. Two linear passes give us the full arrays; a third pass sums the formula. Everything becomes O(n)O(n) at the cost of two additional arrays of size nn.

How it works, step by step

The figure below shows the two ceilings as dashed lines on top of the bars — green for leftMax, red for rightMax. The water at each column is the gap between the lower of the two dashed lines and the bar itself.

Figure · the two ceilingsFor each column the dashed green line marks leftMax[i] and the dashed red line marks rightMax[i]. Water fills the band between the bar top and the lower of the two dashed lines.
01234567891011
  1. Forward pass for leftMax. Initialize leftMax[0] = height[0]. For ii from 11 to n1n-1, leftMax[i] = max(leftMax[i-1], height[i]).
  2. Backward pass for rightMax. Initialize rightMax[n-1] = height[n-1]. For ii from n2n-2 to 00, rightMax[i] = max(rightMax[i+1], height[i]).
  3. Summing pass. For each ii, add min(leftMax[i], rightMax[i]) − height[i] to the total. (This is always ≥ 0 because min(leftMax[i], rightMax[i]) ≥ height[i] — both ceilings include height[i] in their maximum.)

The code

prefix-max-arrays.cpp
#include <algorithm>
#include <vector>
int trap(const std::vector<int>& height) {
const int n = static_cast<int>(height.size());
if (n < 3) return 0;
std::vector<int> leftMax(n);
std::vector<int> rightMax(n);
leftMax[0] = height[0];
for (int i = 1; i < n; ++i) {
leftMax[i] = std::max(leftMax[i - 1], height[i]);
}
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; --i) {
rightMax[i] = std::max(rightMax[i + 1], height[i]);
}
int total = 0;
for (int i = 0; i < n; ++i) {
total += std::min(leftMax[i], rightMax[i]) - height[i];
}
return total;
}

Three linear passes: two to build the prefix-max arrays, one to apply the formula. Clear, correct, easy to argue complexity on.

Why this is a substantial improvement

The two prefix-max arrays are computed once, each in O(n)O(n) time. The formula sum is another O(n)O(n) pass. Total: 3n=O(n)3n = O(n) time, O(n)O(n) auxiliary space. From n=105n = 10^5 being a minute of compute (brute) to milliseconds (prefix max).

The code also reads like the math — two DP recurrences followed by a sum. Interviewers can verify correctness at a glance, which matters when time is tight.

Why we can still do better

We pay O(n)O(n) extra space for the two arrays. Every entry is read exactly once in the summing pass, then discarded. That’s wasteful — at any given column, the algorithm only needs one element from each array. If we can reconstruct those two values on the fly, we can reduce space to O(1)O(1). That’s the two-pointer sweep.

Complexity

  • Time: O(n)O(n) — three linear passes.
  • Space: O(n)O(n) for leftMax and rightMax.
Approach 03

Two-pointer sweep — eliminate the arrays Recommended

Run two pointers inward from the ends, tracking running maxes. Always advance the side with the smaller current bar; it's bounded by the other side. O(n) time, O(1) space.

Time O(n) Space O(1) two-pointersrecommendedconstant-space

The insight, stated cleanly

Look at the formula again: water at column i equals min(leftMax, rightMax) − height[i]. We only need leftMax when it is the smaller of the two, and similarly for rightMax. If we’re standing at a column whose side has the smaller confirmed max, we already know that side is the ceiling — the other side is at least that tall.

Put two pointers l = 0 and r = n-1 at the ends. Track two running values: leftMax (the max of H[0..l]) and rightMax (the max of H[r..n-1]). At each step:

  • If H[l] ≤ H[r]: the left side is the smaller end of the range. leftMax is the correct ceiling for column l (we’ve confirmed from the right side that the ceiling is at least H[r] ≥ H[l], which at worst equals leftMax). Compute water at l, advance l.
  • Otherwise: symmetric on the right.

The key move: we never need the absolute rightMax to compute water at l, only the guarantee that rightMax ≥ leftMax. The comparison H[l] ≤ H[r] gives us that guarantee without storing an array.

Live · two-pointer sweepStep through one column at a time. Each step advances the side with the smaller current bar, updates that side's running max, and adds the column's water contribution. The pointer with the orange triangle is the one that just advanced.
Input:
01234567891011lr
Step 0 of 12l=0, r=11leftMax=0, rightMax=0total = 0

Start. l = 0, r = 11. leftMax and rightMax both 0.

Three transitions worth naming

The widget above lets you step through every iteration. Three moments are particularly instructive — pause on each and the algorithm’s logic clicks into place.

The first water added. For the first two steps the left side advances over the leading zero and the rising edge [0, 1]; leftMax rises to 1 but the bar is at the same height, so no water accrues. On step 3 the cursor lands on a zero with leftMax = 1, and the formula adds the first unit of water.

lr
step 3l=3, r=11 · leftMax=1, rightMax=0 · total=1leftMax = 1, h[l] = 0 → add 1 unit of water at column 2. The algorithm's first deposit.

The first side switch. Through step 3 the left side has been advancing because H[l] ≤ H[r]. On step 4, H[l] = 2 > H[r] = 1 for the first time, and the right side starts moving. This is the comparison kicking in — the algorithm rebalances which side carries the water-adding work based on which end is currently lower.

lr
step 4l=3, r=10 · leftMax=1, rightMax=1 · total=1H[l]=2 > H[r]=1: rightMax updates and r retreats by one. No water added — the right end was the bound and equalled rightMax.

The deepest pour. The middle of the array contains the lowest cells (H[5] = 0) under a confirmed left ceiling of 2. Step 7 deposits 2 units in a single pour — the largest single-step contribution in this trace.

lr
step 7l=6, r=10 · leftMax=2, rightMax=1 · total=4leftMax = 2, h[l] = 0 → 2 units of water at column 5. The deepest single-step deposit in the canonical input.

After step 7 the algorithm continues until the pointers meet at index 7, accumulating one more unit on the right side. The widget plays this out fully; the table at the heart of every two-pointer trace is just these three rhythms repeating: advance on the smaller side, update its running max, deposit the gap.

The code

two-pointers.cpp
#include <algorithm>
#include <vector>
int trap(const std::vector<int>& height) {
const int n = static_cast<int>(height.size());
if (n < 3) return 0;
int l = 0;
int r = n - 1;
int leftMax = 0;
int rightMax = 0;
int total = 0;
while (l < r) {
if (height[l] <= height[r]) {
leftMax = std::max(leftMax, height[l]);
total += leftMax - height[l];
++l;
} else {
rightMax = std::max(rightMax, height[r]);
total += rightMax - height[r];
--r;
}
}
return total;
}

Ten-line while loop with symmetric branches. Two running maxes, no auxiliary arrays. The clean C++20-ish idiom is to prefer one `while` with an `if/else` over two loops.

Correctness, precisely — why advancing the smaller side is safe

This is the subsection interviewers probe hardest, and candidates fail most often.

Invariant. When we’re about to process column l with H[l] ≤ H[r], we have:

  • leftMax ≥ H[k] for all k ≤ l (from the way we updated leftMax).
  • H[r'] ≥ H[l] for some r' > l — specifically r' = r itself, because H[r] ≥ H[l].

The ceiling at column l is min(Ll,Rl)\min(L_l, R_l). We know:

  • Ll=leftMaxL_l = \text{leftMax} after the update.
  • RlH[r]H[l]R_l \ge H[r] \ge H[l]. We don’t know its exact value, but we know RlH[l]R_l \ge H[l], which implies RlleftMaxR_l \ge \text{leftMax} is possible. More importantly, RlH[l]R_l \ge H[l] means the right side has at least one bar tall enough that water can be supported from that side up to height H[l].

Wait — the argument is subtler. The water at l is min(leftMax, R_l) − H[l]. We want to show this equals leftMax − H[l] when H[l] ≤ H[r].

Claim. R_l ≥ leftMax whenever H[l] ≤ H[r].

Proof sketch. R_l is the maximum of H[l..n-1], so R_l ≥ H[r] ≥ H[l]. If leftMax > H[l], then some earlier column in H[0..l-1] already had height ≥ leftMax. But wait — that doesn’t directly give us R_l ≥ leftMax. The argument is actually:

The full rightMax at any time is max(H[r..n-1]). Since we initialized r = n-1 and only decremented r after confirming H[r] > H[l] (i.e., the right side had a taller bar than the left), the running rightMax tracks the tallest right-hand bar we’ve stepped past. Every time we advance r past a bar, that bar’s height is ≤ the H[r'] that “knocked it down” — which itself ≥ the H[l] at the time. Chained, this means the right side always has a bar of height ≥ max(leftMax, H[l]) still waiting (at the new r).

Therefore RlleftMaxR_l \ge \text{leftMax}, and the water is simply leftMax − H[l]. We don’t need R_l’s true value.

The invariant inductively holds: the side we advance has a certified ceiling from the other side, so its water contribution uses only the running max on its own side.

Edge cases

  • Strictly increasing ([1, 2, 3, 4, 5]): every step has H[l] ≤ H[r]; leftMax rises to match H[l] on every step; water added = 0 each time. Total = 0. Correct.
  • Strictly decreasing ([5, 4, 3, 2, 1]): every step has H[l] > H[r]; symmetric, same conclusion. Total = 0.
  • Single valley ([3, 0, 3]): step 1 has H[l] = 3 ≤ H[r] = 3; leftMax = 3, water at l=0 = 0, advance. Step 2: H[l] = 0 ≤ H[r] = 3, leftMax still 3, water = 3. Step 3: pointers meet. Total = 3.
  • Plateau ([2, 2, 2, 2]): every comparison is ≤, leftMax = 2 throughout, water = 0 on every column. Total = 0.

Complexity

  • Time: O(n)O(n). Each iteration advances exactly one pointer; total iterations ≤ nn.
  • Space: O(1)O(1). Two integers plus two counters.
Approach 04

Monotonic stack — rectangles on the fly

Maintain a stack of bar indices in strictly decreasing height order. When a taller bar arrives, pop and compute the bounded rectangle it just closed. O(n) time, O(n) space.

Time O(n) Space O(n) stackmonotonicamortized

The insight

The three previous approaches think column-by-column. The stack approach thinks rectangle-by-rectangle. Every time a taller bar arrives to the right of some shorter bar, the shorter bar has just been enclosed between two taller ones (the left neighbor already on the stack and the new bar on the right). The area of the bounded rectangle is min(left, right) − bottom times the horizontal distance.

By processing bars left to right and maintaining a decreasing monotonic stack, every bar is pushed once and popped once. When a pop happens, we’re closing a rectangle — and summing those closed rectangles exactly equals the trapped water.

How it works, step by step

Maintain a stack of bar indices (not heights — we need the distance for the rectangle width) where heights strictly decrease from bottom to top. For each new bar height[i]:

  1. While the stack is non-empty and height[i] > height[stack.top()]:
    • Pop the top index (call it bottom). This bar is about to be “closed” by a taller bar on its right.
    • If the stack is empty after the pop, no left bound exists — exit the inner loop.
    • Otherwise: peek the new top (call it left). The bar at left is the closest taller bar on bottom’s left.
    • Compute the rectangle: width = i − left − 1, heightBounded = min(height[i], height[left]) − height[bottom], and add width × heightBounded to the total.
  2. Push i onto the stack.

At the end, any bars still on the stack are never “closed” (no taller bar arrives to their right), so they contribute nothing — which is correct, because water needs a taller bar on each side.

The code

monotonic-stack.cpp
#include <algorithm>
#include <stack>
#include <vector>
int trap(const std::vector<int>& height) {
std::stack<int> stk;
int total = 0;
const int n = static_cast<int>(height.size());
for (int i = 0; i < n; ++i) {
while (!stk.empty() && height[i] > height[stk.top()]) {
const int bottom = stk.top();
stk.pop();
if (stk.empty()) break;
const int left = stk.top();
const int width = i - left - 1;
const int bounded = std::min(height[i], height[left]) - height[bottom];
total += width * bounded;
}
stk.push(i);
}
return total;
}

A monotonic stack of indices. The while loop is where the real work happens; each bar is pushed once, popped at most once.

Amortized argument — why it’s O(n)O(n)

The outer for loop runs nn times. The inner while loop looks like it could blow up, but each iteration pops exactly one element from the stack. Across the full run, the total number of pops is bounded by the total number of pushes — which is nn. So the total inner-loop work, summed over the whole algorithm, is O(n)O(n).

This is the same amortized pattern as the chain-head filter in Longest Consecutive Sequence and the sliding-window maximum deque. A nested loop that looks O(n2)O(n^2) is actually O(n)O(n) because each element is touched a bounded number of times.

When to use the stack approach

  • When other histogram problems surround this one. Monotonic stacks solve Largest Rectangle in Histogram, Next Greater Element, Maximal Rectangle, and more. If your interview is walking through that family, the stack is a unifying tool.
  • When you want to compute per-column rectangles and not just per-column water. The stack processes geometric regions; the two-pointer processes heights. Some follow-ups (e.g., “return the largest single puddle”) are easier from the stack view.
  • When the two-pointer invariant feels shaky. Some candidates simply find the stack formulation easier to prove — one rectangle, one pop, visible correctness.

Complexity

  • Time: O(n)O(n) amortized. Each bar pushed once, popped at most once.
  • Space: O(n)O(n) for the stack. In the worst case (strictly decreasing heights), all nn bars are stacked.

Approach comparison

ApproachTimeSpaceWhen to reach for it
Brute forceO(n2)O(n^2)O(1)O(1)Baseline in narration; never in production.
Prefix-max arraysO(n)O(n)O(n)O(n)When clarity > memory. Reads like the formula.
Two-pointer sweepO(n)O(n)O(1)O(1)Default. Constant space, single pass.
Monotonic stackO(n)O(n)O(n)O(n)When the problem sits inside a histogram-family interview.

Default answer: two-pointer. State the “always advance the smaller side” invariant out loud when you close.

Follow-ups to anticipate

The interviewer asks…What to do
”Return the water level at each column, not just the total.”Switch to prefix-max: already computes per-column contributions. Return the array instead of the sum.
”What about a 2D grid — water trapped on a 2D elevation map?”Different problem. Requires a min-heap BFS starting from the boundary (Trapping Rain Water II, LC 407). The 1D formula doesn’t generalize.
”Return the largest single puddle.”Use the stack approach — each pop yields one rectangle. Track the maximum rectangle as you go.
”What if bars have varying widths?”Replace width in every formula with the actual bar width. Same algorithm otherwise.
”Heights arrive as a stream — compute water incrementally.”The stack approach extends naturally: each new bar triggers the pop-and-compute loop. The two-pointer doesn’t fit streams.
”Can we do O(1) space AND O(n) time without the pointer comparison trick?”No general-purpose method known. The two-pointer sweep is tight on both.
”Heights in floats, not ints.”All four approaches work unchanged — comparisons and arithmetic only. Watch for floating-point precision near the clamp-at-zero boundary.
”What’s the worst-case stack depth in the monotonic approach?”O(n)O(n) on a strictly decreasing input: every bar is pushed and none is popped until the algorithm ends (and in that case, no water is trapped — consistent with correctness).

Pattern recognition — min-bound aggregation

The move this chapter teaches — value at position i is bounded by the minimum of two aggregates over the prefix and suffix — is a recurring shape:

  • Largest Rectangle in Histogram. Same stack framework, different geometry (max rectangle instead of trapped area).
  • Container With Most Water. Two-pointer with a similar “advance the smaller side” invariant. The key comparison is on heights, not on running maxes.
  • Sliding Window Maximum. A monotonic deque, not a stack, but the amortized-linear-touches-per-element argument is the same.
  • Minimum in Every Window. Same deque idea, inverted.
  • Candy. Two linear passes (left-to-right and right-to-left) reminiscent of the prefix-max arrays.
  • Shortest Unsorted Continuous Subarray. Prefix-max + suffix-min to locate the two boundaries.

The reflex: when a problem asks for a quantity at each index bounded by aggregates over the prefix and suffix, think prefix/suffix arrays first, then look for a two-pointer elimination. Add monotonic-stack if the problem is naturally geometric.

What to say out loud — the delivery script

Ten beats:

  1. “Let me restate: given an array of non-negative heights, compute the total water trapped between the bars.”
  2. Ask: “non-negative only? any constraints on n? total or per-column?”
  3. “The core formula: water at column i equals min(leftMax[i], rightMax[i]) minus height[i], clamped at zero.”
  4. “Brute force: recompute leftMax and rightMax at every i. That’s O(n2)O(n^2). Name it, move on.”
  5. “Prefix-max arrays: precompute both in two linear passes, then sum the formula. O(n)O(n) time, O(n)O(n) space.”
  6. “Two-pointer sweep: we don’t need both bounds at once. Always advance the side with the smaller current bar — that side’s running max is the true ceiling because the other side has at least one bar taller than our current.”
  7. Write the two-pointer code — 10 lines. Symmetric if/else branches.
  8. Trace the canonical example on the whiteboard. Point at a step where H[l] > H[r] and say: “we advance r because the left is confirmed higher — rightMax is the ceiling at r.”
  9. State the invariant explicitly: “at every step, the side we advance has a confirmed ceiling from the other side. That’s why running max on this side equals min(leftMax, rightMax) for that column.”
  10. O(n)O(n) time, O(1)O(1) space. If the interviewer wants per-column output or a 2D grid I’d switch to prefix-max or min-heap BFS. For the standard problem, this is what I’d ship.”

Summary

Trapping Rain Water is the canonical histogram problem. Four approaches walk a careful arc — recompute the helpers, precompute them, eliminate them, then reframe the problem as rectangles — and each step buys an order-of-magnitude improvement in either time or space.

The interview signal isn’t the final code. It’s recognizing that the obvious answer can be improved, naming what’s expensive about it, and walking the optimization staircase deliberately rather than guessing the optimal answer up front. The broader lesson — the “min-bound aggregation” pattern — appears across the histogram-problem family (largest rectangle, container with most water, sliding-window maximum) and any setting where a per-index quantity depends on aggregates from both directions.