417. Pacific Atlantic Water Flow
On LeetCode ->Problem¶
Given a height grid, water flows to an adjacent cell only if its height is no greater. Return every cell from which water can reach both the Pacific on the top or left edge and the Atlantic on the bottom or right edge.
heights = [[1, 2],
[4, 3]]
Pacific: top/left
Atlantic: bottom/right
Output: [[0, 1], [1, 0], [1, 1]]
Key trick¶
Search backward from each ocean: from a cell, visit neighbors of equal or greater height. Cells reached by both searches are the answer.
Trap¶
Starting a traversal from every cell repeats work and can cost \(O(m^2n^2)\). Also, the reversed search must use >=, and visited cells are necessary because equal-height cells can form cycles.
Why is it interesting?¶
Reversing the graph turns many destination queries into two linear-time reachability searches.
Python solution¶
class Solution:
def pacificAtlantic(self, heights: list[list[int]]) -> list[list[int]]:
m, n = len(heights), len(heights[0])
directions = ((-1, 0), (1, 0), (0, -1), (0, 1))
def flood(starts):
# Reverse water flow: move from an ocean toward cells that are
# at least as high as the current cell.
seen = set(starts)
stack = list(seen)
while stack:
r, c = stack.pop()
for dr, dc in directions:
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]
):
seen.add((nr, nc))
stack.append((nr, nc))
return seen
pacific_starts = (
[(0, c) for c in range(n)]
+ [(r, 0) for r in range(m)]
)
atlantic_starts = (
[(m - 1, c) for c in range(n)]
+ [(r, n - 1) for r in range(m)]
)
pacific = flood(pacific_starts)
atlantic = flood(atlantic_starts)
# Row-major traversal gives deterministic output.
return [
[r, c]
for r in range(m)
for c in range(n)
if (r, c) in pacific and (r, c) in atlantic
]
Time complexity: \(O(mn)\).
Space complexity: \(O(mn)\).
Comment on my solution¶
Your reachability logic is correct, including the positive-result memoization through reach_oceans.
The main issues are:
- Running DFS independently from every cell costs up to \(O(m^2n^2)\).
- Invalid cells are not memoized, so their searches may be repeated almost completely.
- Recursive DFS can exceed Python's recursion limit on a grid containing a long path.
- Finding both oceans only returns from the current recursive call, so earlier stack frames may continue unnecessary exploration.
- Formatting spaces after commas and around tuple elements would improve readability.
Reversing the search removes the need to solve the same reachability problem once per cell.
# Ideas
# - this is a graph reachability problem with 3 constraints:
# - movement (in fact it's a directed graph)
# - be careful it can have cycle (2 adjacent cells with the same value)
# - component touch 2 boundaries
# - left or top and
# - right and bottom
# - maybe start a bfs on each cell
# - keep that cell if it can reach pacific and atlantic
# - small optimization:
# - during bfs if we pass through a cell that can reach boundaries,
# that means that the starting cell can reach it too
# - be careful, for a valid cell (starting the bfs), not all cells
# visited during bfs can reach pacific and atlantic
# WORKS but slow
class Solution:
def pacificAtlantic(self, heights: list[list[int]]) -> list[list[int]]:
m, n = len(heights), len(heights[0])
reach_oceans = set()
reach_pacific = False
reach_atlantic = False
visited = set()
def dfs(start,r,c):
nonlocal reach_pacific, reach_atlantic
if (r,c) in reach_oceans:
reach_oceans.add(start)
return
if r == 0 or c == 0:
reach_pacific = True
if r == m - 1 or c == n - 1:
reach_atlantic = True
if reach_pacific and reach_atlantic:
reach_oceans.add(start)
return
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 visited and heights[nr][nc] <= heights[r][c]:
visited.add((nr,nc))
dfs(start, nr, nc)
for r in range(m):
for c in range(n):
reach_pacific = False
reach_atlantic = False
visited = {(r,c)}
dfs((r,c), r, c)
return list(map(list,reach_oceans))