You are given an array of numbers, each representing the height of a vertical line on a graph. A container can be formed with any pair of these lines, along with the x-axis of the graph. Return the amount of water which the largest container can hold.
Input: heights = [2, 7, 8, 3, 7, 6]
Output: 24
New to this one? Start here. Below is the core idea in everyday language, plus a real-world analogy—then the sections that follow build on it with the formal reasoning, code, and step-by-step walkthroughs.
Two people hold up a flexible sheet between them to catch rainwater. The amount of water they catch depends on two things: how far apart they stand (width) and the height of the shorter person (because water pours over whichever side is lower). Start standing as far apart as possible. To try for more water, it never helps to move the taller person in — the short person still caps the water. So always step the shorter person inward and hope to find a taller one.
If we have two vertical lines, heights[i] and heights[j], the amount of water that can be contained between these two lines is min(heights[i], heights[j]) * (j - i), where j - i represents the width of the container. We take the minimum height because filling water above this height would result in overflow.
In other words, the area of the container depends on two things:
The brute force approach to this problem involves checking all pairs of lines, and returning the largest area found between each pair:
from typing import List
def largest_container_brute_force(heights: List[int]) -> int:
n = len(heights)
max_water = 0
# Find the maximum amount of water stored between all pairs of lines.
for i in range(n):
for j in range(i + 1, n):
water = min(heights[i], heights[j]) * (j - i)
max_water = max(max_water, water)
return max_water#include <vector>
#include <algorithm>
using namespace std;
int largestContainerBruteForce(vector<int>& heights) {
int n = heights.size();
int maxWater = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) { // every pair of lines
int water = min(heights[i], heights[j]) * (j - i);
if (water > maxWater) {
maxWater = water;
}
}
}
return maxWater;
}// This method lives inside a class, e.g. class Solution { ... }
public int largestContainerBruteForce(int[] heights) {
int n = heights.length;
int maxWater = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) { // every pair of lines
int water = Math.min(heights[i], heights[j]) * (j - i);
if (water > maxWater) {
maxWater = water;
}
}
}
return maxWater;
}Searching through all possible pairs of values takes time, where denotes the length of the array. Let's look for a more efficient solution.
We would like both the height and width to be as large as possible to have the largest container.
It’s not immediately obvious how to find the container with the largest height, as the heights of the lines in the array don’t follow a clear pattern. However, we do know the container with the maximum width: the one starting at index 0 and ending at index n - 1.
So, we could start by maximizing the width by setting a pointer at each end of the array. Then, we can gradually reduce the width by moving these two pointers inward, hoping to find a container with a larger height that potentially yields a larger area. This suggests we can use the two-pointer pattern to solve the problem.
Moving a pointer inward means shifting either the left pointer to the right, or the right pointer to the left, effectively narrowing the gap between them.
Consider the following example:
The widest container can store an area of water equal to 10. Since this is the largest container we’ve found so far, let’s set max_water to 10.
How should we proceed? Moving either pointer inward yields a container with a shorter width. This leaves height as the determining factor. In this case, the left line is shorter than the right line, which means that the left line limits the water's height. Therefore, to find a larger container, let's move the left pointer inward:
The current container can hold 24 units of water, the largest amount so far. So, let’s update max_water to 24. Here, the right line is shorter, limiting the water's height. To find a larger container, move the right pointer inward:
After this, we encounter a situation where the height of the left and right lines are equal. In this situation, which pointer should we move inward? Well, regardless of which one, the next container is guaranteed to store less water than the current one. Let’s try to understand why.
Moving either pointer inward yields a container of shorter width, leaving height as the determining factor. However, regardless of which pointer we move inward, the other pointer remains at the same line. So, even if a pointer is moved to a taller line, the other pointer will restrict the height of the water, as we take the minimum of the two lines.
Therefore, since we can’t increase height by moving just one pointer, we can just move both pointers inward:
Now, the right line is limiting the height of the water. So, we move the right pointer inward:
Finally, the left and right pointers meet. We can conclude our search here and return max_water:
Based on the decisions taken in the example, we can summarize the logic:
If the left line is smaller, move the left pointer inward.
If the right line is smaller, move the right pointer inward.
If both lines have the same height, move both pointers inward.
from typing import List
def largest_container(heights: List[int]) -> int:
max_water = 0
left, right = 0, len(heights) - 1
while (left < right):
# Calculate the water contained between the current pair of lines.
water = min(heights[left], heights[right]) * (right - left)
max_water = max(max_water, water)
# Move the pointers inward, always moving the pointer at the shorter line. If
# both lines have the same height, move both pointers inward.
if (heights[left] < heights[right]):
left += 1
elif (heights[left] > heights[right]):
right -= 1
else:
left += 1
right -= 1
return max_water#include <vector>
#include <algorithm>
using namespace std;
int largestContainer(vector<int>& heights) {
int maxWater = 0;
int left = 0;
int right = heights.size() - 1;
while (left < right) {
// Water held by the current pair of lines.
int water = min(heights[left], heights[right]) * (right - left);
if (water > maxWater) {
maxWater = water;
}
// Move the pointer at the shorter line inward.
if (heights[left] < heights[right]) {
left++;
} else if (heights[left] > heights[right]) {
right--;
} else { // equal heights: move both
left++;
right--;
}
}
return maxWater;
}// This method lives inside a class, e.g. class Solution { ... }
public int largestContainer(int[] heights) {
int maxWater = 0;
int left = 0;
int right = heights.length - 1;
while (left < right) {
// Water held by the current pair of lines.
int water = Math.min(heights[left], heights[right]) * (right - left);
if (water > maxWater) {
maxWater = water;
}
// Move the pointer at the shorter line inward.
if (heights[left] < heights[right]) {
left++;
} else if (heights[left] > heights[right]) {
right--;
} else { // equal heights: move both
left++;
right--;
}
}
return maxWater;
}Time complexity: The time complexity of largest_container is because we perform approximately iterations using the two-pointer technique.
Space complexity: We only allocated a constant number of variables, so the space complexity is .
Two step-by-step walkthroughs of the algorithm below: a valid case (the expected happy path) and an invalid / edge case that exercises the tricky parts. Each step shows the program state as the code runs—the same steps apply to Python, C++ and Java.
left line right line water = min(heights) × width
Result: 24 — the container between index 1 and index 5.
| left | right | h[left] | h[right] | width | water | best | Action |
|---|---|---|---|---|---|---|---|
| 0 | 5 | 2 | 6 | 5 | 10 | 10 | left shorter → left→1 |
| 1 | 5 | 7 | 6 | 4 | 24 | 24 | right shorter → right→4 |
| 1 | 4 | 7 | 7 | 3 | 21 | 24 | equal → left→2, right→3 |
| 2 | 3 | 8 | 3 | 1 | 3 | 24 | right shorter → right→2 (meet) |
left line right line water = min(heights) × width
Result: 12 — on a rising wall the best container appears early (indexes 2–5 and 3–5 both give 12); after that every step is strictly narrower.
| left | right | h[left] | h[right] | width | water | best | Action |
|---|---|---|---|---|---|---|---|
| 0 | 5 | 1 | 10 | 5 | 5 | 5 | left shorter → left→1 |
| 1 | 5 | 2 | 10 | 4 | 8 | 8 | left shorter → left→2 |
| 2 | 5 | 4 | 10 | 3 | 12 | 12 | left shorter → left→3 |
| 3 | 5 | 6 | 10 | 2 | 12 | 12 | left shorter → left→4 |
| 4 | 5 | 8 | 10 | 1 | 8 | 12 | left shorter → left→5 (meet) |
Contrast with Dry run 1: there the pointers moved from both sides; here, because every left line is the shortest so far, only left ever moves. Either way it is a single O(n) sweep.
Both runs step exactly as the tables above. Python-specific: the cap uses min(a, b); the area is min(...) * (right - left).
Same steps. C++-specific: min(a, b) from <algorithm>; use a long for width × height on large inputs to avoid overflow.
Same steps. Java-specific: Math.min(a, b); likewise store the area in a long if values can be large.
In addition to the examples discussed throughout this explanation, below are some other examples to consider when testing your code.
| Input | Expected output | Description |
|---|---|---|
heights = [] | 0 | Tests an empty array. |
heights = [1] | 0 | Tests an array with just one element. |
heights = [0, 1, 0] | 0 | Tests an array with no containers that can contain water. |
heights = [3, 3, 3, 3] | 9 | Tests an array where all heights are the same. |
heights = [1, 2, 3] | 2 | Tests an array with strictly increasing heights. |
heights = [3, 2, 1] | 2 | Tests an array with strictly decreasing heights. |