6.18 Pacific Atlantic Water Flow
Source:
src/main/kotlin/grid/PacificAtlanticWaterFlow.ktPattern: reverse-flow multi-source BFS · Core page
The Problem
Cells from which water flows to both the Pacific (top/left edges) and Atlantic (bottom/right edges); water flows to equal-or-lower neighbors.
- Constraints: m, n ≤ 200.
Examples
Input: heights = [[1,2,2,3,5],
[3,2,3,4,4],
[2,4,5,3,1],
[6,7,1,4,5],
[5,1,1,2,4]]
Output: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]
Intuition — reverse the flow: BFS from each ocean, uphill
“Water flows downhill to an ocean” is hard per-cell. Flip it: start at each ocean’s shore and walk uphill (to higher-or-equal neighbors) — cells reachable from the Pacific shore can drain to the Pacific:
pacificReachable = bfs from all top/left edge cells
atlanticReachable = bfs from all bottom/right edge cells
answer = cells in both sets
Why reverse the direction? Testing every cell’s drainage is O(cells × paths); the shore-seeded BFS is one flood per ocean, O(m·n). The 6.16 border-seed trick with a slope rule.
Why “higher-or-equal” going up? next.height >= cur.height reversed = cur.height >= next.height downhill — exactly “water flows to equal-or-lower neighbors”. The comparison direction is the whole logic.
Approach 1 — DFS per cell with memo (check downhill)
For each cell, memoize “can reach Pacific/Atlantic”: correct, more bookkeeping.
Approach 2 — Reverse BFS from both shores (the repo’s version, optimal)
import java.util.*
class PacificAtlanticWaterFlow {
data class Cell(val r: Int, val c: Int)
private val directions = listOf(1 to 0, -1 to 0, 0 to 1, 0 to -1)
/**
* @param heights elevation grid
* @return cells draining to both oceans
*/
fun pacificAtlantic(heights: Array<IntArray>): List<List<Int>> {
val rows = heights.size
val cols = heights[0].size
fun bfs(starts: List<Cell>): Array<BooleanArray> {
val reachable = Array(rows) { BooleanArray(cols) }.apply {
starts.forEach { (r, c) -> this[r][c] = true }
}
val queue: Queue<Cell> = LinkedList(starts)
while (queue.isNotEmpty()) {
val (r, c) = queue.poll()
for ((dr, dc) in directions) {
val nr = r + dr
val nc = c + dc
if (nr in 0 until rows && nc in 0 until cols &&
!reachable[nr][nc] && heights[nr][nc] >= heights[r][c]) {
reachable[nr][nc] = true // uphill from here
queue.offer(Cell(nr, nc))
}
}
}
return reachable
}
val pacific = bfs(
(0 until rows).map { Cell(it, 0) } + (0 until cols).map { Cell(0, it) }
)
val atlantic = bfs(
(0 until rows).map { Cell(it, cols - 1) } + (0 until cols).map { Cell(rows - 1, it) }
)
return (0 until rows).flatMap { r ->
(0 until cols).filter { c -> pacific[r][c] && atlantic[r][c] }
.map { c -> listOf(r, c) }
}
}
}
import java.util.*;
public class PacificAtlanticWaterFlow {
private int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private boolean[][] bfs(int[][] h, Queue<int[]> starts) {
int m = h.length, n = h[0].length;
boolean[][] reach = new boolean[m][n];
for (int[] s : starts) reach[s[0]][s[1]] = true;
while (!starts.isEmpty()) {
int[] cur = starts.poll();
for (int[] d : dirs) {
int nr = cur[0] + d[0], nc = cur[1] + d[1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n &&
!reach[nr][nc] && h[nr][nc] >= h[cur[0]][cur[1]]) {
reach[nr][nc] = true;
starts.offer(new int[]{nr, nc});
}
}
}
return reach;
}
/**
* @param heights elevation grid
* @return cells draining to both oceans
*/
public List<List<Integer>> pacificAtlantic(int[][] heights) {
int m = heights.length, n = heights[0].length;
Queue<int[]> pacificStarts = new LinkedList<>();
Queue<int[]> atlanticStarts = new LinkedList<>();
for (int i = 0; i < m; i++) {
pacificStarts.offer(new int[]{i, 0});
atlanticStarts.offer(new int[]{i, n - 1});
}
for (int j = 0; j < n; j++) {
pacificStarts.offer(new int[]{0, j});
atlanticStarts.offer(new int[]{m - 1, j});
}
boolean[][] pacific = bfs(heights, pacificStarts);
boolean[][] atlantic = bfs(heights, atlanticStarts);
List<List<Integer>> result = new ArrayList<>();
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (pacific[i][j] && atlantic[i][j]) result.add(Arrays.asList(i, j));
return result;
}
}
#include <queue>
#include <vector>
class PacificAtlanticWaterFlow {
int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
std::vector<std::vector<bool>> bfs(std::vector<std::vector<int>>& h,
std::queue<std::pair<int, int>>& starts) {
int m = h.size(), n = h[0].size();
std::vector<std::vector<bool>> reach(m, std::vector<bool>(n, false));
while (!starts.empty()) {
auto [r, c] = starts.front(); starts.pop();
reach[r][c] = true;
for (auto& d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n &&
!reach[nr][nc] && h[nr][nc] >= h[r][c]) {
starts.push({nr, nc});
}
}
}
return reach;
}
public:
/**
* @param heights elevation grid
* @return cells draining to both oceans
*/
std::vector<std::vector<int>> pacificAtlantic(std::vector<std::vector<int>>& heights) {
int m = heights.size(), n = heights[0].size();
std::queue<std::pair<int, int>> ps, as;
for (int i = 0; i < m; i++) { ps.push({i, 0}); as.push({i, n - 1}); }
for (int j = 0; j < n; j++) { ps.push({0, j}); as.push({m - 1, j}); }
auto pacific = bfs(heights, ps);
auto atlantic = bfs(heights, as);
std::vector<std::vector<int>> result;
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (pacific[i][j] && atlantic[i][j]) result.push_back({i, j});
return result;
}
};
from collections import deque
def pacific_atlantic(heights: list[list[int]]) -> list[list[int]]:
"""
@param heights: elevation grid
@return: cells draining to both oceans
"""
rows, cols = len(heights), len(heights[0])
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
def bfs(starts):
reach = [[False] * cols for _ in range(rows)]
queue = deque(starts)
for r, c in starts:
reach[r][c] = True
while queue:
r, c = queue.popleft()
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if (0 <= nr < rows and 0 <= nc < cols and
not reach[nr][nc] and heights[nr][nc] >= heights[r][c]):
reach[nr][nc] = True
queue.append((nr, nc))
return reach
pacific = bfs([(r, 0) for r in range(rows)] + [(0, c) for c in range(cols)])
atlantic = bfs([(r, cols - 1) for r in range(rows)] + [(rows - 1, c) for c in range(cols)])
return [[r, c] for r in range(rows) for c in range(cols)
if pacific[r][c] and atlantic[r][c]]
#![allow(unused)]
fn main() {
use std::collections::VecDeque;
impl Solution {
/// @param heights elevation grid
/// @return cells draining to both oceans
pub fn pacific_atlantic(heights: Vec<Vec<i32>>) -> Vec<Vec<i32>> {
let (m, n) = (heights.len(), heights[0].len());
let dirs = [(1, 0), (-1, 0), (0, 1), (0, -1)];
fn bfs(starts: Vec<(usize, usize)>, h: &Vec<Vec<i32>>,
m: usize, n: usize) -> Vec<Vec<bool>> {
let mut reach = vec![vec![false; n]; m];
let mut queue = VecDeque::new();
for (r, c) in &starts { reach[*r][*c] = true; queue.push_back((*r, *c)); }
while let Some((r, c)) = queue.pop_front() {
for (dr, dc) in dirs {
let (nr, nc) = (r as i32 + dr, c as i32 + dc);
if nr >= 0 && nr < m as i32 && nc >= 0 && nc < n as i32 {
let (ur, uc) = (nr as usize, nc as usize);
if !reach[ur][uc] && h[ur][uc] >= h[r][c] {
reach[ur][uc] = true;
queue.push_back((ur, uc));
}
}
}
}
reach
}
let pacific = bfs((0..m).map(|r| (r, 0)).chain((0..n).map(|c| (0, c))).collect(),
&heights, m, n);
let atlantic = bfs((0..m).map(|r| (r, n - 1)).chain((0..n).map(|c| (m - 1, c))).collect(),
&heights, m, n);
let mut result = Vec::new();
for r in 0..m {
for c in 0..n {
if pacific[r][c] && atlantic[r][c] { result.push(vec![r as i32, c as i32]); }
}
}
result
}
}
}
Dry run
Input: heights = [[1,2],[2,1]].
Pacific starts: (0,0), (0,1), (1,0). Atlantic starts: (1,1), (0,1), (1,0).
Pacific BFS: (0,0)=1 -> neighbors: (1,0)=2 >= 1 ✓ reachable. (0,1)=2 >= 1 ✓.
from (1,0)=2 -> (1,1)=1 >= 2? no. from (0,1)=2 -> (1,1)=1? no.
pacific = {(0,0),(0,1),(1,0)}
Atlantic BFS: (1,1)=1 -> (0,1)=2 ✓, (1,0)=2 ✓.
from (0,1)=2 -> (0,0)=1? no. from (1,0)=2 -> (0,0)=1? no.
atlantic = {(1,1),(0,1),(1,0)}
intersection: (0,1) and (1,0) -> [[0,1],[1,0]] ✓
The reverse flow at work: cell (0,1) (height 2) drains to both oceans — its downhill paths are to the Pacific top edge and the Atlantic right edge. The BFS finds it from both shores because climbing from either shore reaches it (2 >= 2 uphill allowed). Cells (0,0) and (1,1) are valleys reachable from only one side — their >= checks fail the other ocean’s climb.
Complexity
Time. Two floods:
$$ T(m, n) = O(m \cdot n) $$
Space. Two boolean grids:
$$ S(m, n) = O(m \cdot n) $$
Variants & follow-ups
- Surrounded Regions (6.16) — the same border-seed reverse thinking with a different slope rule (none).
- Max Area Of Island (6.17) — the sink-based sibling.
- Interview follow-up: “Why is reverse flow the right frame?” The forward question (“can this cell reach an ocean?”) has m·n starting points. The reverse question (“which cells can an ocean’s shore reach uphill?”) has O(m+n) starts and one flood — the 6.16 “start from the answer’s boundary” pattern at its most valuable.