title: Container With Most Water slug: container-with-most-water difficulty: medium leetcode_id: 11 leetcode_url: https://leetcode.com/problems/container-with-most-water/ categories: - arrays - two-pointers patterns: - two-pointers - greedy description: | You are given an integer array `height` of length `n`. There are `n` vertical lines drawn such that the two endpoints of the ith line are `(i, 0)` and `(i, height[i])`. Find two lines that together with the x-axis form a container, such that the container contains the most water. Return *the maximum amount of water a container can store*. **Note:** You may not slant the container. constraints: | - `n == height.length` - `2 <= n <= 10^5` - `0 <= height[i] <= 10^4` examples: - input: "height = [1,8,6,2,5,4,8,3,7]" output: "49" explanation: "Lines at indices 1 (height 8) and 8 (height 7) form a container with area = min(8, 7) × (8 - 1) = 7 × 7 = 49." - input: "height = [1,1]" output: "1" explanation: "The only possible container has area = min(1, 1) × 1 = 1." explanation: intuition: | Visualise the problem: you have vertical lines of varying heights, and you want to trap the most water between two of them. The water level is limited by the **shorter** line (water would spill over), and the width is the distance between the lines. Think of it like this: `Area = width × min(left_height, right_height)` If we start with pointers at both ends, we have **maximum width**. The only way to potentially increase area is to find taller lines. But here's the key insight: If `height[left] < height[right]`, the current area is limited by `height[left]`. Moving `right` inward would: - **Decrease** width (always) - Keep height limited by `height[left]` (at best) or make it smaller So moving the **taller** pointer can never improve the area! We should always move the **shorter** pointer, hoping to find a taller line that compensates for the reduced width. This greedy choice is provably optimal because we're eliminating pairs that cannot possibly be better than what we've already found. approach: | We solve this using **Two Pointers with Greedy Movement**: **Step 1: Initialise pointers and tracking variable** - `left = 0` (start of array) - `right = len(height) - 1` (end of array) - `max_water = 0` to track the best area found   **Step 2: Calculate area and move pointers** - While `left < right`: - Calculate current area: `width × min(height[left], height[right])` - Update `max_water` if current area is larger - Move the pointer pointing to the **shorter** line inward: - If `height[left] < height[right]`: increment `left` - Otherwise: decrement `right`   **Step 3: Return the maximum** - After pointers meet, return `max_water`   This works because moving the shorter pointer gives us the only chance to find a better solution. Moving the taller pointer cannot improve the area — it's mathematically impossible. common_pitfalls: - title: Moving the Wrong Pointer description: | The algorithm only works if you move the **shorter** pointer. Moving the taller one (or moving randomly) breaks the correctness proof. Why? If `height[left] < height[right]`, the area is constrained by `height[left]`. Any container with `left` as one boundary cannot have more water than we've already calculated (width would be smaller, height at most `height[left]`). Moving `left` eliminates these inferior possibilities and might find a taller left boundary. wrong_approach: "Always move left pointer, or move randomly" correct_approach: "Always move the pointer with the smaller height" - title: Using Nested Loops (Brute Force) description: | Checking all O(n²) pairs works but is far too slow for `n = 10^5` (10 billion operations). The two-pointer approach achieves O(n) by intelligently eliminating pairs that cannot be optimal. Each pointer moves at most n times total. wrong_approach: "for i in range(n): for j in range(i+1, n): ..." correct_approach: "Two pointers moving inward based on height comparison" - title: Forgetting to Update Maximum description: | Calculate the area **before** moving the pointer, and always update `max_water`. It's easy to forget one of these steps and miss the optimal answer. wrong_approach: "Moving pointer before calculating area" correct_approach: "Calculate area first, update max, then move pointer" key_takeaways: - "**Two pointers from opposite ends**: A powerful pattern for optimization on sorted/indexed data" - "**Move the limiting factor**: When one element constrains the result, changing it gives the best chance of improvement" - "**Greedy with proof**: This isn't just heuristic — moving the shorter pointer is provably optimal" - "**O(n) vs O(n²)**: Two pointers can eliminate the need for nested loops when the problem has the right structure" time_complexity: "O(n). Each pointer moves at most n times, and we do O(1) work per step." space_complexity: "O(1). Only a few variables for pointers and the maximum area." solutions: - approach_name: Two Pointers is_optimal: true code: | def max_area(height: list[int]) -> int: left = 0 right = len(height) - 1 max_water = 0 while left < right: # Calculate width and height of current container width = right - left h = min(height[left], height[right]) # Update maximum if this container is larger max_water = max(max_water, width * h) # Move the pointer pointing to the shorter line # (moving the taller one can't improve the result) if height[left] < height[right]: left += 1 else: right -= 1 return max_water explanation: | **Time Complexity:** O(n) — Each pointer moves at most n positions total. **Space Complexity:** O(1) — Only constant extra space used. We start with maximum width and greedily try to find taller lines. By always moving the shorter pointer, we ensure we don't miss any potentially better containers. The proof: any container involving the shorter line at its current position has already been considered (or would have less area due to reduced width).