170 lines
7.1 KiB
YAML
170 lines
7.1 KiB
YAML
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
|
||
|
||
function_signature: "def max_area(height: list[int]) -> int:"
|
||
|
||
test_cases:
|
||
visible:
|
||
- input: { height: [1, 8, 6, 2, 5, 4, 8, 3, 7] }
|
||
expected: 49
|
||
- input: { height: [1, 1] }
|
||
expected: 1
|
||
hidden:
|
||
- input: { height: [4, 3, 2, 1, 4] }
|
||
expected: 16
|
||
- input: { height: [1, 2, 1] }
|
||
expected: 2
|
||
- input: { height: [2, 3, 10, 5, 7, 8, 9] }
|
||
expected: 36
|
||
- input: { height: [1, 2, 4, 3] }
|
||
expected: 4
|
||
- input: { height: [1, 1, 1, 1, 1, 1, 1, 1] }
|
||
expected: 7
|
||
- input: { height: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] }
|
||
expected: 25
|
||
- input: { height: [1, 3, 2, 5, 25, 24, 5] }
|
||
expected: 24
|
||
|
||
description: |
|
||
You are given an integer array `height` of length `n`. There are `n` vertical lines drawn such that the two endpoints of the i<sup>th</sup> 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).
|