11. Container With Most Water
Problem Description
You have 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. Specifically, the i
-th line has endpoints at coordinates (i, 0)
and (i, height[i])
.
Your task is to select any two lines from these n
vertical lines that, together with the x-axis, form a container. The container will hold water, and the amount of water it can hold is determined by:
- The distance between the two selected lines (width of the container)
- The height of the shorter of the two selected lines (since water would overflow from the shorter side)
The area of water the container can hold is calculated as: min(height[left], height[right]) ร (right - left)
, where left
and right
are the indices of the two selected lines.
You need to find the maximum amount of water that can be stored by choosing the optimal pair of lines. Note that the container cannot be tilted or slanted - it must remain upright.
For example, if height = [1,8,6,2,5,4,8,3,7]
, you might choose the lines at indices 1 and 8 (with heights 8 and 7). The container width would be 8 - 1 = 7
, and the water height would be min(8, 7) = 7
, giving a total area of 7 ร 7 = 49
.
Return the maximum area of water that can be contained.
Intuition
Let's start with the brute force approach: we could check every possible pair of lines and calculate the water area for each pair, keeping track of the maximum. This would require checking n ร (n-1) / 2
pairs, giving us O(nยฒ)
time complexity. Can we do better?
The key insight is that we can use two pointers starting from the widest possible container - one at the leftmost line (l = 0
) and one at the rightmost line (r = n-1
). This gives us the maximum possible width to start with.
Now, why can we safely move pointers inward without missing the optimal solution? Consider what happens when we have two lines at positions l
and r
:
- The current area is
min(height[l], height[r]) ร (r - l)
- If we move either pointer inward, the width
(r - l)
will definitely decrease - For the area to increase despite the reduced width, we need a taller line
Here's the crucial observation: we should always move the pointer pointing to the shorter line. Why?
If height[l] < height[r]
and we move the right pointer left, any new container formed with the current left line will have:
- A smaller width than before
- A height that's still limited by
height[l]
(since it's the shorter one)
This means moving the pointer at the taller line can never give us a better result with the current shorter line. However, if we move the pointer at the shorter line, we might find a taller line that could potentially create a larger area despite the reduced width.
This greedy strategy ensures we explore all potentially optimal solutions while eliminating combinations that cannot possibly be better than what we've already found. By always moving the pointer at the shorter line, we're essentially saying: "This shorter line is the limiting factor, let's try to find a better one."
The algorithm continues until the two pointers meet, at which point we've considered all viable containers and can return the maximum area found.
Learn more about Greedy and Two Pointers patterns.
Solution Approach
We implement the two-pointer technique to solve this problem efficiently in O(n)
time.
Initial Setup:
- Initialize two pointers:
l = 0
(left pointer at the start) andr = len(height) - 1
(right pointer at the end) - Initialize
ans = 0
to track the maximum water area found
Main Algorithm:
We use a while loop that continues as long as l < r
:
-
Calculate Current Area:
- The current water area is
min(height[l], height[r]) * (r - l)
- Store this in a variable
t
- Update
ans
to be the maximum of the currentans
andt
- The current water area is
-
Move Pointers:
- Compare the heights at the two pointers
- If
height[l] < height[r]
, incrementl
(move left pointer right) - Otherwise, decrement
r
(move right pointer left) - This ensures we always move the pointer pointing to the shorter line
Why This Works:
The algorithm systematically explores potentially optimal containers while pruning those that cannot be better:
- Starting with the widest container guarantees we don't miss any wide containers that might be optimal
- Moving the shorter line's pointer is optimal because keeping it while reducing width cannot improve the area
- Each iteration either finds a better solution or eliminates a line from consideration
- After
n-1
iterations, all possible optimal containers have been considered
Time and Space Complexity:
- Time:
O(n)
- each element is visited at most once as pointers move toward each other - Space:
O(1)
- only using a constant amount of extra space for pointers and variables
The final result ans
contains the maximum water area that can be stored.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with height = [1, 3, 2, 4, 2]
.
Initial Setup:
- Left pointer
l = 0
(height = 1) - Right pointer
r = 4
(height = 2) - Maximum area
ans = 0
Iteration 1:
- Current container: lines at indices 0 and 4
- Width = 4 - 0 = 4
- Height = min(1, 2) = 1 (limited by the shorter line)
- Area = 1 ร 4 = 4
- Update:
ans = 4
- Since height[0] = 1 < height[4] = 2, move left pointer:
l = 1
Iteration 2:
- Current container: lines at indices 1 and 4
- Width = 4 - 1 = 3
- Height = min(3, 2) = 2
- Area = 2 ร 3 = 6
- Update:
ans = 6
(new maximum!) - Since height[1] = 3 > height[4] = 2, move right pointer:
r = 3
Iteration 3:
- Current container: lines at indices 1 and 3
- Width = 3 - 1 = 2
- Height = min(3, 4) = 3
- Area = 3 ร 2 = 6
- Keep:
ans = 6
(no improvement) - Since height[1] = 3 < height[3] = 4, move left pointer:
l = 2
Iteration 4:
- Current container: lines at indices 2 and 3
- Width = 3 - 2 = 1
- Height = min(2, 4) = 2
- Area = 2 ร 1 = 2
- Keep:
ans = 6
(no improvement) - Since height[2] = 2 < height[3] = 4, move left pointer:
l = 3
Termination:
- Now
l = 3
andr = 3
, sol < r
is false - Algorithm terminates
- Return
ans = 6
The maximum water area is 6, formed by the lines at indices 1 and 4, with a width of 3 and height of 2.
Solution Implementation
1class Solution:
2 def maxArea(self, height: List[int]) -> int:
3 # Initialize two pointers at the beginning and end of the array
4 left = 0
5 right = len(height) - 1
6 max_area = 0
7
8 # Use two-pointer approach to find the maximum area
9 while left < right:
10 # Calculate the area with current left and right boundaries
11 # Area = width * height (where height is the minimum of the two sides)
12 width = right - left
13 current_height = min(height[left], height[right])
14 current_area = width * current_height
15
16 # Update maximum area if current area is larger
17 max_area = max(max_area, current_area)
18
19 # Move the pointer pointing to the shorter line
20 # This is optimal because moving the taller line would never increase the area
21 if height[left] < height[right]:
22 left += 1
23 else:
24 right -= 1
25
26 return max_area
27
1class Solution {
2 /**
3 * Finds the maximum area of water that can be contained between two vertical lines.
4 * Uses two-pointer technique to efficiently find the optimal container.
5 *
6 * @param height Array where height[i] represents the height of line at position i
7 * @return Maximum area of water that can be contained
8 */
9 public int maxArea(int[] height) {
10 // Initialize two pointers at the start and end of the array
11 int leftPointer = 0;
12 int rightPointer = height.length - 1;
13
14 // Variable to track the maximum area found so far
15 int maxAreaFound = 0;
16
17 // Continue until the two pointers meet
18 while (leftPointer < rightPointer) {
19 // Calculate current area: minimum height * width between pointers
20 int currentHeight = Math.min(height[leftPointer], height[rightPointer]);
21 int currentWidth = rightPointer - leftPointer;
22 int currentArea = currentHeight * currentWidth;
23
24 // Update maximum area if current area is larger
25 maxAreaFound = Math.max(maxAreaFound, currentArea);
26
27 // Move the pointer with smaller height inward
28 // This greedy approach works because moving the larger height
29 // can never increase the area (width decreases, height is limited by smaller side)
30 if (height[leftPointer] < height[rightPointer]) {
31 leftPointer++;
32 } else {
33 rightPointer--;
34 }
35 }
36
37 return maxAreaFound;
38 }
39}
40
1class Solution {
2public:
3 int maxArea(vector<int>& height) {
4 // Initialize two pointers: left at the beginning, right at the end
5 int left = 0;
6 int right = height.size() - 1;
7 int maxWater = 0;
8
9 // Use two-pointer technique to find maximum water container
10 while (left < right) {
11 // Calculate current water area: minimum height ร width
12 // Water level is limited by the shorter line
13 int currentArea = min(height[left], height[right]) * (right - left);
14
15 // Update maximum water area if current is larger
16 maxWater = max(maxWater, currentArea);
17
18 // Move the pointer pointing to the shorter line inward
19 // This greedy approach works because:
20 // - Moving the shorter line might find a taller line (potentially increasing area)
21 // - Moving the taller line will definitely decrease width without guarantee of height increase
22 if (height[left] < height[right]) {
23 ++left; // Move left pointer rightward if left line is shorter
24 } else {
25 --right; // Move right pointer leftward if right line is shorter (or equal)
26 }
27 }
28
29 return maxWater;
30 }
31};
32
1/**
2 * Finds the maximum area of water that can be contained between two vertical lines
3 * Uses two-pointer technique to efficiently find the optimal container
4 * @param height - Array of non-negative integers representing line heights
5 * @returns Maximum water area that can be contained
6 */
7function maxArea(height: number[]): number {
8 // Initialize two pointers at the start and end of the array
9 let left: number = 0;
10 let right: number = height.length - 1;
11
12 // Track the maximum area found so far
13 let maxAreaValue: number = 0;
14
15 // Continue until the two pointers meet
16 while (left < right) {
17 // Calculate current area: minimum height ร width between pointers
18 const currentArea: number = Math.min(height[left], height[right]) * (right - left);
19
20 // Update maximum area if current area is larger
21 maxAreaValue = Math.max(maxAreaValue, currentArea);
22
23 // Move the pointer pointing to the shorter line inward
24 // This greedy approach ensures we explore potentially larger areas
25 if (height[left] < height[right]) {
26 left++;
27 } else {
28 right--;
29 }
30 }
31
32 return maxAreaValue;
33}
34
Time and Space Complexity
Time Complexity: O(n)
, where n
is the length of the array height
. The algorithm uses a two-pointer approach that starts from both ends of the array. In each iteration of the while loop, either the left pointer l
moves right or the right pointer r
moves left, but never both. Since the pointers start at opposite ends and move toward each other, they will meet after at most n-1
iterations. Each iteration performs constant time operations (calculating area, comparing values, updating pointers), resulting in a linear time complexity.
Space Complexity: O(1)
. The algorithm only uses a fixed amount of extra space regardless of the input size. The variables l
, r
, ans
, and t
are the only additional storage used, and their space requirement doesn't scale with the input size n
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Moving Both Pointers Simultaneously
A common mistake is trying to "optimize" by moving both pointers when heights are equal, thinking it might speed up the algorithm:
Incorrect approach:
if height[left] < height[right]: left += 1 elif height[left] > height[right]: right -= 1 else: # Wrong: Moving both pointers when equal left += 1 right -= 1
Why it's wrong: Moving both pointers simultaneously can cause you to skip potentially optimal containers. For example, if both current heights are equal but the next element from one side is much taller, you'd miss that combination.
Correct approach: Move only one pointer even when heights are equal:
if height[left] < height[right]: left += 1 else: right -= 1 # This handles both > and == cases
2. Incorrect Area Calculation
Another pitfall is calculating the area incorrectly by using indices instead of the distance between them:
Incorrect:
current_area = min(height[left], height[right]) * min(left, right)
# or
current_area = min(height[left], height[right]) * max(left, right)
Correct:
current_area = min(height[left], height[right]) * (right - left)
3. Off-by-One Errors with Initial Pointer Positions
Some might incorrectly initialize pointers:
Incorrect:
left = 0
right = len(height) # Wrong: index out of bounds
Correct:
left = 0
right = len(height) - 1 # Correct: last valid index
4. Forgetting Edge Cases
Not handling arrays with fewer than 2 elements (though most problems guarantee n โฅ 2):
Robust solution:
def maxArea(self, height: List[int]) -> int:
if len(height) < 2:
return 0
left = 0
right = len(height) - 1
max_area = 0
# ... rest of the code
Which type of traversal does breadth first search do?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donโt Miss This!