Topics › Sliding Window and Two Pointers › Medium Problems

Container with most water

Easy TUF ID 2396 Blind 75

Problem

You are given an array height of n non-negative integers. Picture n vertical lines at x-positions 0, 1, ..., n - 1, where line i goes from (i, 0) up to (i, height[i]). Choose two lines i < j. Together with the x-axis they form a container that holds

water(i, j) = min(height[i], height[j]) × (j - i)

because the water level cannot rise above the shorter line. Return the maximum water any pair can hold. The lines between i and j do not matter; the container is just the two walls.

Constraints (LeetCode 11): 2 ≤ n ≤ 10^5, 0 ≤ height[i] ≤ 10^4. The methods return long so they stay correct even for heights up to Integer.MAX_VALUE, and they return 0 when fewer than two lines exist.

Example 1

height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
answer = 49         // lines at x = 1 (height 8) and x = 8 (height 7): min(8, 7) × 7

Example 2

height = [4, 3, 2, 1, 4]
answer = 16         // the two outer lines: min(4, 4) × 4

Do not confuse this with “Trapping Rain Water”, where every bar holds water above it and the bars in between matter. Here only two lines are chosen and the rest are ignored.

Approach 1: Brute force

Intuition. Every container is a pair of lines, so evaluate every pair and keep the best. Starting j at i + 1 covers each pair exactly once.

Algorithm.

  1. For every i, for every j > i: compute min(height[i], height[j]) × (j - i).
  2. Track the maximum.
static long bruteForce(int[] height) {
    long best = 0;
    for (int i = 0; i < height.length; i++) {
        for (int j = i + 1; j < height.length; j++) {
            long area = (long) Math.min(height[i], height[j]) * (j - i);
            best = Math.max(best, area);
        }
    }
    return best;
}

Complexity. Time O(n^2): n(n - 1)/2 pairs. Space O(1).

For n = 10^5 that is about 5 × 10^9 pairs. We need a way to rule out most pairs without evaluating them.

Approach 2: Optimal, two pointers

Intuition. Start with the widest possible container: i = 0 and j = n - 1. Every other container is narrower, so the only way to beat the current one is with a taller limiting line. Now look at the shorter of the two lines, say height[i] ≤ height[j]. Pair line i with any other partner k between them (i < k < j):

So every container that uses line i with a closer partner is no better than the one we just measured. Line i has nothing left to offer, and we can discard it by moving i one step right. Symmetrically, if height[j] is the shorter, discard j. Each step discards one line for good, so after at most n - 1 steps the pointers meet.

Moving the taller line instead would be a mistake: the shorter line stays as the limit, the width shrinks, and the area can only go down, while we might throw away the taller line’s best partner.

Algorithm.

  1. i = 0, j = n - 1, best = 0.
  2. While i < j:
    • best = max(best, min(height[i], height[j]) × (j - i)).
    • If height[i] < height[j], do i++; otherwise do j--.
  3. Return best.
static long optimal(int[] height) {
    long best = 0;
    int i = 0, j = height.length - 1;
    while (i < j) {
        long area = (long) Math.min(height[i], height[j]) * (j - i);
        best = Math.max(best, area);
        if (height[i] < height[j]) i++;                  // the shorter line cannot do better with any closer partner
        else j--;
    }
    return best;
}

Dry run on Example 1, height = [1, 8, 6, 2, 5, 4, 8, 3, 7]:

i (h) j (h) area best move
0 (1) 8 (7) 1 × 8 = 8 8 i++ (1 < 7)
1 (8) 8 (7) 7 × 7 = 49 49 j–
1 (8) 7 (3) 3 × 6 = 18 49 j–
1 (8) 6 (8) 8 × 5 = 40 49 j– (equal heights)
1 (8) 5 (4) 4 × 4 = 16 49 j–
1 (8) 4 (5) 5 × 3 = 15 49 j–
1 (8) 3 (2) 2 × 2 = 4 49 j–
1 (8) 2 (6) 6 × 1 = 6 49 j–, pointers meet

Complexity. Time O(n): each iteration moves one pointer inward by one. Space O(1).

Why the pruning never loses the optimum. Let (a, b) be an optimal pair with a positive amount of water (if the best is 0, returning 0 is already right). The algorithm measures (a, b) unless it discards a or b first. Suppose the left pointer sits on a while the right pointer is at some j > b. The algorithm would discard a only if height[a] < height[j]. But then the pair (a, j) has height height[a] ≥ min(height[a], height[b]) and width j - a > b - a, so it holds strictly more water than (a, b), which contradicts optimality. So a survives until the right pointer comes down to b. The mirror argument shows b survives while the left pointer moves up to a. Therefore the pointers eventually sit on a and b at the same time, and the optimum is measured.

Edge cases and pitfalls