7. Container With Most Water
«В инженерной оптимизации побеждает не тот, кто слепо перебирает все возможности, а тот, кто умеет одним строгим рассуждением отсечь миллионы заведомо бесперспективных вариантов».
— Брайан Керниган
LeetCode 11. Container With Most Water (Medium / Core)
Вам дан целочисленный массив неотрицательных чисел height длины $N \ge 2$. Каждое число представляет высоту вертикальной стенки, расположенной в точке с координатой $x = i$.
Требуется выбрать две линии, которые вместе с осью абсцисс образуют сосуд, вмещающий максимально возможное количество воды.
Формула площади воды между стенками $i$ и $j$ ($i < j$):
$$\text{Area}(i, j) = \min(height[i], height[j]) \times (j - i)$$
Задача проверяет способность инженера доказать математический инвариант: почему жадный сдвиг более короткой стенки никогда не приводит к потере глобального максимума, и как свести кажущийся квадратичный перебор $O(N^2)$ к элегантному линейному алгоритму за $O(N)$ и $O(1)$ памяти.
Калибровка условий и границы данных
Вход: height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
Вывод: 49
Объяснение: Максимальный объем достигается между стенкой высотой 8 (индекс 1)
и стенкой высотой 7 (индекс 8). Расстояние: 8 - 1 = 7. Высота воды: min(8, 7) = 7.
Площадь: 7 * 7 = 49.
Математический инвариант: Почему жадный сдвиг безопасен?
Бытовая аналогия: представьте, что вы строите прямоугольный бассейн. Его объем ограничен самой низкой из двух стенок (вода перельется через край).
Мы начинаем с максимально возможной ширины: ставим указатели по краям массива — left = 0 и right = len - 1.
Теперь мы обязаны сужать бассейн, сдвигая один из указателей внутрь. Какую стенку сдвинуть — высокую или низкую?
- Допустим,
height[left] < height[right]. - Если мы оставим стенку
leftна месте и сдвинем правую (высокую) стенку внутрь на любую позицию $k$, ширина нового бассейна гарантированно уменьшится: $(k - left) < (right - left)$. - А что с высотой? Высота нового бассейна будет $\min(height[left], height[k]) \le height[left]$. Она не может стать больше $height[left]$!
- Следовательно, площадь любого бассейна с фиксированной стенкой
leftи любой внутренней правой стенкой заведомо меньше, чем текущая площадь!
Стенка left выжала свой абсолютный теоретический максимум при максимальной ширине. Мы можем навсегда выбросить её из рассмотрения и сделать left++.
Аналогично, если короче стенка справа — делаем right--.
Визуальный алгоритмический процесс (Two Pointers)
flowchart TD
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
Start["Старт: left = 0, right = n - 1<br>maxArea = 0"]:::entry --> Check{"left < right?"}:::warning
Check -->|"Да"| Calc["Вычисляем текущую площадь:<br>h = min(height[left], height[right])<br>w = right - left<br>area = h * w<br>maxArea = max(maxArea, area)"]:::process
Calc --> Branch{"height[left] < height[right]?"}:::accent
Branch -->|"Да: левая стенка лимитирует"| MoveL["Сдвигаем левую стенку внутрь:<br>left++"]:::process
Branch -->|"Нет: правая стенка лимитирует"| MoveR["Сдвигаем правую стенку внутрь:<br>right--"]:::process
MoveL --> Check
MoveR --> Check
Check -->|"Нет (указатели встретились)"| Finish["Возвращаем maxArea"]:::success
Идиоматичная реализация на Go
package main
// Вспомогательный хелпер min (в Go 1.21+ встроен в builtin)
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func maxArea(height []int) int {
left := 0
right := len(height) - 1
maxVolume := 0
for left < right {
h := minInt(height[left], height[right])
w := right - left
currentArea := h * w
if currentArea > maxVolume {
maxVolume = currentArea
}
// Жадный сдвиг: жертвуем более слабой стенкой,
// сохраняя шанс найти более высокую стенку внутри
if height[left] < height[right] {
left++
} else {
right--
}
}
return maxVolume
}
Mechanical Sympathy: Почему два встречных указателя идеальны для CPU
- Двустороннее последовательное чтение: Указатели
leftиrightперемещаются строго монотонно. Кэш-линии процессора считываются последовательно с обоих концов среза, гарантируя высокий Cache Hit Rate. - Нулевой оверхед памяти: Алгоритм не создает объектов в куче (
zero allocations). Переменныеleft,right,h,w,currentAreaполностью аллоцируются в регистрах CPU. - Минимальная длина цикла: Весь алгоритм требует ровно $N-1$ итераций, выполняясь на процессоре с частотой 3 ГГц менее чем за 0.1 миллисекунды для $10^5$ элементов.
Резюме
Container With Most Water учит строгому доказательству инварианта через метод исключения заведомо неоптимальных состояний (Exchange Argument).
В следующей лекции мы разберем задачу о максимальной прибыли на бирже, где достаточно отслеживать минимальную историческую цену — классическую задачу Best Time to Buy and Sell Stock: 8. Best Time to Buy and Sell Stock.