๐ŸŽ“ A1Learner
๐Ÿ”

DSA Container with Most Water

Two vertical walls and the gap between them form a container that holds an area of water. The shorter wall sets the level, so the optimal scan moves the shorter wall inward each step.

The problem

Container With Most Water places vertical walls in a row. Two walls and the space between them form a container. The task is the largest amount of water any pair of walls can hold.

The water amount is an area. The area is the gap between the walls times the height of the shorter wall. The shorter wall sets the water level, because water spills over it first.

The cases used in this article

cases = [
    ([1, 8, 6, 2, 5, 4, 8, 3, 7], 49),
    ([1, 1], 1),
    ([4, 3, 2, 1, 4], 16),
    ([1, 2, 1], 2),
]
for heights, expected in cases:
    print(heights, 'best area:', expected)

Intuition

The widest pair is the first idea. Take the two outermost walls. The gap is as large as it can be.

Width alone does not win. A short outer wall caps the level, and a narrower pair of tall walls can hold far more. Fixing one wall and scanning partners has the same flaw in reverse: the fixed wall may be the cap that no partner can lift.

The widest pair against a narrower taller pair

heights = [1, 8, 6, 2, 5, 4, 8, 3, 7]
wide = (8 - 0) * min(heights[0], heights[8])
tall = (8 - 1) * min(heights[1], heights[8])
print('widest pair, walls 1 and 7: area', wide)
print('narrower pair, walls 8 and 7: area', tall)

Brute force

The brute force tries every pair of walls. Each pair gives one area. The largest area wins.

Trying every pair of walls

def max_area_brute(heights):
    best = 0
    pairs = 0
    for left in range(len(heights)):
        for right in range(left + 1, len(heights)):
            pairs += 1
            area = (right - left) * min(heights[left], heights[right])
            if area > best:
                best = area
    return best, pairs

for heights in [[1, 8, 6, 2, 5, 4, 8, 3, 7], [4, 3, 2, 1, 4], [1, 2, 1]]:
    best, pairs = max_area_brute(heights)
    print(heights, 'best:', best, 'pairs tried:', pairs)

The answer is correct. The pair count grows with the square of the wall count. Most of those pairs can be ruled out without scoring them.

The optimal solution: move the shorter wall

The key idea is to move the shorter wall inward. Moving the taller wall keeps the same level and only shrinks the gap, so that move can never improve the area.

The two-pointers solution

def max_area(heights):
    left = 0
    right = len(heights) - 1
    best = 0
    while left < right:
        area = (right - left) * min(heights[left], heights[right])
        if area > best:
            best = area
        if heights[left] < heights[right]:
            left += 1
        else:
            right -= 1
    return best

cases = [[1, 8, 6, 2, 5, 4, 8, 3, 7], [1, 1], [4, 3, 2, 1, 4], [1, 2, 1]]
for heights in cases:
    print(heights, 'best area:', max_area(heights))

Walkthrough

Take the walls [1, 8, 6, 2, 5, 4, 8, 3, 7]. The pointers start on the outermost walls. Each step scores one container and then moves the shorter wall.

Each container scored, in order

heights = [1, 8, 6, 2, 5, 4, 8, 3, 7]
left = 0
right = len(heights) - 1
best = 0
while left < right:
    area = (right - left) * min(heights[left], heights[right])
    if area > best:
        best = area
    print('walls', heights[left], 'and', heights[right], 'area', area, 'best', best)
    if heights[left] < heights[right]:
        left += 1
    else:
        right -= 1
print('final best:', best)

The first container scores only 8. Its left wall of height 1 is the cap, so that wall moves. The best container pairs the walls of height 8 and 7, for 7 times 7, which is 49.

Complexity

Time is O(n). Each step moves one pointer inward, and each wall leaves the scan once.

Space is O(1). Two indices, one area and one best value are the only extra storage.

Counting pairs against pointer steps

def brute_pairs(n):
    return n * (n - 1) // 2

for n in [9, 20, 40]:
    print('walls', n, 'brute pairs:', brute_pairs(n), 'pointer steps at most:', n - 1)

Edge cases and pitfalls

Two walls form exactly one container. The loop scores that pair once and ends. Walls of equal height move the right pointer in this version, and either move is safe because the level cannot rise while the gap shrinks.

Moving the taller wall is the central pitfall. The level stays capped by the shorter wall. The gap only shrinks, so every later container with that pair of ends is worse.

Edge cases, run

def max_area(heights):
    left = 0
    right = len(heights) - 1
    best = 0
    while left < right:
        area = (right - left) * min(heights[left], heights[right])
        if area > best:
            best = area
        if heights[left] < heights[right]:
            left += 1
        else:
            right -= 1
    return best

print('two walls:', max_area([1, 1]))
print('all equal:', max_area([3, 3, 3, 3]))
print('falling:', max_area([5, 4, 3, 2, 1]))
print('zero walls:', max_area([0, 0, 0]))
print('one tall middle:', max_area([1, 9, 1]))

Exercise

Walls of height 4 and 9 stand five steps apart. Which wall moves next, and why?