Files
codetutor/backend/data/questions/pacific-atlantic-water-flow.yaml

301 lines
14 KiB
YAML

title: Pacific Atlantic Water Flow
slug: pacific-atlantic-water-flow
difficulty: medium
leetcode_id: 417
leetcode_url: https://leetcode.com/problems/pacific-atlantic-water-flow/
categories:
- graphs
- arrays
patterns:
- dfs
- matrix-traversal
function_signature: "def pacific_atlantic(heights: list[list[int]]) -> list[list[int]]:"
test_cases:
visible:
- 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]] }
expected: [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]]
- input: { heights: [[1]] }
expected: [[0, 0]]
hidden:
- input: { heights: [[1, 1], [1, 1]] }
expected: [[0, 0], [0, 1], [1, 0], [1, 1]]
- input: { heights: [[1, 2], [4, 3]] }
expected: [[0, 1], [1, 0], [1, 1]]
- input: { heights: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] }
expected: [[0, 2], [1, 2], [2, 0], [2, 1], [2, 2]]
- input: { heights: [[10, 10, 10], [10, 1, 10], [10, 10, 10]] }
expected: [[0, 0], [0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1], [2, 2]]
- input: { heights: [[3, 3, 3, 3, 3]] }
expected: [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]]
- input: { heights: [[3], [3], [3], [3], [3]] }
expected: [[0, 0], [1, 0], [2, 0], [3, 0], [4, 0]]
description: |
There is an `m x n` rectangular island that borders both the **Pacific Ocean** and **Atlantic Ocean**. The **Pacific Ocean** touches the island's left and top edges, and the **Atlantic Ocean** touches the island's right and bottom edges.
The island is partitioned into a grid of square cells. You are given an `m x n` integer matrix `heights` where `heights[r][c]` represents the **height above sea level** of the cell at coordinate `(r, c)`.
The island receives a lot of rain, and the rain water can flow to neighbouring cells directly north, south, east, and west if the neighbouring cell's height is **less than or equal to** the current cell's height. Water can flow from any cell adjacent to an ocean into the ocean.
Return *a **2D list** of grid coordinates* `result` *where* `result[i] = [r_i, c_i]` *denotes that rain water can flow from cell* `(r_i, c_i)` *to **both** the Pacific and Atlantic oceans*.
constraints: |
- `m == heights.length`
- `n == heights[r].length`
- `1 <= m, n <= 200`
- `0 <= heights[r][c] <= 10^5`
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]]"
explanation: "These cells can reach both oceans. For example, [2,2] flows to Pacific via [2,2] -> [1,2] -> [0,2] and to Atlantic via [2,2] -> [2,3] -> [2,4]."
- input: "heights = [[1]]"
output: "[[0,0]]"
explanation: "The single cell borders both oceans, so water flows to both."
explanation:
intuition: |
Imagine standing on the island during heavy rain. Water flows downhill (or stays level), eventually reaching an ocean. The question asks: from which cells can water reach *both* oceans?
The naive approach would be to start from every cell and try to find paths to both oceans — but this is inefficient. Instead, think about the problem **in reverse**: rather than asking "where can water from this cell go?", ask "which cells can water reach this ocean from?"
**The key insight**: Start from the ocean boundaries and flow *uphill* (to cells with equal or greater height). Any cell reachable by "reverse flow" from the Pacific can eventually drain into the Pacific. Similarly for the Atlantic.
Think of it like this: imagine the ocean water rising and flooding cells it can reach (only moving to equal or higher ground). After flooding from both oceans, the cells that got flooded by *both* are our answer.
By running DFS from all Pacific-bordering cells and separately from all Atlantic-bordering cells, we get two sets of reachable cells. The intersection of these sets is our answer.
approach: |
We solve this using **Reverse DFS from Ocean Boundaries**:
**Step 1: Initialise tracking sets**
- `pacific_reachable`: A set of cells that can reach the Pacific Ocean
- `atlantic_reachable`: A set of cells that can reach the Atlantic Ocean
- The Pacific borders the top row and left column
- The Atlantic borders the bottom row and right column
&nbsp;
**Step 2: Define DFS helper function**
- The DFS explores cells by moving to neighbours with **equal or greater** height (reverse flow)
- Mark cells as visited by adding them to the respective ocean's reachable set
- Explore all four directions: up, down, left, right
- Skip cells that are out of bounds, already visited, or have lower height
&nbsp;
**Step 3: Run DFS from Pacific borders**
- Start DFS from every cell in the top row (row 0)
- Start DFS from every cell in the left column (column 0)
- All cells visited get added to `pacific_reachable`
&nbsp;
**Step 4: Run DFS from Atlantic borders**
- Start DFS from every cell in the bottom row (row m-1)
- Start DFS from every cell in the right column (column n-1)
- All cells visited get added to `atlantic_reachable`
&nbsp;
**Step 5: Find the intersection**
- Return all cells that appear in *both* `pacific_reachable` and `atlantic_reachable`
- These are the cells where water can flow to both oceans
common_pitfalls:
- title: Forward Flow is Inefficient
description: |
Starting DFS from every cell and checking if it can reach both oceans leads to redundant work. With a 200x200 grid (40,000 cells), each potentially doing O(m*n) work, this approaches O((m*n)^2) — far too slow.
The reverse flow approach does at most O(m*n) work for each ocean, giving O(m*n) total.
wrong_approach: "DFS from every cell checking reachability to both oceans"
correct_approach: "Reverse DFS from ocean boundaries, then intersect"
- title: Incorrect Flow Direction
description: |
When doing reverse DFS from the oceans, water flows *uphill* (to equal or greater height). A common mistake is continuing to use the normal downhill condition.
**Normal flow**: water goes to neighbour if `neighbour_height <= current_height`
**Reverse flow**: water came from neighbour if `neighbour_height >= current_height`
Mixing these up gives wrong results.
wrong_approach: "Check if neighbour height <= current height during reverse DFS"
correct_approach: "Check if neighbour height >= current height during reverse DFS"
- title: Forgetting Corner Cells
description: |
The corner cells belong to both ocean boundaries:
- Top-left `(0, 0)`: Pacific (top row AND left column)
- Top-right `(0, n-1)`: Pacific (top) and Atlantic (right)
- Bottom-left `(m-1, 0)`: Pacific (left) and Atlantic (bottom)
- Bottom-right `(m-1, n-1)`: Atlantic (bottom AND right)
Using sets for visited cells naturally handles the overlap — starting DFS from a corner twice doesn't cause issues.
key_takeaways:
- "**Reverse the problem**: Instead of asking 'where can this cell reach?', ask 'which cells can reach this destination?' This often simplifies graph reachability problems."
- "**Multi-source BFS/DFS**: When checking reachability to a boundary, start from all boundary cells simultaneously rather than from every interior cell."
- "**Set intersection**: When finding cells satisfying multiple conditions, compute each condition separately and intersect the results."
- "**Matrix traversal pattern**: This problem combines DFS with the 4-directional movement pattern common in grid problems."
time_complexity: "O(m * n). Each cell is visited at most twice (once for Pacific DFS, once for Atlantic DFS), and each visit does O(1) work plus recursive calls to unvisited neighbours."
space_complexity: "O(m * n). We store two sets that can each contain up to m*n cells, plus the recursion stack which can be O(m * n) deep in the worst case."
solutions:
- approach_name: Reverse DFS
is_optimal: true
code: |
def pacific_atlantic(heights: list[list[int]]) -> list[list[int]]:
if not heights or not heights[0]:
return []
m, n = len(heights), len(heights[0])
pacific_reachable = set()
atlantic_reachable = set()
def dfs(row: int, col: int, reachable: set, prev_height: int) -> None:
# Skip if out of bounds, already visited, or can't flow uphill
if (row < 0 or row >= m or col < 0 or col >= n or
(row, col) in reachable or heights[row][col] < prev_height):
return
# Mark this cell as reachable from this ocean
reachable.add((row, col))
# Explore all four directions (reverse flow: go to >= height)
current_height = heights[row][col]
dfs(row + 1, col, reachable, current_height) # down
dfs(row - 1, col, reachable, current_height) # up
dfs(row, col + 1, reachable, current_height) # right
dfs(row, col - 1, reachable, current_height) # left
# Start DFS from Pacific borders (top row and left column)
for col in range(n):
dfs(0, col, pacific_reachable, heights[0][col])
for row in range(m):
dfs(row, 0, pacific_reachable, heights[row][0])
# Start DFS from Atlantic borders (bottom row and right column)
for col in range(n):
dfs(m - 1, col, atlantic_reachable, heights[m - 1][col])
for row in range(m):
dfs(row, n - 1, atlantic_reachable, heights[row][n - 1])
# Return cells reachable from both oceans
return [[r, c] for r, c in pacific_reachable & atlantic_reachable]
explanation: |
**Time Complexity:** O(m * n) — Each cell visited at most twice.
**Space Complexity:** O(m * n) — For the two reachable sets and recursion stack.
We reverse the problem: instead of checking where water from each cell can go, we check which cells can reach each ocean by flowing "uphill" from the ocean boundaries. The intersection of both reachable sets gives our answer.
- approach_name: Reverse BFS
is_optimal: true
code: |
from collections import deque
def pacific_atlantic(heights: list[list[int]]) -> list[list[int]]:
if not heights or not heights[0]:
return []
m, n = len(heights), len(heights[0])
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
def bfs(starts: list[tuple[int, int]]) -> set[tuple[int, int]]:
reachable = set(starts)
queue = deque(starts)
while queue:
row, col = queue.popleft()
current_height = heights[row][col]
# Check all four neighbours
for dr, dc in directions:
new_row, new_col = row + dr, col + dc
# Skip if out of bounds, visited, or lower height
if (0 <= new_row < m and 0 <= new_col < n and
(new_row, new_col) not in reachable and
heights[new_row][new_col] >= current_height):
reachable.add((new_row, new_col))
queue.append((new_row, new_col))
return reachable
# Pacific borders: top row + left column
pacific_starts = [(0, c) for c in range(n)] + [(r, 0) for r in range(1, m)]
# Atlantic borders: bottom row + right column
atlantic_starts = [(m - 1, c) for c in range(n)] + [(r, n - 1) for r in range(m - 1)]
pacific_reachable = bfs(pacific_starts)
atlantic_reachable = bfs(atlantic_starts)
return [[r, c] for r, c in pacific_reachable & atlantic_reachable]
explanation: |
**Time Complexity:** O(m * n) — Each cell visited at most twice.
**Space Complexity:** O(m * n) — For the reachable sets and BFS queue.
This is the iterative BFS version of the same approach. Starting from all ocean boundary cells simultaneously, BFS explores cells level by level. Both approaches have the same complexity; BFS avoids potential stack overflow for very large grids.
- approach_name: Brute Force DFS
is_optimal: false
code: |
def pacific_atlantic(heights: list[list[int]]) -> list[list[int]]:
if not heights or not heights[0]:
return []
m, n = len(heights), len(heights[0])
result = []
def can_reach_ocean(start_row: int, start_col: int, ocean: str) -> bool:
visited = set()
def dfs(row: int, col: int, prev_height: int) -> bool:
# Check if we've reached the target ocean
if ocean == "pacific" and (row < 0 or col < 0):
return True
if ocean == "atlantic" and (row >= m or col >= n):
return True
# Skip invalid cells
if (row < 0 or row >= m or col < 0 or col >= n or
(row, col) in visited or heights[row][col] > prev_height):
return False
visited.add((row, col))
current = heights[row][col]
# Try all four directions
return (dfs(row - 1, col, current) or # up
dfs(row + 1, col, current) or # down
dfs(row, col - 1, current) or # left
dfs(row, col + 1, current)) # right
return dfs(start_row, start_col, heights[start_row][start_col])
# Check every cell
for row in range(m):
for col in range(n):
if can_reach_ocean(row, col, "pacific") and can_reach_ocean(row, col, "atlantic"):
result.append([row, col])
return result
explanation: |
**Time Complexity:** O((m * n)^2) — For each of m*n cells, we may visit up to m*n cells.
**Space Complexity:** O(m * n) — For the visited set and recursion stack per DFS.
This naive approach checks each cell individually for reachability to both oceans. It's correct but inefficient due to redundant exploration. For a 200x200 grid, this could mean up to 1.6 billion operations. Included to illustrate why the reverse-flow approach is necessary.