Container With Most Water
Optimize area calculations by moving pointers based on height comparisons
SPACED REPETITION Β· 15 practice questions
Make this lesson stick.
Try 3 questions now. No account needed. Sample answers aren't saved.
or sign in to practice all 15One comparison retires a whole wall
Here is a row of vertical lines, one per index: height = [1, 8, 6, 2, 5, 4, 8, 3, 7]. Pick two of them. Together with the floor they make a container, and the water inside rises until it spills over the shorter line. The water it holds is
area = min(height[i], height[j]) Γ (j β i)
Find the pair that holds the most.
The obvious plan measures every pair. Nine lines give 36 pairs, which is nothing. LeetCode allows 100,000 lines, which gives n(nβ1)/2 = 4,999,950,000 pairs. The brute force below measured about 13 million pairs per second in CPython on a fast laptop, so one input would take about six and a half minutes. No judge waits that long.
The two-pointer solution in this lesson measures 99,999 pairs for the same input, and it finished in about 10 milliseconds. It rests on one observation:
Start with the widest pair. Its shorter wall has just met its best possible partner, because every other partner is closer and the water can never rise above that wall. So one measurement retires that wall for good.
Each measurement retires one wall, and after n β 1 of them nothing is left to check. Most of this lesson is about why that is safe, because the proof is what lets you reuse the idea and spot when it breaks.
| Approach | Pairs measured when n = 100,000 | Time | Extra space |
|---|---|---|---|
| Every pair | 4,999,950,000 | O(nΒ²) | O(1) |
| Two pointers: retire the shorter wall | 99,999 | O(n) | O(1) |
This lesson builds on Two Pointer Techniques, where converging pointers solve Two Sum II on sorted input, and on Foundation: Arrays & Strings: best here is a running summary (Move 1), and left and right start at opposite ends like the reversal pointers of Move 4. Two things are new. The input is not sorted, and the choice of which pointer to move comes from comparing the two heights, not from comparing a sum with a target. Why that choice is safe is the interesting question.
Read the contract
This is LeetCode 11: Container With Most Water. You get height, a list of n integers. Line i runs from (i, 0) up to (i, height[i]). Return the largest amount of water two lines can hold with the x-axis. The container may not be slanted. The limits are 2 β€ n β€ 10β΅ and 0 β€ height[i] β€ 10β΄.
Four details decide what you compute:
- Return a number. The judge wants the area, not the two indices.
- Only the two chosen lines matter. The lines between them have no thickness. They neither block the water nor take up room.
- Width is the distance, j β i. It counts the gaps between the lines, not the lines. Lines 3 and 5 are 2 apart.
- n β₯ 2, but heights can be 0. There is always at least one pair, and the answer can be 0.
Here is the best container for the example, lines 1 and 8. The # columns are its walls, the | lines are ignored, and ~ is water:
8 # |
7 #~~~~~~~~~~~~~~~~~~~|~~~~~~~#
6 #~~~|~~~~~~~~~~~~~~~|~~~~~~~#
5 #~~~|~~~~~~~|~~~~~~~|~~~~~~~#
4 #~~~|~~~~~~~|~~~|~~~|~~~~~~~#
3 #~~~|~~~~~~~|~~~|~~~|~~~|~~~#
2 #~~~|~~~|~~~|~~~|~~~|~~~|~~~#
1 | #~~~|~~~|~~~|~~~|~~~|~~~|~~~#
0 1 2 3 4 5 6 7 8 <- index
The water is 7 gaps wide and 7 levels deep, so the area is 49. Line 1 is 8 tall, but the water stops at 7, where line 8 ends.
Predict first: the two tallest lines are the 8s at indices 1 and 6. Is that pair the answer?
Check your answer
No. The pair (1, 6) holds min(8, 8) Γ 5 = 40. The pair (1, 8) gives up one level of height and gains two units of width: min(8, 7) Γ 7 = 49. The widest pair, (0, 8), holds only min(1, 7) Γ 8 = 8. Height and width trade off against each other, so neither one alone picks the winner.
Every pair, honestly
Smell: you are maximizing a score over all pairs i < j, and one factor of that score (here, the width) only shrinks as a pair moves inward.
Write the slow version first. It is short, obviously correct, and it becomes the reference you test the fast version against.
def max_area_brute(height):
n = len(height)
best = 0
for i in range(n):
for j in range(i + 1, n):
best = max(best, min(height[i], height[j]) * (j - i))
return best
print(max_area_brute([1, 8, 6, 2, 5, 4, 8, 3, 7])) # 49
It measures n(nβ1)/2 pairs: O(nΒ²) time and O(1) extra space. At 13 million pairs per second, the 4,999,950,000 pairs for n = 100,000 take about 385 seconds.
Where the time goes
Picture every pair as one cell of a triangle, the pair table. Row i holds the pairs whose left wall is i, and column j holds the pairs whose right wall is j. Here it is for a smaller input, with each cell holding that pair's area:
height = [1, 5, 7, 2, 6, 3]
j=1 j=2 j=3 j=4 j=5
(5) (7) (2) (6) (3)
i=0 (1) 1 2 3 4 5
i=1 (5) 5 4 15 12
i=2 (7) 2 12 9
i=3 (2) 2 4
i=4 (6) 3
Look at row 0. Wall 0 is 1 tall, so every pair in that row holds at most 1 Γ width, and the widest cell, 5, has to be the best one in the row. The brute force still computes all five cells.
That observation is the whole speedup. If one measurement can tell you the best cell of an entire row, you can skip the rest of that row. The next section finds such a measurement at every step, not just for the shortest wall.
The shorter wall is finished
Predict first: start with the widest pair of the LeetCode example, (0, 8). The heights are 1 and 7, so the area is 8. Without measuring anything else, which of the two walls can you rule out of every future container, and why? What about the other wall?
Check your answer
Wall 0. Any other container that uses wall 0 pairs it with some wall k between 1 and 7. Its water cannot rise above 1, because wall 0 caps it, and its width k β 0 is less than 8. So it holds at most 1 Γ 7 = 7 < 8. Wall 0 has already met its best partner.
You cannot say the same about wall 8. Its other partners are closer too, but one of them might be taller than 1 and lift the water. One is: wall 1 with wall 8 holds 49.
That is the whole rule: measure the current pair, then retire the shorter wall by moving its pointer inward.
def max_area(height):
left, right = 0, len(height) - 1
best = 0
while left < right:
area = min(height[left], height[right]) * (right - left)
best = max(best, area)
if height[left] < height[right]:
left += 1 # the left wall is shorter: it has met its best partner
else:
right -= 1 # the right wall is shorter, or they tie
return best
print(max_area([1, 8, 6, 2, 5, 4, 8, 3, 7])) # 49
Here is every iteration on the example, printed by running the code:
| step | left | right | h[left] | h[right] | width | area | best | move |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 8 | 1 | 7 | 8 | 8 | 8 | left += 1 |
| 2 | 1 | 8 | 8 | 7 | 7 | 49 | 49 | right -= 1 |
| 3 | 1 | 7 | 8 | 3 | 6 | 18 | 49 | right -= 1 |
| 4 | 1 | 6 | 8 | 8 | 5 | 40 | 49 | right -= 1 (tie) |
| 5 | 1 | 5 | 8 | 4 | 4 | 16 | 49 | right -= 1 |
| 6 | 1 | 4 | 8 | 5 | 3 | 15 | 49 | right -= 1 |
| 7 | 1 | 3 | 8 | 2 | 2 | 4 | 49 | right -= 1 |
| 8 | 1 | 2 | 8 | 6 | 1 | 6 | 49 | right -= 1 |
After step 8, left == right == 1 and the loop stops. Three things to notice:
- The answer appears at step 2, but the code cannot know that nothing better is coming. It keeps retiring walls until none are left.
- Step 3 is worse than step 2 (18 after 49). Retiring the shorter wall does not promise a better next pair. It promises that the pairs you skip are no better than one you already measured.
- Wall 1 never retires. It is 8 tall, the maximum, so it is never the strictly shorter side. At the step-4 tie (8 and 8), this code moves
right.
Cost. Every iteration moves exactly one pointer by one step, so the gap right β left starts at n β 1, drops by 1 each time, and stops at 0. The loop runs exactly n β 1 times, whatever the heights: O(n) time. The pointers meet wherever the rule takes them, not necessarily in the middle (here, at index 1). Extra space is O(1): three integers and a temporary.
Your turn: trace max_area([2, 3, 4, 5, 18, 17, 6]). List the pairs it measures, give the result, and say whether it ever measures the two tallest walls, 18 and 17.
Check your answer
| step | pair | heights | area | best | retires |
|---|---|---|---|---|---|
| 1 | (0, 6) | 2, 6 | 12 | 12 | left |
| 2 | (1, 6) | 3, 6 | 15 | 15 | left |
| 3 | (2, 6) | 4, 6 | 16 | 16 | left |
| 4 | (3, 6) | 5, 6 | 15 | 16 | left |
| 5 | (4, 6) | 18, 6 | 12 | 16 | right |
| 6 | (4, 5) | 18, 17 | 17 | 17 | right |
The result is 17. The left pointer marches past four short walls because each one is shorter than the 6 at the right end. At step 5 the comparison flips, because 18 is taller than 6, so the right pointer moves for the first time. The last measurement is the two tallest walls: they are adjacent, so their width is only 1, and they still win by one unit.
Why it never misses the answer
"Move the shorter one" sounds like a rule of thumb. It is a proof, and the pair table shows it.
The pair table, erased one line at a time
Run max_area on the six-wall input from before and record, for every cell, the step that retired it. A star marks the pair measured at that step:
height = [1, 5, 7, 2, 6, 3] (* = measured at that step)
j=1 j=2 j=3 j=4 j=5
(5) (7) (2) (6) (3)
i=0 (1) 1 1 1 1 1*
i=1 (5) 3 3 3* 2*
i=2 (7) 5* 4* 2
i=3 (2) 4 2
i=4 (6) 2
| step | measured | area | retires | cells retired | largest area among them |
|---|---|---|---|---|---|
| 1 | (0, 5) | 5 | row 0 (wall 0) | 5 | 5 |
| 2 | (1, 5) | 12 | column 5 (wall 5) | 4 | 12 |
| 3 | (1, 4) | 15 | row 1 (wall 1) | 3 | 15 |
| 4 | (2, 4) | 12 | column 4 (wall 4) | 2 | 12 |
| 5 | (2, 3) | 2 | column 3 (wall 3) | 1 | 2 |
Each step erases one full row or column of what is left of the triangle, and the starred pair sits at its outer corner: the widest cell of that line. The last column of the table is the point. In every erased line, the largest area is the one just measured. Five steps erase 5 + 4 + 3 + 2 + 1 = 15 cells, which is every pair, and the answer, 15 at (1, 4), was measured at step 3.
The proof in plain words
Here is the sentence that stays true:
Invariant: before each iteration, every pair that uses a wall outside
left..rightholds at mostbest.
- At the start,
left..rightcovers every wall, so there is nothing to check. - Each step keeps it true. Say the code retires the left wall. It does that only when
height[left] < height[right], so the area it just measured isheight[left] Γ (right β left). The pairs that leave the range are(left, k)for every k fromleft + 1toright. Each one's water is capped byheight[left], and its widthk β leftis at mostright β left. So each holds at most the area just measured, andbestis at least that. Retiring the right wall (whenheight[right] <= height[left], ties included) is the mirror image. - At the end,
left == right, so every pair uses a wall outside the range. All of them hold at mostbest, andbestis the area of a pair that was really measured. Sobestis the maximum. β
The proof used exactly two facts about the retired wall:
- It caps the water. Paired with anything, the shorter wall allows a level of at most its own height.
- Its remaining partners are all closer. Moving inward only shrinks the width.
Why not retire the taller wall? Fact 1 fails for it. A closer partner that is taller than the current short wall can lift the water, so the taller wall's row is not finished. A small failure is [1, 5, 5]. The correct code measures (0, 2) = 2, retires wall 0, then finds (1, 2) = 5. The swapped rule measures (0, 2) = 2, retires wall 2, measures (0, 1) = 1, and returns 2. On the LeetCode example it returns 8: it keeps the 1-high wall 0 to the end and throws away every wall that could have beaten it.
Ties. When the two heights are equal, both walls are finished. Each one's other partners are closer and capped at the same height. Moving either pointer is safe, and so is moving both at once. This code moves right on a tie; writing if height[left] <= height[right] moves left instead. Different tie rules visit different pairs and return the same answer.
Why "at most", not "less than"? The retired pairs include the pair just measured, and sooner or later the optimal pair itself, and those can equal best. Every other pair retired in a step is strictly narrower than the measured one, so it is strictly smaller as long as the short wall is taller than 0. A wall of height 0 holds 0 with every partner, so there even the narrower pairs tie.
π§ Stronger than it needs to be. The proof shows only that
bestreaches the maximum. In fact, when the answer is positive,max_areameasures every optimal pair. Take an optimal pair (i, j) with area OPT > 0, and look at the first step that retires i or j, say i. Thenleft == iandright >= j. Ifrightwere beyond j, the pair just measured would have water levelheight[i], which is at least the level of (i, j), and more width, so it would hold more than OPT. That is impossible. Soright == j, and (i, j) itself was measured. Only when the answer is 0, as in[0, 0, 5], can an optimal pair go unmeasured, and then every pair ties at 0 anyway. A check of 197,650 inputs under both tie rules agreed.
Your turn: the proof never mentions water. Which of these scores can you still maximize with "measure, then retire the shorter wall"? Check each against the two facts before you look.
min(h[i], h[j]) Γ (j β i + 1), which counts lines instead of gaps(h[i] + h[j]) Γ (j β i)max(h[i], h[j]) Γ (j β i)
Check your answer
- Yes. The score still depends only on the lower wall and a width that shrinks inward, so both facts hold.
- No. The retired short wall can still score well with a tall partner, because the taller wall now adds to the score. On
[1, 1, 4, 2]the rule retires wall 0 first, yet the best pair is (0, 2) = (1 + 4) Γ 2 = 10. The rule returns 9. - No, for the same reason: the score rewards the taller wall. On
[1, 1, 4, 2]the rule returns 6, and (0, 2) scores 4 Γ 2 = 8.
The general rule: retiring the shorter wall is safe for any score that depends only on the lower wall and the width, and never decreases when either one grows. If a score breaks that, you need a new proof, or a different technique.
Break it on purpose
Each of these edits to max_area looks reasonable. Every result below comes from running the edited code on the LeetCode example, whose true answer is 49:
| Edit | Returns | Verdict |
|---|---|---|
Assign best once, after the loop, from the last pair |
6 | Wrong: it keeps only the last, narrowest pair |
Width right - left + 1 |
56 | Wrong: it counts lines, not gaps; [1, 1] gives 2 |
max(...) instead of min(...) for the level |
56 | Wrong: the water spills over the shorter wall |
| Retire the taller wall | 8 | Wrong: [1, 5, 5] gives 2 |
while left <= right |
49 | Harmless: one extra pass with width 0 |
Compare with <=, so ties move left |
49 | Fine: a different, equally valid tie rule |
The first bug is the sneakiest because it passes some tests: on [4, 9] there is only one pair, so the last pair is also the best. Update best inside the loop, for the pair you just measured, on every iteration.
Edge cases
| Input | Result | What to notice |
|---|---|---|
[4, 9] |
4 | n = 2: one iteration, then left == right |
[0, 0] |
0 | Zeros are allowed, so the answer can be 0 |
[5, 5, 5, 5, 5] |
20 | All ties: the first, widest pair wins; this code then moves right every time |
[1, 2, 3, 4, 5] |
6 | left moves every time: (0, 4), (1, 4), (2, 4), (3, 4) |
[5, 4, 3, 2, 1] |
6 | right moves every time: (0, 4), (0, 3), (0, 2), (0, 1) |
β οΈ Other languages. Python integers never overflow. In Java, C# or C++, check that the product fits. Here the largest possible area is 10β΄ Γ 99,999 = 999,990,000, which fits in a 32-bit int (the limit is 2,147,483,647). With larger limits you would need 64-bit arithmetic.
Test it against the slow version
The brute force earns its keep here. Small heights, 0 to 6, force plenty of ties and zeros, which is where pointer bugs hide:
import random
def check(solution, trials=3000):
for _ in range(trials):
n = random.randint(2, 9)
height = [random.randint(0, 6) for _ in range(n)]
got, want = solution(height), max_area_brute(height)
assert got == want, (height, got, want)
print("all matched")
check(max_area)
Success prints all matched. All four wrong edits in the table fail within a handful of trials, and the assertion message hands you a small failing input to trace by hand.
Your turn: a teammate says the loop wastes time measuring pairs that obviously lose, and submits this:
def max_area_skip(height):
left, right = 0, len(height) - 1
best = 0
while left < right:
low = min(height[left], height[right])
best = max(best, low * (right - left))
while left < right and height[left] <= low:
left += 1
while left < right and height[right] <= low:
right -= 1
return best
Is it correct? What is its time complexity? How many areas does it compute on the LeetCode example?
Check your answer
Correct. Every wall it skips is no taller than low, and every pair such a wall could still form is no wider than right β left. So each skipped pair holds at most low Γ (right β left), the area just measured. That is the same argument as before, applied to several walls at once. The loop always makes progress: low is the height of one of the two walls, so at least one inner loop moves at least once.
Still O(n) time. The pointers still take at most n β 1 steps in total. What it saves is area computations: on the example it measures only (0, 8), (1, 8) and (1, 6), three instead of eight. check(max_area_skip) prints all matched. It is a fine constant-factor tweak, but it changes nothing about the growth rate.
Cousin, not twin: Trapping Rain Water
Predict first: now make the lines solid bars, each 1 unit wide, and pour rain over the whole skyline [1, 8, 6, 2, 5, 4, 8, 3, 7]. Is the trapped water 49 again?
Check your answer
No, it is 19. Two things changed. The bars take up space, so water sits only above each bar, not through it. And there is no longer a single container: every column holds its own water, up to the lower of the tallest bar on its left and the tallest bar on its right.
This is LeetCode 42: Trapping Rain Water. Water above column i is
water[i] = min(tallest bar in 0..i, tallest bar in i..nβ1) β height[i]
Both ranges include i itself, so the result is never negative.
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| height | 1 | 8 | 6 | 2 | 5 | 4 | 8 | 3 | 7 |
| tallest in 0..i | 1 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
| tallest in i..nβ1 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 7 | 7 |
| water | 0 | 0 | 2 | 6 | 3 | 4 | 0 | 4 | 0 |
The water sums to 19. A prefix-maximum array and a suffix-maximum array compute this table in O(n) time and O(n) space. Two pointers drop the arrays:
def trap(height):
left, right = 0, len(height) - 1
left_max = right_max = 0
water = 0
while left < right:
left_max = max(left_max, height[left]) # tallest in 0..left
right_max = max(right_max, height[right]) # tallest in right..n-1
if left_max < right_max:
water += left_max - height[left] # cell left is settled
left += 1
else:
water += right_max - height[right] # cell right is settled
right -= 1
return water
print(trap([1, 8, 6, 2, 5, 4, 8, 3, 7])) # 19
Why it works. left_max is exactly the left bound for cell left. Its right bound is the tallest bar anywhere to its right, which is at least right_max. If left_max < right_max, the lower bound is left_max no matter what stands in between, so the water above left is settled at left_max β height[left]. Otherwise, the mirror argument settles cell right. Each step settles one cell. The cell where the pointers meet always holds a tallest bar, so it holds no water. That is O(n) time and O(1) extra space.
Both problems start at the ends and move the lower side inward, but for different reasons:
| Container With Most Water | Trapping Rain Water | |
|---|---|---|
| Answer | Area of the single best pair | Total over every column |
| Bars between | Ignored: lines have no thickness | Hold water above them and take up room |
| What one step does | Retires a wall: it has met its best partner | Settles the water above one cell |
| What it compares | The two current heights | The two running maxima |
[1, 8, 6, 2, 5, 4, 8, 3, 7] |
49 | 19 |
| Cost | O(n) time, O(1) space | O(n) time, O(1) space (O(n) space with prefix/suffix arrays) |
Your turn: for [3, 0, 2, 0, 4], what does max_area return, and what does trap return? Work both out by hand first.
Check your answer
max_area returns 12: the outer walls, min(3, 4) Γ 4. The bars in between do not matter.
trap returns 7. Every inner column is bounded by 3 on the left and 4 on the right, so its water level is 3. The columns hold 3 β 0, 3 β 2 and 3 β 0, which is 3 + 1 + 3 = 7. The outer columns hold nothing. In the two-pointer run, right_max is 4 from the start and left_max never exceeds 3, so the code settles cells 0 to 3 from the left and stops at the 4.
Final round: no label on the problem
Real problems don't announce their technique. Check whether the two facts of the proof hold, and if they don't, look for another tool.
Challenge 1: fences at arbitrary positions
You get fence posts as (position, height) pairs in no particular order, for example [(9, 4), (0, 3), (5, 8), (12, 6), (2, 7)]. Positions are distinct. Stretch a tarp between two posts; it holds min(height) Γ (distance between their positions). Return the best amount.
- Which technique applies?
- What must change first, and which fact of the proof needs that change?
- What does it cost?
Hint
Run max_area's rule on the list exactly as given, using the real distance as the width. Is a pair closer to the middle of the list always narrower?
Check your answer
Sort by position, then run the same two pointers with the width x[right] β x[left]:
def max_fence_area(posts):
posts = sorted(posts) # by position
left, right = 0, len(posts) - 1
best = 0
while left < right:
(xl, hl), (xr, hr) = posts[left], posts[right]
best = max(best, min(hl, hr) * (xr - xl))
if hl < hr:
left += 1
else:
right -= 1
return best
print(max_fence_area([(9, 4), (0, 3), (5, 8), (12, 6), (2, 7)])) # 60
The winners are the posts at 2 and 12, with 6 Γ 10 = 60. The proof needs fact 2: every remaining partner is closer. That holds only when positions increase from left to right. On the unsorted list, a pair nearer the middle of the list can be farther apart on the ground, and the rule returns 42 here instead of 60.
Sorting costs O(n log n) time and the sorted copy O(n) space; the scan after it is O(n).
Challenge 2: two sights and a walk
values = [8, 1, 5, 2, 6] rates sightseeing spots along a street. The score of spots i < j is values[i] + values[j] + i β j: two attractions, minus the walk between them. Return the best score. This is LeetCode 1014: Best Sightseeing Pair.
It is a pair problem with a distance in it. Is it this lesson's technique?
Check your answer
No. Fact 1 fails: the score is not capped by the smaller value. Fact 2 no longer helps: the remaining partners are still closer, but closer now scores more, because distance costs points. On [2, 3, 1, 1, 3] the retire-the-smaller rule returns 3 under either tie rule, while spots 0 and 1 score 2 + 3 + 0 β 1 = 4.
The right tool is Move 1 of the Foundation lesson, a running summary. Split the score into a part for each spot: (values[i] + i) + (values[j] - j). For a fixed j, the best partner is the earlier spot with the largest values[i] + i, which is one number you can carry forward:
def max_score_sightseeing(values):
best_left = values[0] + 0 # largest values[i] + i so far
best = float("-inf")
for j in range(1, len(values)):
best = max(best, best_left + values[j] - j) # j as the second spot
best_left = max(best_left, values[j] + j) # j as a future first spot
return best
print(max_score_sightseeing([8, 1, 5, 2, 6])) # 11
It returns 11, from spots 0 and 2: 8 + 5 + 0 β 2. That is O(n) time and O(1) space. As in the stock problem, score j as the second spot before letting it become a first spot.
Quick-fire: which tool?
Name the technique for each one before you open the answer.
- Buy on one day and sell on a later day; maximize the price difference.
- Choose two lines; maximize min(height) Γ distance.
- Rain falls on a skyline of unit-width bars; how much stays?
- Find two values in a sorted array that add up to a target.
Check your answer
- A running minimum of earlier prices (Foundation, Move 1). Only one side of the pair needs remembering.
- This lesson: measure, retire the shorter wall.
- Trapping Rain Water: settle the side with the smaller running maximum.
- Converging pointers that compare the sum with the target: Two Sum II in the Two Pointer Techniques lesson. The same pair-table picture applies. A sum that is too small retires the left value's row, because every remaining partner is no larger.
Cheat sheet
| Piece | Rule | Why |
|---|---|---|
| Area | min(height[i], height[j]) * (j - i) |
Water spills over the lower wall; width counts gaps |
| Start | left = 0, right = n - 1 |
The widest pair; every pair lies inside the range |
| Each step | Measure, update best, retire the shorter wall |
Its best remaining partner is the one just measured |
| Ties | Move either pointer, or both | Both walls are finished |
| Invariant | Pairs using a wall outside left..right hold at most best |
At the end, that is every pair |
| Cost | Exactly n β 1 iterations: O(n) time, O(1) space | right - left drops by 1 per iteration |
| Reuse the rule when | The score depends only on the lower wall and the width, and never decreases when either grows | Those are the only two facts the proof uses |
| Trapping Rain Water | Settle the side with the smaller running maximum | That side's water level is already decided |
Before moving on, open a blank editor and solve LeetCode 11 from memory. Then explain it aloud: what the contract asks for, what the brute force wastes, what the invariant says before each iteration, why the shorter wall is finished but the taller one is not, and what goes wrong on [1, 5, 5] if you swap them. Finish with one score change that keeps the rule safe and one that breaks it.
Next: Sliding Window Patterns, where two pointers move in the same direction and keep state about everything between them. For the rest of the pointer family, including Two Sum II, 3Sum and fast/slow pointers, go back to Two Pointer Techniques.