The Water Gauge
A medium Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- medium
- Seniority
- L4
Problem
A terrain heightmap is stored as a list of non-negative bar heights. After heavy rain, water collects in the dips between taller bars; return the total units of water the profile traps.
Elevation bars trap water in the dips. Total the volume.
Example
Input:
heights = [0,1,0,2,1,0,1,3,2,1,2,1]
Output:
6
Worked solution and explanation
What this really is
Strip the rain metaphor and this is a bounded-minimum problem: the water standing over bar i is min(tallest bar to its left, tallest bar to its right) - heights[i], and never less than zero. Everyone lands the min part eventually. The trap is the bookkeeping. Candidates reach for a single running maximum and discover it only ever sees ONE wall, so they either rescan both sides at every bar (O(n^2)) or, worse, take the max of the two walls instead of the min. Take the max and you claim water piled higher than the shorter wall can hold: physically impossible volumes. Forget to clamp at zero and a tall bar sitting in a trough contributes negative water and drags your total down.
Getting to one pass, O(1) space
Step 1: Anchor two pointers at the ends
Put left at index 0 and right at the last bar, and remember the tallest bar seen from each end so far in left_max and right_max. These two scalars are your only known walls.
Step 2: Advance the side with the shorter wall
Move whichever pointer sits behind the shorter wall. If left_max <= right_max, the left wall is binding, so advance left; otherwise advance right. Because the shorter wall caps the water, you can commit that side without ever knowing the taller wall's exact height.
Step 3: Commit water as you go
After each move, refresh that side's running max, then add running_max - heights[pointer] to the total. It is always non-negative because the running max already includes the current bar.
The solution
def trap_rainwater(heights: list[int]) -> int:
if len(heights) < 3:
return 0
left, right = 0, len(heights) - 1
left_max, right_max = heights[left], heights[right]
total = 0
while left < right:
if left_max <= right_max:
left += 1
left_max = max(left_max, heights[left])
total += left_max - heights[left]
else:
right -= 1
right_max = max(right_max, heights[right])
total += right_max - heights[right]
return total- Prefix/suffix max arrays
- Two passes build
left_max[i]andright_max[i], a third sumsmin(...) - h[i]. Dead clear, three linear scans, but O(n) extra space for the two arrays. A perfectly good first answer to say out loud, and it embodies the same min-of-two-walls insight. - Two converging pointers
- Keep only two scalars for the running maxima and walk inward from both ends. Same O(n) time, but O(1) space, because the shorter wall is always the binding one. This is the version to land.
Common follow-up questions
- Walk me through why the two-pointer version can safely commit water on the shorter side. (Confirms the candidate can articulate the O(1) two-pointer reduction from the array version.)
- What if the elevation map were 2D instead of a single profile? (Tests BFS from the boundary with a priority queue (Trapping Rain Water II).)
- What changes if bars could have negative heights? (Tests reasoning about water level relative to the lowest point when floors can dip below zero.)