Container With Most Water
Optimize area calculations by moving pointers based on height comparisons
Why Container With Most Water is a Gateway to Two-Pointer Mastery
Imagine you're an architect designing a water reservoir. You have a row of walls of varying heights, and your job is to pick any two walls to act as the sides of your tank. The floor is free β it's just the ground between them. Which two walls do you choose to hold the most water? This is exactly the puzzle behind one of LeetCode's most beloved problems, and understanding it deeply will unlock a family of algorithmic thinking that shows up again and again in technical interviews. Grab the free flashcards at the end of each section to cement what you learn β this lesson is designed so each concept builds on the last.
The Container With Most Water problem sits at a perfect crossroads: it's simple enough to understand in thirty seconds, yet rich enough that the journey from your first instinct (check every pair!) to the elegant solution (move smartly with two pointers) teaches you something genuinely profound about how to think about array traversal. Let's start at the beginning.
Breaking Down the Problem Statement
You are given an integer array height of length n. Each element height[i] represents the height of a vertical line drawn at position i on the x-axis. Your goal is to find two lines that, together with the x-axis, form a container that holds the maximum amount of water.
Let's make this concrete. Say your input is:
height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
Picture this as a skyline viewed from the side:
8 8
| 6 | 7
| | 5 | |
| | | 4 | |
| | | | | 3 |
| 2 | | | | |
1| | | | | |
+---+---+-+-+---+--->
0 1 2 3 4 5 6 7 8
The key insight is the area formula. If you pick the wall at index left and the wall at index right, the water that can be stored is limited by the shorter of the two walls (water spills over the shorter side) and the distance between them (wider containers hold more):
area = min(height[left], height[right]) * (right - left)
For left = 1 (height 8) and right = 8 (height 7), the area is min(8, 7) * (8 - 1) = 7 * 7 = 49. That turns out to be the answer for this input.
π‘ Mental Model: Think of the water's surface as a flat line. It can only rise as high as the shorter wall. The longer wall is irrelevant beyond that height β extra height on one side buys you nothing if the other side is shorter.
The Brute Force Approach: Correct but Costly
Your first instinct is almost certainly the right human instinct: check every possible pair of walls. This is the brute force approach, and it absolutely produces the correct answer. Here's what it looks like in code:
def max_area_brute_force(height):
"""
O(nΒ²) brute force: check every pair of walls.
Correct, but too slow for large inputs.
"""
n = len(height)
max_water = 0
for left in range(n):
for right in range(left + 1, n): # right always starts after left
# Area = shorter wall * distance between walls
water = min(height[left], height[right]) * (right - left)
max_water = max(max_water, water)
return max_water
## Example usage
height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
print(max_area_brute_force(height)) # Output: 49
This works perfectly for small arrays. The outer loop picks a left wall; the inner loop tries every possible right wall to the right of it. You track the running maximum area and return it at the end.
π€ Did you know? For an array of length n, the number of unique pairs is n * (n - 1) / 2. With n = 10, that's just 45 comparisons β totally fine. But with n = 100,000 (a common LeetCode constraint), you're looking at nearly 5 billion comparisons. On a modern computer executing ~10βΈ simple operations per second, that's about 50 seconds. LeetCode's time limit is typically 1β2 seconds. Brute force simply doesn't scale.
Understanding the Performance Cost
The brute force runs in O(nΒ²) time complexity β the nested loops mean that as the input doubles in size, the work quadruples. Its space complexity is O(1) since you only store a single running maximum. The time problem is the dealbreaker at scale.
π Quick Reference Card:
| Approach | β±οΈ Time Complexity | πΎ Space Complexity | β Correct? | π Scalable? |
|---|---|---|---|---|
| π Brute Force | O(nΒ²) | O(1) | Yes | No |
| π― Two Pointers | O(n) | O(1) | Yes | Yes |
The Mental Shift: From Exhaustive Search to Intelligent Decision-Making
Here is where the real learning begins. The gap between O(nΒ²) and O(n) isn't just a performance upgrade β it represents a fundamentally different way of thinking about the problem.
β Wrong thinking: "I need to check every pair because I might miss the best one."
β Correct thinking: "I can reason about which pairs are worth checking and skip the rest without missing the answer."
This shift β from exhaustive enumeration to principled elimination β is the heart of the two-pointer technique. The key question becomes: when can I be certain that moving a pointer in a specific direction cannot possibly make things worse, and might make things better?
Let's think through the logic at a high level before diving into the full algorithm in Section 2.
Imagine you start with pointers at the two ends of the array: left = 0 and right = n - 1. This gives you the maximum possible width. Now, you have a choice: move the left pointer right, or move the right pointer left. Either way, the width shrinks by 1. The only way to compensate for lost width is to find a taller wall.
π‘ Pro Tip: Here's the pivotal insight β if height[left] < height[right], the left wall is the bottleneck. Moving the right pointer inward can't help: even if the new right wall is taller, you're still limited by the same short left wall, but now with less width. The only rational move is to advance the left pointer, hunting for a taller wall that might compensate for the reduced width. This is not a guess β it's a proof, and we'll formalize it in Section 2.
π― Key Principle: The two-pointer approach works here because moving a pointer in the wrong direction is provably suboptimal. This means we never need to check those configurations β they are mathematically eliminated.
Previewing the Two-Pointer Solution
Here's a preview of what the optimal solution looks like. Don't worry about internalizing every detail yet β that's what Section 2 is for. The goal right now is to feel the elegance of it:
def max_area_two_pointers(height):
"""
O(n) two-pointer approach.
Start wide, then shrink inward intelligently.
"""
left, right = 0, len(height) - 1
max_water = 0
while left < right:
# Calculate area with current pointers
current_area = min(height[left], height[right]) * (right - left)
max_water = max(max_water, current_area)
# Move the pointer pointing to the shorter wall
# (moving the taller wall's pointer can never help)
if height[left] < height[right]:
left += 1 # Left wall is the bottleneck; try a taller left wall
else:
right -= 1 # Right wall is the bottleneck (or equal); try a taller right wall
return max_water
## Example usage
height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
print(max_area_two_pointers(height)) # Output: 49
Notice how the outer loop runs at most n times β the two pointers march toward each other and meet in the middle. No nested loops. No redundant checks. Just a single, confident pass through the array.
π§ Mnemonic: "Shrink from the short side." Whenever you need to decide which pointer to move, always move the one pointing at the shorter wall. The tall wall is precious β it has more potential to contribute to future containers. Protect it.
Where This Fits in Your Two-Pointer Learning Path
The two-pointer pattern is one of the most versatile tools in a competitive programmer's toolkit. It appears in dozens of LeetCode problems across varying difficulty levels, and Container With Most Water is the ideal entry point because:
π§ The logic is visual. The water metaphor makes the pointer movement feel intuitive, not arbitrary. You can draw a picture of what's happening.
π The correctness proof is accessible. Unlike some algorithms where correctness requires deep mathematical machinery, the reasoning here can be explained in plain English β and once you see why it works, it sticks.
π§ The pattern generalizes cleanly. The core idea β start at the extremes, use a decision rule to eliminate impossible configurations, converge inward β appears in Two Sum II (sorted array), Trapping Rain Water, 3Sum, and many others.
π― The contrast with brute force is stark. Going from O(nΒ²) to O(n) is dramatic enough to make the lesson feel meaningful, not just academic.
π‘ Real-World Example: Two-pointer thinking mirrors real engineering decisions. Imagine you're optimizing a two-sided assembly line: one team works left-to-right, the other right-to-left. The bottleneck is always the slower team. You invest resources in speeding up the slower side β speeding up the already-faster side yields no throughput gain. This is exactly the logic behind moving the shorter-wall pointer.
Here's a visual map of how the two-pointer learning path typically unfolds:
[Container With Most Water] β You are here
|
βΌ
[Two Sum II - Sorted Input]
|
βΌ
[3Sum / 4Sum Variants]
|
βΌ
[Trapping Rain Water] β Advanced application
|
βΌ
[Sliding Window Variants] β Related family
Each step in this path builds on the intuition you develop here. The decision logic becomes more nuanced, the edge cases more subtle, but the core mental model β make intelligent, directional choices rather than exhaustive ones β remains constant.
β οΈ Common Mistake β Mistake 1: Learners sometimes memorize the two-pointer solution for this problem without understanding why the pointer movement rule works. When they encounter a variation β say, a problem where the cost function changes β they're stuck. Always understand the reasoning, not just the recipe. Section 2 of this lesson is dedicated entirely to the proof of correctness.
β οΈ Common Mistake β Mistake 2: Assuming that two-pointer always means starting from opposite ends. In some problems (like the sliding window pattern), both pointers start at the left. Container With Most Water belongs to the converging variant, and recognizing that distinction matters when you encounter new problems.
Setting Up the Journey Ahead
By the time you finish this three-part lesson, you'll be able to:
π― Recognize the container-style two-pointer pattern on sight and know immediately which strategy to apply.
π― Explain why each pointer movement is correct, not just what the code does β a critical distinction in technical interviews.
π― Identify at least five other LeetCode problems where this pattern or a close variant applies.
π― Avoid the common implementation pitfalls that trip up even experienced candidates under interview pressure.
The problem statement is deceptively simple. The brute force is obvious. But the space between "it works" and "it works efficiently, and here's exactly why" is where real algorithmic thinking lives. That's the space we're about to explore together.
Section 2 awaits β and with it, the full mathematical story of why the two-pointer approach is not just fast, but provably correct.
The Two-Pointer Strategy: Logic, Movement Rules, and Proof of Correctness
The brute-force approach to Container With Most Water checks every possible pair of lines β O(nΒ²) work that grinds to a halt on large inputs. The two-pointer strategy collapses that to a single linear pass, and understanding why it works is the real prize. This section gives you both the mechanics and the mathematical confidence that you are never missing the optimal answer.
Starting at Maximum Width: The Core Insight
The area of any container is determined by two quantities: the width between the two lines and the height of the shorter line. Formally:
area = min(height[left], height[right]) Γ (right - left)
The two-pointer technique begins by placing one pointer at the leftmost index (left = 0) and one at the rightmost index (right = len(height) - 1). This is not arbitrary β it is the single most important setup decision in the algorithm.
π― Key Principle: You start at maximum width because width can only decrease as pointers move inward. If you start anywhere else, you are voluntarily discarding potentially large containers before you ever evaluate them.
Think of it this way: every step inward sacrifices one unit of width. The only justification for making that sacrifice is the hope of gaining enough height to compensate. By starting at the widest possible configuration, you ensure that the very first area you compute is a meaningful candidate β and every subsequent move is a deliberate, justified trade-off.
height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
indices: 0 1 2 3 4 5 6 7 8
Initial state:
βββββββββββββββββββββββββββββββββββββββ
β L=0 R=8 β
β β β β
β 1 8 6 2 5 4 8 3 7β
β β β β
β left right β
β β
β width = 8 - 0 = 8 β
β height = min(1, 7) = 1 β
β area = 1 Γ 8 = 8 β
βββββββββββββββββββββββββββββββββββββββ
The Movement Rule: Always Advance the Shorter Side
After computing the current area and updating your running maximum, you must move one pointer inward. The rule is: advance the pointer whose height is smaller. If both heights are equal, move either one (both are equally valid).
π‘ Mental Model: Imagine you are negotiating a deal. The container's area is limited by the shorter wall β it is the bottleneck. Moving the taller wall inward is irrational because the bottleneck does not change (it is still the shorter wall), but the width shrinks. You must move the shorter wall to have any chance of a better deal.
Let's make this concrete with a logical argument:
Suppose height[left] < height[right]. The current area is height[left] Γ (right - left). Now ask: what happens if we move the right pointer inward to any position r' where r' < right?
- The new width is
(r' - left) < (right - left)β strictly smaller. - The new limiting height is
min(height[left], height[r']), which is at mostheight[left](sinceheight[left]is already the smaller side). - Therefore the new area β€
height[left] Γ (r' - left)<height[left] Γ (right - left)= current area.
β
Correct thinking: Every container that uses the current left pointer paired with any position to the left of right is provably worse than the current container. We can discard all of them by moving right inward β but wait, that means we should not move right. We should move left.
β Wrong thinking: "Moving the taller pointer might reveal a much taller line that compensates." Even if a taller line exists, the area is still capped by the shorter side (which did not move), and the width is now smaller. The math does not support it.
Proof of Correctness: Nothing Is Ever Missed
The formal guarantee of the algorithm rests on one claim: for every pair (i, j) that represents the optimal container, the algorithm will evaluate that pair or prove it unnecessary.
Here is the argument by contradiction. Assume the optimal pair is (i*, j*) with i* < j*, and assume the algorithm never evaluates it. For the algorithm to skip this pair, one of the two pointers must have "jumped over" one of these indices β meaning at some earlier step, left passed i* or right passed j*.
Without loss of generality, say left passed i*. That means at some step, left == i* and the algorithm moved it inward (to i* + 1). The movement rule says this happens only when height[left] β€ height[right], meaning height[i*] β€ height[right] at that moment. But then right is to the right of j* (since right has not yet passed j*). So the container (i*, right) at that moment has width β₯ (j* - i*) and height = min(height[i*], height[right]) β₯ min(height[i*], height[j*]). This means the container (i*, right) is at least as good as the supposed optimal (i*, j*) β contradicting the assumption that (i*, j*) is strictly better than everything evaluated. β
π― Key Principle: The algorithm does not evaluate every pair β it eliminates pairs that cannot beat the current best without looking at them, proving they are suboptimal.
Step-by-Step Code Walkthrough in Python
Here is the clean, annotated implementation:
def maxArea(height: list[int]) -> int:
left = 0 # Start left pointer at beginning
right = len(height) - 1 # Start right pointer at end (max width)
max_area = 0 # Track the best area seen so far
while left < right: # Continue until pointers meet
# Width is the distance between the two pointers
width = right - left
# Height is limited by the shorter of the two lines
current_height = min(height[left], height[right])
# Compute area and update the running maximum
current_area = width * current_height
max_area = max(max_area, current_area)
# Move the pointer with the smaller height inward
# Moving the taller pointer can never improve the result
if height[left] < height[right]:
left += 1 # Left is shorter β advance it
else:
right -= 1 # Right is shorter (or equal) β advance it
return max_area
Let's trace through height = [1, 8, 6, 2, 5, 4, 8, 3, 7] step by step:
Step β left β right β h[L] β h[R] β width β area β max β Move
ββββββΌβββββββΌββββββββΌβββββββΌβββββββΌββββββββΌβββββββΌβββββββΌββββββ
1 β 0 β 8 β 1 β 7 β 8 β 8 β 8 β L β
2 β 1 β 8 β 8 β 7 β 7 β 49 β 49 β R β
3 β 1 β 7 β 8 β 3 β 6 β 18 β 49 β R β
4 β 1 β 6 β 8 β 8 β 5 β 40 β 49 β R β
5 β 1 β 5 β 8 β 4 β 4 β 16 β 49 β R β
6 β 1 β 4 β 8 β 5 β 3 β 15 β 49 β R β
7 β 1 β 3 β 8 β 2 β 2 β 4 β 49 β R β
8 β 1 β 2 β 8 β 6 β 1 β 6 β 49 β R β
9 β left == right β loop ends β
Result: 49 β
β οΈ Common Mistake: Forgetting the left < right condition (using β€ instead). When both pointers land on the same index, the width is zero and the area is always zero β there is nothing to compute. The loop must terminate before they overlap.
Notice in Step 2 that the algorithm correctly keeps left = 1 (height 8) and moves right inward repeatedly β because height[1] = 8 is the tallest line in the array and should be paired with the next-best candidate on the right.
Complexity Analysis
Time Complexity β O(n): Each pointer starts at one end of the array. At every iteration, exactly one pointer moves one step inward. Since there are n elements, the two pointers together take at most n - 1 steps before they meet. The algorithm performs a constant amount of work per step, giving a tight O(n) bound.
Space Complexity β O(1): The algorithm uses only three integer variables β left, right, and max_area β regardless of the input size. No auxiliary arrays, no recursion stack, no dynamic allocation.
| Approach | Time | Space | Notes |
|---|---|---|---|
| π΄ Brute Force | O(nΒ²) | O(1) | Checks every pair |
| π’ Two Pointers | O(n) | O(1) | Single inward pass |
π€ Did you know? On an array of 100,000 elements, brute force performs roughly 5 billion comparisons. The two-pointer solution performs fewer than 100,000. That is a 50,000Γ speedup β not a small constant improvement, but an entirely different growth curve.
π‘ Pro Tip: Notice that both approaches use O(1) space. The two-pointer technique earns its efficiency purely through smarter logic β not by trading memory for speed. This makes it especially valuable in memory-constrained environments.
π§ Mnemonic: "Short side moves, tall side stays." The taller wall has done its job anchoring the current maximum; the only hope for improvement lies in replacing the shorter wall with something better.
Putting the Pieces Together
The elegance of this algorithm comes from a rare combination: it is simple to implement (twelve lines of clean Python), easy to trace by hand, and backed by a rigorous correctness proof. The movement rule is not a heuristic or a guess β it is a logical deduction. Every discarded pair is discarded because the math proves it cannot be optimal.
π Quick Reference Card:
| π― Decision Point | π Rule | π§ Reason |
|---|---|---|
| π Initialization | left=0, right=n-1 |
Maximize starting width |
| π― Area formula | min(h[L], h[R]) Γ (R-L) |
Shorter wall is the limit |
| π§ Move rule | Advance shorter pointer | Taller pointer move is provably worse |
| π§ Termination | left < right |
Zero-width container is meaningless |
| π Complexity | O(n) time, O(1) space | Single inward pass, no extra memory |
With this foundation firmly in place, the next section will test your understanding against tricky edge cases, explore common implementation pitfalls, and show you how to recognize this same two-pointer pattern hiding inside other LeetCode problems.
Putting It Into Practice: Edge Cases, Pitfalls, and Pattern Recognition
You now understand why the two-pointer algorithm works for Container With Most Water β the mathematical guarantee that moving the shorter pointer is always safe. But understanding a proof and writing bug-free code under interview pressure are two very different things. This final section bridges that gap. We'll walk through the mistakes that trip up even experienced engineers, stress-test the algorithm against tricky edge cases, and build a mental toolkit for recognizing when this exact pattern applies to problems you haven't seen before.
The Most Dangerous Mistake: Moving the Wrong Pointer
Let's start with the mistake that silently destroys correctness without triggering any obvious error.
β οΈ Common Mistake β Mistake 1: Moving the pointer with the larger height β οΈ
It seems intuitive at first glance. "The taller wall limits nothing β move it and maybe I'll find an even taller one." But this reasoning is fatally flawed.
β Wrong thinking: "The larger height isn't the bottleneck, so I should try replacing it with something bigger."
β Correct thinking: "The smaller height IS the bottleneck. No matter what wall I pair it with, the water level can never exceed it. The only hope for improvement is to move the shorter pointer and find a taller replacement."
Here's a concrete example to make this visceral. Suppose your heights are [8, 1, 6, 7, 3] and your pointers are at indices 0 (height 8) and 4 (height 3). The area is min(8, 3) * (4 - 0) = 12. If you mistakenly move the left pointer (the taller one), you jump to index 1 with height 1, giving you min(1, 3) * 3 = 3. You've just abandoned the optimal left wall and will never return to it.
The correct move is to advance the right pointer inward (from index 4 toward the center), because height 3 is the shorter side and is capping your potential.
π― Key Principle: The two-pointer algorithm's correctness guarantee rests entirely on one rule β always move the pointer with the smaller height. Breaking this rule means you may skip over the globally optimal pair without ever evaluating it.
## β
CORRECT implementation β move the shorter-side pointer
def maxArea(height):
left, right = 0, len(height) - 1
max_water = 0
while left < right:
# Calculate current area
width = right - left
current_area = min(height[left], height[right]) * width
max_water = max(max_water, current_area) # β
Update on EVERY iteration
# Move the pointer with the SMALLER height inward
if height[left] <= height[right]:
left += 1 # β
Correct: advance the shorter (or equal) side
else:
right -= 1 # β
Correct: advance the shorter side
return max_water
Notice the <= in the comparison. When heights are equal, it doesn't matter which pointer you move β both walls cap the container at the same level, and moving either one inward can only decrease the width. Moving the left pointer in this case is perfectly valid.
The Silent Bug: Updating maxArea in the Wrong Place
β οΈ Common Mistake β Mistake 2: Only updating maxArea outside or at the end of the loop β οΈ
This one is subtle. Consider this broken pattern:
## β BROKEN β maxArea updated in the wrong place
def maxArea_broken(height):
left, right = 0, len(height) - 1
max_water = 0
while left < right:
width = right - left
current_area = min(height[left], height[right]) * width
if height[left] <= height[right]:
left += 1
else:
right -= 1
# β BUG: Only the LAST computed area is saved here
max_water = current_area
return max_water
This code only stores the area from the final iteration β the pair of pointers right before they cross. The maximum area could have occurred at any point during the traversal. You must call max_water = max(max_water, current_area) inside the loop, on every single iteration, before any pointer moves.
π‘ Pro Tip: Think of max_water as a running scoreboard. Every time two pointers face each other, you record their score. The scoreboard only ever goes up, and you read the final score at the end.
Edge Cases: Does Your Solution Handle These?
Every interviewer worth their salt will probe these scenarios. Let's walk through each one methodically.
Edge Case 1: Array With Exactly Two Elements
With height = [4, 9], there's only one possible container. Your pointers start at left=0, right=1. They compute area min(4,9) * 1 = 4, then the loop condition left < right fails and the loop exits immediately. The algorithm handles this correctly with zero special-casing needed.
Edge Case 2: All Heights Equal
For height = [5, 5, 5, 5, 5], every step moves the left pointer (because height[left] <= height[right] always), and the maximum is found on the very first iteration: 5 * 4 = 20. The algorithm naturally finds this because the widest container (with identical heights) is evaluated first.
Edge Case 3: Strictly Increasing Heights
With height = [1, 2, 3, 4, 5], the right wall is always taller, so we always advance the left pointer. The algorithm evaluates pairs (0,4), (1,4), (2,4), (3,4) β shrinking width but gaining height. The maximum here is min(1,5)*4=4 vs min(2,5)*3=6 vs min(3,5)*2=6 vs min(4,5)*1=4. Correct answer: 6. The algorithm finds it.
Edge Case 4: Strictly Decreasing Heights
With height = [5, 4, 3, 2, 1], the left wall is always taller, so we always advance the right pointer inward. The algorithm evaluates (0,4), (0,3), (0,2), (0,1). Maximum: min(5,1)*4=4, min(5,2)*3=6, min(5,3)*2=6, min(5,4)*1=4. Correct answer: 6. β
Strictly Decreasing Heights: [5, 4, 3, 2, 1]
Step 1: L=0(5) R=4(1) β area = min(5,1)*4 = 4 max=4
Move R inward (1 < 5)
Step 2: L=0(5) R=3(2) β area = min(5,2)*3 = 6 max=6
Move R inward (2 < 5)
Step 3: L=0(5) R=2(3) β area = min(5,3)*2 = 6 max=6
Move R inward (3 < 5)
Step 4: L=0(5) R=1(4) β area = min(5,4)*1 = 4 max=6
Loop ends (L+1 = R, next move would cross)
Result: 6 β
π€ Did you know? For strictly decreasing arrays, the optimal container always involves the first (tallest) element as one wall. The two-pointer algorithm "discovers" this naturally without any special-case code.
Pattern Recognition: When Does This Technique Apply?
The real power of mastering Container With Most Water isn't solving this problem β it's developing an eye for the entire family of problems that yield to the same thinking.
π§ Mnemonic β The TWO-B Rule: Look for Two Boundaries that Operate Oppositely (one from each end) while Both contributing to a value you want to optimize.
Here are the signal patterns to watch for:
π§ Signal 1 β Two boundary indices matter: If the value you're computing depends on both a left and a right index simultaneously (not just one at a time), two pointers are worth considering.
π§ Signal 2 β Monotonic trade-off: If moving one pointer inward always decreases one factor (like width) while potentially improving another (like height), the two-pointer shrinking strategy may be provably safe.
π§ Signal 3 β Sorted or partially ordered structure: If the array has a predictable ordering (sorted, unimodal, non-decreasing from one side), you can often prove that one direction of pointer movement can be safely discarded.
π§ Signal 4 β O(nΒ²) brute force with nested loops: Any time you catch yourself writing for i in range(n): for j in range(i+1, n), ask: "Is there a two-pointer argument that eliminates the inner loop?"
Comparing Container With Most Water to Trapping Rain Water
These two problems are frequently confused β and studying the contrast sharpens your pattern recognition significantly.
| Container With Most Water | Trapping Rain Water | |
|---|---|---|
| π― Goal | Maximize a single rectangle | Sum trapped water across all positions |
| π Constraint | Only two walls matter | Every column's water depends on both sides |
| π§ Pointer Logic | Move shorter pointer inward | Track max-left and max-right for each column |
| π Complexity | O(n) time, O(1) space | O(n) time, O(1) space (two-pointer variant) |
| π§ Key Insight | Abandon shorter wall to maximize min() | Each cell fills to min(maxLeft, maxRight) - height |
Both problems use two pointers starting at opposite ends. But in Trapping Rain Water, you maintain running maximums from each side and compute water at every single cell β you can't skip any. In Container With Most Water, you're choosing one pair out of all possible pairs.
The Three-Step Mental Checklist
Before writing a single line of code on a two-pointer boundary problem, run through this checklist:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TWO-POINTER BOUNDARY CHECKLIST β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Step 1: IDENTIFY the two boundary values β
β β What are left and right contributing to the answer? β
β β Can you express the value as f(left, right)? β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Step 2: PROVE the movement rule is safe β
β β If I move the left pointer right, what do I lose? β
β β Can I prove the skipped configurations can't be better?β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Step 3: CONFIRM termination and completeness β
β β Does the loop end? (left < right shrinks every step) β
β β Is every promising pair considered at least once? β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π‘ Mental Model: Think of the two pointers as scouts on opposite ends of a valley. They walk toward each other. At each meeting point, they measure the landscape. The rule for which scout steps forward must be provably safe β meaning the scout who steps back can never be part of a better solution than what's already been recorded.
## Complete, clean, interview-ready solution
def maxArea(height: list[int]) -> int:
"""
Two-pointer approach: O(n) time, O(1) space.
Always advance the pointer with the smaller height.
"""
left, right = 0, len(height) - 1
max_water = 0
while left < right:
# Width shrinks as pointers converge
width = right - left
# Height is capped by the shorter wall
h = min(height[left], height[right])
# Record area before moving any pointer
max_water = max(max_water, h * width)
# The shorter wall can only get worse by staying;
# move it inward hoping for improvement
if height[left] < height[right]:
left += 1
else:
# Handles height[left] == height[right] too
right -= 1
return max_water
Summary: What You Now Understand
When you started this lesson, Container With Most Water may have looked like a clever trick problem. You now see it as a window into a family of provably-optimal greedy algorithms that eliminate work by reasoning about what configurations can never be optimal.
π Quick Reference Card: Container With Most Water β Full Summary
| π Concept | π What to Remember |
|---|---|
| π― Core Algorithm | Two pointers from ends, move shorter inward |
| β οΈ Fatal Mistake | Moving the taller pointer breaks correctness |
| π§ Why It Works | Taller pointer paired with shorter: any narrower pair with shorter wall is already dominated |
| π§ Update Rule | maxArea updated inside loop, every iteration |
| π Complexity | O(n) time / O(1) space |
| π Equal Heights | Move either pointer (both are valid) |
| π― Pattern Signal | Two boundaries + optimize f(magnitude, distance) |
| π Related Problem | Trapping Rain Water (sum across all cells, not one pair) |
β οΈ Final Critical Points:
- β οΈ Never move the larger pointer β this is the single most common correctness bug.
- β οΈ Always update
max_waterbefore moving any pointer, inside the loop. - β οΈ The proof of correctness is what makes this O(n) β without the proof, you don't know you're not skipping the answer.
Practical Next Steps
π Next Problem β Trapping Rain Water (LeetCode #42): Apply the two-pointer mental model to a harder variant. You'll use the same inward-converging structure but maintain maxLeft and maxRight running maximums to compute water at every cell.
π§ Generalize to Three Sum (LeetCode #15): This problem uses a sorted array and fixes one element with an outer loop, then applies the exact Container-style two-pointer logic on the remaining subarray. Your mental checklist applies directly.
π― Practice the Proof Habit: On your next LeetCode session, before coding any two-pointer solution, write one sentence in a comment: "I can skip configuration X because Y." This habit will catch bugs before they're written and impress interviewers who ask "how do you know this is correct?"
The journey from brute-force nested loops to clean O(n) pointer convergence is not just about this one problem. It's a shift in how you read arrays β not as sequences to exhaustively search, but as structures with exploitable properties that let you reason your way to the answer.