DSA sheet · Graphs · Grid traversal · start from the border (reverse flow)
Pacific Atlantic Water Flow
Rain falls on an island of heights; water runs downhill to two oceans. Which cells can send water to both oceans? Trying every cell one by one is slow. The teacher's key move is the same one we used in Surrounded Regions: start from the border (the ocean side) and walk inwards, but with the flow rule reversed, so water "climbs" from the sea. One climb from the Pacific border fills a Pacific grid, one climb from the Atlantic border fills an Atlantic grid, and the answer is every cell marked in both. She calls this question very important for interviews.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · Graph and grid basics this page needs
- Part A · Brute force: follow the water from every cell
- Part B · Reverse DFS from both borders (the teacher's code)
- Part C · The same thing with BFS
- Part D · Revision page
Part 0 · Graph and grid basics this page needs
Nodes, edges, directed, undirected, weighted
A graph is a set of nodes (points) joined by edges (lines). An undirected edge works both ways, (0)—(1). A directed edge is one-way, (0)→(1). A weighted edge carries a number, (0)—5—(1).
This problem is interesting because the grid looks undirected, but the water rule makes it directed: water can go from height 5 to height 3, but not from 3 back up to 5. Between equal heights it can go both ways.
(5)→(3) downhill: water flows 5 → 3, never 3 → 5 (4)⇄(4) equal heights: both ways
Three ways a graph can be given, and building an adjacency list
| form | looks like | neighbours of u |
|---|---|---|
| edge list | [[0,1],[1,2]] | scan all edges, so convert first |
| adjacency matrix | n × n of 0/1 | every v with M[u][v] == 1 |
| adjacency list | [[1],[0,2],[1]] | adj[u] |
| grid (this problem) | a 2-D list of heights | the 4 cells around (r, c), filtered by the rule |
n = 3
edges = [[0, 1], [1, 2]] # 0 -> 1, 1 -> 2
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v) # directed: one way only
print(adj) # [[1], [2], []]Visited, connected components, BFS, DFS
The visited mark stops a walk from going round in circles (with equal heights, water could bounce 4 → 4 → 4… forever). A connected component is a group of nodes that can reach each other. BFS spreads level by level with a queue (collections.deque). DFS goes as deep as it can with recursion, then backs up. Both reach exactly the same set of cells from a start, so for "mark everything reachable" either one works.
A grid is a graph
Each cell (r, c) is a node; its neighbours are up, down, left, right, which the problem calls north, south, west, east. The 4 moves live in a list of (dr, dc) pairs. Always do the bounds check before reading a neighbour.
north (-1, 0)
west (0,-1) ← (r, c) → east (0, +1)
south (+1, 0)
heights = [[1, 2],
[3, 4]]
rows, cols = len(heights), len(heights[0])
r, c = 0, 1
found = []
for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
found.append((nr, nc))
print(found) # [(1, 1), (0, 0)]Multi-start traversal
Here we don't start from one cell; we start from every border cell of an ocean, all sharing one visited grid. A cell already marked by an earlier start is never explored again. That sharing is what keeps the total work small (see Part B ⑨).
Part A · Brute force: follow the water from every cell
LeetCode 417 · Pacific Atlantic Water Flow
1The question in simple words
An m × n island is split into square cells; heights[r][c] is the height of each cell. The Pacific touches the top and left edges. The Atlantic touches the bottom and right edges. Rain on a cell can flow to a north/south/east/west neighbour whose height is less than or equal to the current height, and from any cell next to an ocean straight into that ocean. Return every [r, c] whose water can reach both oceans.
Pacific ~ ~ ~ ~ ~
~ 1 2 2 3 (5) *
~ 3 2 3 (4) (4) *
Pacific ~ 2 4 (5) 3 1 * Atlantic
~ (6) (7) 1 4 5 *
~ (5) 1 1 2 4 *
* * * * *
Atlantic
( ) = the 7 answer cells
Answer: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]].
The teacher's two example cells
- The 4 at (2,1). Its left neighbour is 2 and the cell above is 2: lower, so water runs left and up, straight to the Pacific. But below is 7 and right is 5: higher, water can't go there. Can it get to the Atlantic some other way? No. So (2,1) is not in the answer.
- The 5 at (2,2). Up is 3 (lower), then that 3 → 3 at (1,2)… keeps going to the top/left: Pacific ✓. Right is 3, then 1 at (2,4), which touches the right edge: Atlantic ✓. (Down is 1 too.) Both oceans, so (2,2) is in the answer.
to Pacific: 5(2,2) → 3(1,2) → 2(1,1) → 2(0,1) → top edge to Atlantic: 5(2,2) → 3(2,3) → 1(2,4) → right edge
2What the constraints tell us
1 ≤ m, n ≤ 200→ at most 40,000 cells. The grid is never empty.- Heights are 0 to 10⁵; we only compare them, so size doesn't matter.
- If we do a full search from every cell, that's up to 40,000 × 40,000 = 1.6 × 10⁹ steps. Way over the rough 10⁸ limit. So the brute force will be too slow on big grids. We still write it, because it's the natural first idea and a great checker.
3Intuition
Drop a bucket of water on a cell and watch where it can run. If some of it reaches the top or left edge, and some of it reaches the bottom or right edge, the cell counts.
4Building the logic
"Where can the water run?" is a traversal: from the cell, step to neighbours with height ≤ current, and keep going. Use a fresh visited set per start, because equal heights let water go back and forth. While walking, note whether we ever stand on a cell in row 0 or column 0 (Pacific side) and in row m−1 or column n−1 (Atlantic side).
→ The problem says a cell next to an ocean flows straight into it. A top-row cell has the Pacific right above it, so water there is already in the Pacific.
5Approach steps
- For every cell, run a BFS that moves to neighbours with height ≤ current.
- Track two flags: touched a Pacific edge? touched an Atlantic edge?
- If both, add the cell to the answer.
6Code (Python)
from collections import deque
class Solution:
def pacificAtlantic(self, heights):
m, n = len(heights), len(heights[0])
res = []
for i in range(m):
for j in range(n):
pac = atl = False
seen = {(i, j)}
q = deque([(i, j)])
while q:
r, c = q.popleft()
if r == 0 or c == 0:
pac = True
if r == m - 1 or c == n - 1:
atl = True
for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)):
nr, nc = r + dr, c + dc
if (0 <= nr < m and 0 <= nc < n and (nr, nc) not in seen
and heights[nr][nc] <= heights[r][c]): # downhill or flat
seen.add((nr, nc))
q.append((nr, nc))
if pac and atl:
res.append([i, j])
return res7Code line by line
| line | what it means |
|---|---|
| seen = {(i, j)} | A fresh visited set for this one drop of water. |
| if r == 0 or c == 0: pac = True | Top row or left column: next to the Pacific. |
| if r == m - 1 or c == n - 1: atl = True | Bottom row or right column: next to the Atlantic. A corner like (0, n−1) sets both. |
| heights[nr][nc] <= heights[r][c] | The flow rule from the problem: water only goes to an equal or lower cell. |
| if pac and atl: res.append([i, j]) | Both oceans reached. |
8Dry run
For the 4 at (2,1): the queue visits (2,1) → (1,1)=2, (2,0)=2 → from (1,1): (0,1)=2 → from (0,1): (0,0)=1. Rows/columns touched: row 0 and column 0 → pac = True. Never row 4 or column 4 → atl = False. Not added ✓, as the teacher said.
9Complexity & remember
- Time O((m·n)²): one search of up to m·n cells, for each of m·n cells. Too slow at 200 × 200.
- Space O(m·n) for one search's set and queue.
Part B · Reverse DFS from both borders
1The question
Same question. Goal: O(m·n) instead of O((m·n)²).
2What the constraints tell us
40,000 cells means an O(m·n) idea is instant, and storing a couple of m × n grids (80,000 booleans) is no problem at all.
3Intuition: let the ocean climb uphill
The teacher's observation: water can only enter an ocean through a border cell. Every cell on the top row and left column pours into the Pacific; every cell on the bottom row and right column pours into the Atlantic. So the border cells are where all the successful paths end.
Instead of starting at every cell and hoping to reach the border, start at the border and walk the paths backwards. Walking a downhill path backwards means walking uphill: from a cell, move to a neighbour whose height is greater than or equal to the current one. Every cell this reverse walk reaches can send water down to that border, and so into the ocean.
real flow: 7 → 4 → 2 → (Pacific) each step goes ≤ reverse walk: (Pacific) → 2 → 4 → 7 each step goes ≥
4Building the logic from the teacher's walk-through
Why the condition flips to ≥
Say we're standing on a border 2 and look at a neighbour 3. Could water from that 3 come to us? Yes, 3 → 2 is downhill. So the 3 can reach this ocean: mark it. Now say the neighbour is a 1. Could water from 1 climb to our 2? No. Don't mark it from here. So in the reverse walk: go to a neighbour only if its height ≥ the current height. Equal counts too, because flow between equal heights is allowed.
→ It's the same rule read from the other end. The question says "from a cell, water goes to a neighbour that is ≤". We are walking from the ocean back to the cell, so each step is the question's step done in reverse: "to a neighbour that is ≥". That's why the teacher stresses "the opposite definition".
Why one visited grid is not enough: one per ocean
A cell isn't just "visited or not". We need to know which ocean's walk reached it. A cell reached by both walks is an answer. So each cell needs two pieces of information. The teacher's two options: store a pair (Pacific, Atlantic) in every cell, or make two boolean grids the same size as heights: pac and atl. Both cost 2 × m × n. She goes with two grids.
Which border fills which grid
| border | ocean | start a reverse walk that fills… |
|---|---|---|
| first row (row 0) | Pacific | pac |
| first column (column 0) | Pacific | pac |
| last row (row m−1) | Atlantic | atl |
| last column (column n−1) | Atlantic | atl |
Skip a border cell that is already marked
In her walk-through, when she moves on to the next border cell and finds it already true, she does not start again from it: an earlier walk already got there and therefore already explored everything reachable from it. Starting again would redo that work.
pac. Should the Atlantic walk skip it too?→ No! The two grids are independent. The Atlantic walk only knows about
atl. A cell can be visited once by each walk, which is exactly why the total is 2 × m × n.The final pass
After all four border loops, walk every cell: if pac[i][j] and atl[i][j] are both True, add [i, j] to the answer.
5Approach steps
- Make
pacandatl, two m × n grids of False. - For each row i: DFS from (i, 0) into
pac, and from (i, n−1) intoatl. - For each column j: DFS from (0, j) into
pac, and from (m−1, j) intoatl. - DFS(r, c, grid): mark the cell; for each in-bounds, unmarked neighbour with height ≥ current, recurse.
- Collect every cell that is True in both grids.
6Code (Python)
import sys
class Solution:
def pacificAtlantic(self, heights):
sys.setrecursionlimit(100000)
m, n = len(heights), len(heights[0])
pac = [[False] * n for _ in range(m)]
atl = [[False] * n for _ in range(m)]
def dfs(r, c, seen):
seen[r][c] = True
for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)):
nr, nc = r + dr, c + dc
if nr < 0 or nr >= m or nc < 0 or nc >= n:
continue # off the island
if seen[nr][nc]:
continue # already reached by this ocean
if heights[nr][nc] < heights[r][c]:
continue # lower: water can't climb to us
dfs(nr, nc, seen)
for i in range(m):
if not pac[i][0]:
dfs(i, 0, pac) # first column -> Pacific
if not atl[i][n - 1]:
dfs(i, n - 1, atl) # last column -> Atlantic
for j in range(n):
if not pac[0][j]:
dfs(0, j, pac) # first row -> Pacific
if not atl[m - 1][j]:
dfs(m - 1, j, atl) # last row -> Atlantic
res = []
for i in range(m):
for j in range(n):
if pac[i][j] and atl[i][j]:
res.append([i, j])
return res7Code line by line
| line | what it means |
|---|---|
| pac = [[False] * n for _ in range(m)] | "Can this cell's water reach the Pacific?" One flag per cell. |
| atl = ... | The same question for the Atlantic, kept separately. |
| def dfs(r, c, seen): | One DFS for both oceans: we pass in which grid to fill. |
| seen[r][c] = True | We got here walking up from the ocean, so this cell drains into it. |
| if nr < 0 or ... : continue | Bounds check: skip directions that leave the island. |
| if seen[nr][nc]: continue | Visited check for this ocean. Stops loops between equal heights. |
| if heights[nr][nc] < heights[r][c]: continue | The reversed rule: a lower neighbour can't send water up to us. |
| dfs(nr, nc, seen) | Higher or equal: it drains into us, so it reaches the ocean too. Go deeper from it. |
| if not pac[i][0]: dfs(i, 0, pac) | Start from a border cell only if no earlier walk of this ocean reached it. |
| if pac[i][j] and atl[i][j] | Reached from both sides → water from here can reach both oceans. |
sys.setrecursionlimit?→ A 200 × 200 grid that climbs in a long snake can make the DFS 40,000 calls deep. Python's default limit is about 1000. The BFS version in Part C avoids recursion completely.
8Dry run
The LeetCode grid. Loop order: for each row i, first the Pacific start (i, 0), then the Atlantic start (i, 4); then the same for each column. Neighbour order: up, down, left, right.
c0 c1 c2 c3 c4 r0 1 2 2 3 5 r1 3 2 3 4 4 r2 2 4 5 3 1 r3 6 7 1 4 5 r4 5 1 1 2 4
The Pacific walk (fills pac)
| step | cell visited | neighbours checked (need ≥ current) | call stack after | newly marked in pac |
|---|---|---|---|---|
| 1 | start (0,0)=1 | up: off · down (1,0)=3 ≥1 ✓ | (0,0) (1,0) | (0,0) (1,0) |
| 2 | (1,0)=3 | up seen · down (2,0)=2 <3 ✗ · left off · right (1,1)=2 <3 ✗ → returns | (0,0) | - |
| 3 | back at (0,0) | left off · right (0,1)=2 ≥1 ✓ | (0,0) (0,1) | (0,1) |
| 4 | (0,1)=2 | down (1,1)=2 equal ✓ | … (0,1) (1,1) | (1,1) |
| 5 | (1,1)=2 | up seen · down (2,1)=4 ✓ | … (1,1) (2,1) | (2,1) |
| 6 | (2,1)=4 | down (3,1)=7 ✓ (its neighbours are (2,1) seen, then 1, 6, 1, all lower → returns) · left (2,0)=2 ✗ · right (2,2)=5 ✓ (all its neighbours lower → returns) | … (1,1) | (3,1) (2,2) |
| 7 | back at (1,1) | left (1,0) seen · right (1,2)=3 ✓ | … (1,1) (1,2) | (1,2) |
| 8 | (1,2)=3 → (1,3)=4 → (1,4)=4 (equal ✓) → (0,4)=5 ✓ | each step goes up or flat; (0,4) has nothing higher left | (0,0) (0,1) (1,1) (1,2) (1,3) (1,4) (0,4) | (1,3) (1,4) (0,4) |
| 9 | unwind back to (0,1) | right (0,2)=2 ✓ → (0,3)=3 ✓ → its right (0,4) seen | … (0,1) (0,2) (0,3) | (0,2) (0,3) |
| 10 | stack empties. Next starts: (1,0) already true → skip | - | (empty) | - |
| 11 | start (2,0)=2 | down (3,0)=6 ✓ (its neighbours (4,0)=5 and (3,1) are lower or seen) | (2,0) (3,0) | (2,0) (3,0) |
| 12 | (3,0) skip · start (4,0)=5 | up (3,0) seen · right (4,1)=1 ✗ | (4,0) | (4,0) |
| 13 | row loop: (0,0)…(0,4) all already true → skip | - | (empty) | - |
The Atlantic walk (fills atl)
| start | cells it climbs to | notes |
|---|---|---|
| (0,4)=5 | only itself | left (0,3)=3 and down (1,4)=4 are lower |
| (1,4)=4 | (1,3)=4 | equal height counts |
| (2,4)=1 | (3,4)=5, (2,3)=3 → (3,3)=4, (2,2)=5 | a low border cell lets the walk climb far inside |
| (3,4), (4,4) | (3,4) skipped (already true); (4,4)=4 alone | (4,3)=2 is lower |
| (4,0)=5 | (3,0)=6 → (3,1)=7 | bottom-row loop starts here |
| (4,1)=1 | (4,2)=1 → (3,2)=1, (4,3)=2 | rest of the bottom row is already true |
P P P P P P P P P P P P P . . P P . . . P . . . .
. . . . A . . . A A . . A A A A A A A A A A A A A
. . . . ✓ . . . ✓ ✓ . . ✓ . . ✓ ✓ . . . ✓ . . . .
Result: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]] ✓. Note (2,1), the teacher's 4, is P but not A, as she predicted.
9Complexity & remember
The worry the teacher clears up: a single DFS from the first border cell might cover the whole grid (m·n). There are about m + n border starts. Is it m·n per start, around n³ in total? No. Once a cell is marked in pac, no later Pacific walk enters it again, and we don't even start from marked border cells. If the first start marks a big area, the next start can only mark what's left, and so on. All the Pacific starts together mark each cell at most once: m·n.
- Time O(m·n): m·n for the Pacific walks + m·n for the Atlantic walks (they don't share marks) + m·n for the final pass = 3·m·n.
- Space O(m·n): two grids = 2·m·n, plus the recursion stack. Constants drop.
pac; Atlantic = bottom row + right column into atl. Answer = cells in both.Part C · The same thing with BFS
The teacher says the only job left after the reversal is "start from these cells and mark everything reachable", so DFS or BFS, whichever feels easier. Steps ①–④ are the same as Part B. What changes:
- Instead of one DFS per border cell, we can put all the Pacific border cells into one queue at the start (a multi-source BFS), mark them, and let the queue spread. Same for the Atlantic.
- The rule for stepping is identical: in bounds, not yet marked, height ≥ current.
- No recursion, so no recursion limit to worry about.
5Approach steps
- Pacific starts: every cell of row 0 and column 0. Atlantic starts: every cell of row m−1 and column n−1.
- For each ocean: mark all its starts, put them in a deque, BFS uphill.
- Return the cells marked by both.
6Code (Python)
from collections import deque
class Solution:
def pacificAtlantic(self, heights):
m, n = len(heights), len(heights[0])
def bfs(starts):
seen = [[False] * n for _ in range(m)]
q = deque()
for r, c in starts:
if not seen[r][c]:
seen[r][c] = True
q.append((r, c))
while q:
r, c = q.popleft()
for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)):
nr, nc = r + dr, c + dc
if (0 <= nr < m and 0 <= nc < n and not seen[nr][nc]
and heights[nr][nc] >= heights[r][c]):
seen[nr][nc] = True
q.append((nr, nc))
return seen
pac = bfs([(0, j) for j in range(n)] + [(i, 0) for i in range(m)])
atl = bfs([(m - 1, j) for j in range(n)] + [(i, n - 1) for i in range(m)])
return [[i, j] for i in range(m) for j in range(n) if pac[i][j] and atl[i][j]]7Code line by line
| line | what it means |
|---|---|
| for r, c in starts: ... q.append((r, c)) | All border cells of one ocean go in at once. The corner (0,0) appears twice in the list; the if not seen stops a double push. |
| heights[nr][nc] >= heights[r][c] | The same reversed rule as Part B, written as the condition to go rather than to skip. |
| return seen | The finished grid for that ocean. |
| return [[i, j] for ... if pac[i][j] and atl[i][j]] | The final pass, in one line. |
8Dry run (Atlantic, first two rounds)
atl grid as Part B ✓9Complexity & remember
- Time O(m·n), space O(m·n): same as Part B.
Part D · Revision page
| A · brute force | B · reverse DFS | C · reverse BFS | |
|---|---|---|---|
| start from | every cell | the border cells of each ocean | |
| step rule | neighbour ≤ current (real flow) | neighbour ≥ current (flow reversed) | |
| visited | a fresh set per cell | one grid per ocean: pac, atl | |
| answer | reached a Pacific edge and an Atlantic edge | pac[i][j] and atl[i][j] | |
| time | O((m·n)²) | O(m·n) | |
| space | O(m·n) | O(m·n) | |
| Surrounded Regions (Problem 8) | Pacific Atlantic (this one) | |
|---|---|---|
| start from | border O's | border cells (all of them) |
| step onto | neighbour that is O | neighbour with height ≥ current |
| marks | one kind (#) | two kinds (Pacific, Atlantic) |
| the answer is | the cells NOT reached | the cells reached by BOTH |
2. So start at the borders and walk backwards: uphill, height ≥ current.
3. Top + left →
pac; bottom + right → atl.4. Never start from (or re-enter) a cell already marked for that ocean.
5. Answer = cells true in both grids. O(m·n) time and space.
✗ using > instead of ≥ (equal heights do flow)
✗ one shared visited grid for both oceans
✗ mixing up the sides: Pacific is top + left, Atlantic is bottom + right
✗ thinking the border loops make it O(n³): marks are shared across starts
h = [[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]] print(sorted(Solution().pacificAtlantic(h))) # [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] print(Solution().pacificAtlantic([[1]])) # [[0, 0]] print(sorted(Solution().pacificAtlantic([[2, 1], [1, 2]]))) # [[0, 0], [0, 1], [1, 0], [1, 1]] print(sorted(Solution().pacificAtlantic([[3, 3, 3], [3, 1, 3], [3, 3, 3]]))) # [[0, 0], [0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1], [2, 2]]
Based on this video: Pacific Atlantic Water Flow | Graph | Pattern-wise DSA sheet