6. Pacific Atlantic Water Flow
«Вода течет вниз, но инженерная мысль способна повернуть реки вспять, начав с океана».
— Дональд Эрвин Кнут
Смена перспективы: Реверсивный обход графа
В задаче 5. Number of Islands мы искали замкнутые области, запуская алгоритм заливки из конкретной точки. Но в системной инженерии и маршрутизации часто встречается паттерн Множественных источников (Multi-source).
Задача Pacific Atlantic Water Flow (LeetCode №417) — классический пример такого паттерна.
Условие: Дана матрица высот m x n. Остров омывается Тихим океаном (сверху и слева) и Атлантическим океаном (снизу и справа). Вода может течь вниз или на том же уровне (то есть в соседние ячейки с высотой $\le$).
Нужно найти координаты всех ячеек, дождевая вода из которых может достичь обоих океанов.
Наивный подход: Прямая симуляция
Первое, что приходит в голову — симуляция законов физики.
Пройдемся двойным циклом по всем ячейкам (r, c). Для каждой ячейки запустим DFS. Если DFS найдет путь до левой/верхней границы и до правой/нижней — добавим клетку в ответ.
Почему это провал на интервью?
Представьте, что у вас остров, который идет под уклон в виде ступенек. Для ячейки на вершине DFS обойдет весь остров. Для ячейки чуть ниже — почти весь остров.
Мы будем многократно вычислять одни и те же пути. Временная сложность такого алгоритма составит $O((M \times N)^2)$. Для матрицы $1000 \times 1000$ (миллион ячеек) это $10^{12}$ операций. Вы гарантированно получите Time Limit Exceeded (TLE).
Архитектурное решение: Инверсия потока (Reverse Flow)
Чтобы снизить сложность до линейной $O(M \times N)$, нам нужно сменить точку обзора.
Вместо того чтобы пускать воду с каждой горы вниз, давайте затопим остров от океанов вверх.
Мы знаем, что вода течет вниз (высота $X \ge Y$). Значит, если мы идем от океана, мы можем двигаться только вверх или по плоскости (высота $X \le Y$).
Алгоритм:
- Запускаем обход (DFS или BFS) от всех ячеек, граничащих с Тихим океаном. Отмечаем все достижимые клетки как
pacificVisited. - Запускаем обход от всех ячеек, граничащих с Атлантическим океаном. Отмечаем все достижимые клетки как
atlanticVisited. - Проходим по матрице один раз. Если ячейка есть и в
pacificVisited, и вatlanticVisited— это наш ответ.
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
A["Тихий океан<br>Верх-Лево"]:::entry -->|"Заливка вверх по рельефу"| B["Посещенные Тихим<br>pacificVisited"]:::process
C["Атлантический океан<br>Низ-Право"]:::entry -->|"Заливка вверх по рельефу"| D["Посещенные Атлантикой<br>atlanticVisited"]:::process
B --> E["Пересечение множеств<br>Искомые вершины"]:::success
D --> E
Идиоматичная реализация (Middle Level)
В этой реализации мы используем два среза visited. Это надежный, легко читаемый код, демонстрирующий базовое владение алгоритмом.
package main
// pacificAtlanticMiddle решает задачу через два двумерных массива посещений.
func pacificAtlanticMiddle(heights [][]int) [][]int {
if len(heights) == 0 {
return nil
}
rows, cols := len(heights), len(heights[0])
pacific := make([][]bool, rows)
atlantic := make([][]bool, rows)
for i := 0; i < rows; i++ {
pacific[i] = make([]bool, cols)
atlantic[i] = make([]bool, cols)
}
var dfs func(r, c int, visited [][]bool, prevHeight int)
dfs = func(r, c int, visited [][]bool, prevHeight int) {
// Проверка выхода за границы
if r < 0 || c < 0 || r >= rows || c >= cols {
return
}
// Вода не может течь "вверх" от океана, если высота меньше предыдущей
if heights[r][c] < prevHeight {
return
}
// Защита от зацикливания
if visited[r][c] {
return
}
visited[r][c] = true
// Идем в 4 стороны
currH := heights[r][c]
dfs(r+1, c, visited, currH)
dfs(r-1, c, visited, currH)
dfs(r, c+1, visited, currH)
dfs(r, c-1, visited, currH)
}
// Запускаем DFS от береговых линий
for r := 0; r < rows; r++ {
dfs(r, 0, pacific, heights[r][0]) // Левый берег
dfs(r, cols-1, atlantic, heights[r][cols-1]) // Правый берег
}
for c := 0; c < cols; c++ {
dfs(0, c, pacific, heights[0][c]) // Верхний берег
dfs(rows-1, c, atlantic, heights[rows-1][c]) // Нижний берег
}
// Ищем пересечение
var res [][]int
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if pacific[r][c] && atlantic[r][c] {
res = append(res, []int{r, c})
}
}
}
return res
}
Mechanical Sympathy: Оптимизация для Senior/Lead (Bitmasks & Flat Arrays)
Как Principal Engineer, вы должны видеть архитектурные резервы:
- Перерасход памяти: Два среза
[][]bool. В 64-битной архитектуре слайс[][]bool— это массив заголовковSliceHeader(по 24 байта каждый), указывающих на разрозненные куски памяти. Для матрицы $1000 \times 1000$ это создаст $2000$ лишних аллокаций в куче и фрагментирует L1-кэш. - Лишние проверки: Мы дважды обходим граф и дважды выделяем состояние.
Решение:
Мы можем упаковать всё состояние матрицы в один одномерный срез (Flat Array) типа []byte, а принадлежность к океанам закодировать битовыми масками (Bitmasks):
0b00(0) — не посещено0b01(1) — достижимо из Тихого океана (Pacific)0b10(2) — достижимо из Атлантического океана (Atlantic)0b11(3) — достижимо из обоих (искомый результат)
Production-Ready реализация (Bitmasks)
package main
// pacificAtlantic находит ячейки с двойным стоком через битовые маски и одномерный массив.
// Временная сложность: O(M * N) — каждая вершина посещается не более 2 раз.
// Пространственная сложность: O(M * N) для плоского среза states и стека рекурсии.
func pacificAtlantic(heights [][]int) [][]int {
if len(heights) == 0 {
return nil
}
rows, cols := len(heights), len(heights[0])
// Единый плоский массив состояний: 1 - Pacific, 2 - Atlantic, 3 - Both
states := make([]byte, rows*cols)
var dfs func(r, c int, stateMask byte, prevHeight int)
dfs = func(r, c int, stateMask byte, prevHeight int) {
if r < 0 || c < 0 || r >= rows || c >= cols {
return
}
idx := r*cols + c // Вычисление 1D-индекса
// Условие реверсивного подъема
if heights[r][c] < prevHeight {
return
}
// Побитовое И: если данный океан уже посещал ячейку, прерываем обход
if states[idx]&stateMask == stateMask {
return
}
// Побитовое ИЛИ: маркируем достижимость текущим океаном
states[idx] |= stateMask
currH := heights[r][c]
dfs(r+1, c, stateMask, currH)
dfs(r-1, c, stateMask, currH)
dfs(r, c+1, stateMask, currH)
dfs(r, c-1, stateMask, currH)
}
// Pacific (Маска = 1)
for c := 0; c < cols; c++ {
dfs(0, c, 1, heights[0][c])
}
for r := 0; r < rows; r++ {
dfs(r, 0, 1, heights[r][0])
}
// Atlantic (Маска = 2)
for c := 0; c < cols; c++ {
dfs(rows-1, c, 2, heights[rows-1][c])
}
for r := 0; r < rows; r++ {
dfs(r, cols-1, 2, heights[r][cols-1])
}
// Собираем результат: ищем ячейки с обоими битами (состояние 3)
var res [][]int
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if states[r*cols+c] == 3 {
res = append(res, []int{r, c})
}
}
}
return res
}
Оценка сложности
- Время: $O(M \times N)$. В худшем случае (совершенно плоский остров) каждый узел будет посещен максимум $2$ раза (один раз Тихим океаном, один раз Атлантическим). Благодаря отсечению
states[idx]&stateMask == stateMaskповторные проходы пресекаются за $O(1)$. - Память: $O(M \times N)$. В оптимизированной версии используется один срез
[]byteразмером $M \times N$ байт, что в $16$ раз компактнее[][]bool(без учета заголовков слайсов).
Резюме
- Многоточечный старт (Multi-source): Если у задачи несколько источников (береговая линия, границы матрицы, очаги возгорания), выгодно запускать обход последовательно от границ, накапливая общее состояние.
- Инверсия логики: Если прямой обход дает $O(N^2)$, подумайте, можно ли начать с конца. Этот паттерн часто встречается в задачах на маршруты.
- Плоская матрица (Flat Grid): Отраслевой стандарт работы с сетками в Go, радикально снижающий нагрузку на GC и повышающий Spatial Locality кэша.
Мы разобрали поиск путей и компонент. Но графы таят в себе еще одну угрозу. Иногда добавление всего одного ребра превращает строгую иерархию в хаотичный цикл. Как быстро обнаруживать такие «лишние» связи в динамически меняющихся сетях? Эту проблему мы решим в следующей статье: 7. Redundant Connection.