417. Pacific Atlantic Water Flow - Explanation

Problem Link

Description

You are given a rectangular island heights where heights[r][c] represents the height above sea level of the cell at coordinate (r, c).

The islands borders the Pacific Ocean from the top and left sides, and borders the Atlantic Ocean from the bottom and right sides.

Water can flow in four directions (up, down, left, or right) from a cell to a neighboring cell with height equal or lower. Water can also flow into the ocean from cells adjacent to the ocean.

Find all cells where water can flow from that cell to both the Pacific and Atlantic oceans. Return it as a 2D list where each element is a list [r, c] representing the row and column of the cell. You may return the answer in any order.

Example 1:

Input: heights = [
  [4,2,7,3,4],
  [7,4,6,4,7],
  [6,3,5,3,6]
]

Output: [[0,2],[0,4],[1,0],[1,1],[1,2],[1,3],[1,4],[2,0]]

Example 2:

Input: heights = [[1],[1]]

Output: [[0,0],[0,1]]

Constraints:

  • 1 <= heights.length, heights[r].length <= 100
  • 0 <= heights[r][c] <= 1000


Topics

Recommended Time & Space Complexity

You should aim for a solution with O(m * n) time and O(m * n) space, where m is the number of rows and n is the number of columns in the grid.


Hint 1

A brute force solution would be to traverse each cell in the grid and run a BFS from each cell to check if it can reach both oceans. This would result in an O((m * n)^2) solution. Can you think of a better way? Maybe you should consider a reverse way of traversing.


Hint 2

We can use the Depth First Search (DFS) algorithm starting from the border cells of the grid. However, we reverse the condition such that the next visiting cell should have a height greater than or equal to the current cell. For the top and left borders connected to the Pacific Ocean, we use a hash set called pacific and run a DFS from each of these cells, visiting nodes recursively. Similarly, for the bottom and right borders connected to the Atlantic Ocean, we use a hash set called atlantic and run a DFS. The required coordinates are the cells that exist in both the pacific and atlantic sets. How do you implement this?


Hint 3

We perform DFS from the border cells, using their respective hash sets. During the DFS, we recursively visit the neighbouring cells that are unvisited and have height greater than or equal to the current cell's height and add the current cell's coordinates to the corresponding hash set. Once the DFS completes, we traverse the grid and check if a cell exists in both the hash sets. If so, we add that cell to the result list.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Depth First Search (DFS) - Recursive traversal to explore connected components and paths in grids
  • Breadth First Search (BFS) - Level-by-level traversal as an alternative approach for grid exploration
  • 2D Matrix/Grid Traversal - Moving in four directions and tracking visited cells
  • Multi-source Search - Starting BFS/DFS from multiple boundary cells simultaneously

1. Brute Force (Backtracking)

Intuition

For each cell, we try to see where water can flow if it starts there.

Rule: water can flow from a cell to a neighbor only if the neighbor’s height is <= current height (downhill or flat).

So for every (r, c):

  • Run DFS exploring all paths that keep going to same or lower heights.
  • If during DFS we ever step out of the grid:
    • Out of top/left boundary ⇒ it can reach the Pacific.
    • Out of bottom/right boundary ⇒ it can reach the Atlantic.
  • If both oceans are reachable, include (r, c) in the answer.

To avoid infinite loops, we temporarily mark the current cell as visited (here by setting it to inf) while exploring, then restore it after backtracking.

Algorithm

  1. For each cell (r, c):
    • Set pacific = false, atlantic = false.
    • DFS(r, c, prevVal = +∞).
  2. DFS(r, c, prevVal):
    • If r < 0 or c < 0: set pacific = true, return.
    • If r == ROWS or c == COLS: set atlantic = true, return.
    • If heights[r][c] > prevVal: return (can't flow uphill).
    • Mark cell as visited (temporary), explore 4 neighbors with prevVal = currentHeight.
    • If both oceans found, you can stop early.
    • Restore the cell value (backtrack).
  3. If after DFS both flags are true, add (r, c) to result.
  4. Return result list.
class Solution:
    def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
        ROWS, COLS = len(heights), len(heights[0])
        directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]

        pacific = atlantic = False

        def dfs(r, c, prevVal):
            nonlocal pacific, atlantic
            if r < 0 or c < 0:
                pacific = True
                return
            if r >= ROWS or c >= COLS:
                atlantic = True
                return
            if heights[r][c] > prevVal:
                return

            tmp = heights[r][c]
            heights[r][c] = float('inf')
            for dx, dy in directions:
                dfs(r + dx, c + dy, tmp)
                if pacific and atlantic:
                    break
            heights[r][c] = tmp

        res = []
        for r in range(ROWS):
            for c in range(COLS):
                pacific = False
                atlantic = False
                dfs(r, c, float('inf'))
                if pacific and atlantic:
                    res.append([r, c])
        return res

Time & Space Complexity

  • Time complexity: O(mn4mn)O(m * n * 4 ^ {m * n})
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns.


Intuition

Instead of starting DFS from every cell (slow), we reverse the thinking:

A cell can reach an ocean if water can flow from that cell to the ocean (downhill/flat).
Reverse it: start from the ocean borders and move uphill/flat (to neighbors with height >= current).
If you can climb from the ocean to a cell, then that cell can flow down to that ocean.

So we do 2 DFS runs:

  • From all Pacific border cells (top row + left column) → mark all reachable cells in pac
  • From all Atlantic border cells (bottom row + right column) → mark all reachable cells in atl

Answer = cells that are in both sets.

Algorithm

  1. Create two visited sets: pac, atl.
  2. DFS rule (reverse flow):
    • From cell (r, c), you may go to a neighbor (nr, nc) only if
      heights[nr][nc] >= heights[r][c] (uphill or same).
  3. Run DFS from every Pacific border cell, fill pac.
  4. Run DFS from every Atlantic border cell, fill atl.
  5. For every cell in the grid, if it's in both pac and atl, add it to result.
  6. Return result.
class Solution:
    def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
        ROWS, COLS = len(heights), len(heights[0])
        pac, atl = set(), set()

        def dfs(r, c, visit, prevHeight):
            if ((r, c) in visit or
                r < 0 or c < 0 or
                r == ROWS or c == COLS or
                heights[r][c] < prevHeight
            ):
                return
            visit.add((r, c))
            dfs(r + 1, c, visit, heights[r][c])
            dfs(r - 1, c, visit, heights[r][c])
            dfs(r, c + 1, visit, heights[r][c])
            dfs(r, c - 1, visit, heights[r][c])

        for c in range(COLS):
            dfs(0, c, pac, heights[0][c])
            dfs(ROWS - 1, c, atl, heights[ROWS - 1][c])

        for r in range(ROWS):
            dfs(r, 0, pac, heights[r][0])
            dfs(r, COLS - 1, atl, heights[r][COLS - 1])

        res = []
        for r in range(ROWS):
            for c in range(COLS):
                if (r, c) in pac and (r, c) in atl:
                    res.append([r, c])
        return res

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns.


Intuition

Do the same “reverse flow” idea, but with BFS.

A cell can flow to an ocean if you can start from the ocean border and move backwards into the grid using the rule:

  • from (r, c) you can go to neighbor (nr, nc) if heights[nr][nc] >= heights[r][c]
    (because in the real direction water would flow from higher/equal down to (r, c)).

So:

  • Multi-source BFS from all Pacific border cells marks pac[r][c] = True
  • Multi-source BFS from all Atlantic border cells marks atl[r][c] = True
    Cells that are True in both are the answer.

Algorithm

  1. Create two boolean grids pac and atl (same size as heights), all false.
  2. Build two source lists:
    • pacificSources: all cells on top row + left column
    • atlanticSources: all cells on bottom row + right column
  3. Run BFS(sources, oceanGrid):
    • Initialize queue with all sources.
    • While queue not empty:
      • pop (r, c), mark oceanGrid[r][c] = true
      • for each neighbor (nr, nc) in 4 directions:
        • if inside grid AND not visited in oceanGrid AND heights[nr][nc] >= heights[r][c],
          push (nr, nc) into queue.
  4. Run BFS for Pacific → fill pac.
  5. Run BFS for Atlantic → fill atl.
  6. Iterate all cells; if pac[r][c] and atl[r][c] are both true, add [r, c] to result.
  7. Return result.
class Solution:
    def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
        ROWS, COLS = len(heights), len(heights[0])
        directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
        pac = [[False] * COLS for _ in range(ROWS)]
        atl = [[False] * COLS for _ in range(ROWS)]

        def bfs(source, ocean):
            q = deque(source)
            while q:
                r, c = q.popleft()
                ocean[r][c] = True
                for dr, dc in directions:
                    nr, nc = r + dr, c + dc
                    if (0 <= nr < ROWS and 0 <= nc < COLS and
                        not ocean[nr][nc] and
                        heights[nr][nc] >= heights[r][c]
                    ):
                        q.append((nr, nc))

        pacific = []
        atlantic = []
        for c in range(COLS):
            pacific.append((0, c))
            atlantic.append((ROWS - 1, c))

        for r in range(ROWS):
            pacific.append((r, 0))
            atlantic.append((r, COLS - 1))

        bfs(pacific, pac)
        bfs(atlantic, atl)

        res = []
        for r in range(ROWS):
            for c in range(COLS):
                if pac[r][c] and atl[r][c]:
                    res.append([r, c])
        return res

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns.


Common Pitfalls

Using the Wrong Flow Direction Comparison

When starting from ocean borders and moving inward, the comparison must check if the neighbor height is greater than or equal to the current height (water flows uphill in reverse). Using the normal downhill comparison (neighbor <= current) gives incorrect reachability since the logic is reversed.

Running Separate DFS/BFS for Each Cell

A brute force approach that runs a full traversal from every cell to check ocean reachability leads to O((m*n)^2) time complexity or worse. The efficient approach is to run only two traversals total: one multi-source search from all Pacific border cells and one from all Atlantic border cells.

Not Properly Handling Edge and Corner Cells

Edge cells border one ocean while corner cells border both oceans. A common bug is forgetting that cells on the top row and left column touch the Pacific, while cells on the bottom row and right column touch the Atlantic. Missing any border initialization causes incomplete reachability sets.