Start learning free

LESSON 485 OF 543 · GRAPHS

Pacific Atlantic Water Flow in Rust: Two Multi-Source BFS Searches

Practice O(m · n) Read the full explanation

A rectangular island lies between two oceans. The Pacific Ocean touches its left and top edges, and the Atlantic Ocean touches its right and bottom edges.

The island is a grid of square cells with m rows and n columns, given as heights: an array of rows, where each row is an array of integers and heights[r][c] is the height of the cell in row r and column c. Rows are numbered from 0, top to bottom, and columns from 0, left to right.

Rain falls on every cell. Water flows from a cell to a neighboring cell (up, down, left or right) when the neighbor's height is less than or equal to the cell's height, and from any cell on an edge into the ocean that touches that edge.

Return the cells from which water can flow to both oceans, as a Vec of [r, c] pairs, in reading order: row by row from the top, and left to right within a row.

 

Example 1:

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: [0, 4] is on the top edge (Pacific) and on the right edge (Atlantic).
From [2, 2], height 5, water runs up to the Pacific through [1, 2] and [0, 2] (heights 3 and 2),
and right to the Atlantic through [2, 3] and [2, 4] (heights 3 and 1).
From [3, 1], height 7, it runs left to [3, 0] (height 6, on the Pacific's edge) and down to [4, 1]
(height 1, on the Atlantic's edge).
[2, 1], height 4, reaches the Pacific through [2, 0], but every path to the Atlantic needs a step up.

Example 2:

Input: heights = [[1]]
Output: [[0, 0]]
Explanation: The only cell touches all four edges, so its water reaches both oceans.

Example 3:

Input: heights = [
  [5, 5, 5],
  [5, 1, 5],
  [5, 5, 5]
]
Output: [[0, 0], [0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1], [2, 2]]
Explanation: Water flows between cells of equal height, so the 5s pass it along the border
to both oceans. The 1 in the middle is lower than all its neighbors: its water stays where it is.

 

Constraints:

  • 1 <= m, n <= 200
  • Every row has n values.
  • 0 <= heights[r][c] <= 105

Note:

Your algorithm should run in O(m · n) time and use O(m · n) extra space.

Following the water down from every cell, one search per cell, can cost O((m · n)2): think of a large flat lake in the middle of the island, where each cell's search wanders over the whole lake before it gives up. The tutorial turns the question around and climbs up from the oceans instead. A BFS starts from all the cells along the Pacific's edges at once, as the BFS in 01 Matrix starts from all the zeros, and moves to a neighbor only when the neighbor is at least as high, because water from there could flow down to here. Every cell it reaches can send its water to the Pacific. A second BFS does the same from the Atlantic's edges, and the answer is the cells that both searches reach. Each search visits every cell at most once.

STEP 1 OF 25
    Ctrl + Enter to check

    RUST · GRAPHS

    Pacific Atlantic Water Flow in Rust, explained

    Problem overview

    An island is a grid of cells, heights, with m rows and n columns. The Pacific Ocean touches the left and top edges, and the Atlantic Ocean touches the right and bottom edges. Water flows from a cell to a neighbor above, below, left or right when the neighbor's height is less than or equal to the cell's, and from any cell on an edge into the ocean that touches that edge. The function pacific_atlantic returns the cells from which water can reach both oceans, as a vector of [r, c] pairs in reading order: row by row from the top, left to right within a row.

    For the grid

    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
    

    the answer is [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]]. The cell [0, 4] is on the top edge and on the right edge, so it touches both oceans. The cell [2, 2] has height 5, and water runs up to the Pacific through heights 3 and 2, and right to the Atlantic through heights 3 and 1. The cell [2, 1] has height 4 and reaches the Pacific through [2, 0], but every path to the Atlantic needs a step up.

    For [[1]] the answer is [[0, 0]], because the single cell touches all four edges. For a ring of 5s around a single 1 the answer is every cell except the middle one: water moves between equal heights, and the 1 is lower than all its neighbors.

    The lesson asks for O(m · n) time and O(m · n) extra space.

    Approach: climb up from each ocean

    The direct idea follows the water down from every cell, one search per cell. That can cost O((m · n)²): think of a large flat lake in the middle of the island, where each cell's search wanders over the whole lake before giving up.

    Turn the question around. Start at an ocean and climb. A search that begins on all the cells along an ocean's two edges moves to a neighbor only when the neighbor is at least as high as the current cell. If the neighbor is at least as high, its water can flow down into the current cell, and from there along the same path to the ocean. So every cell the search reaches can send its water to that ocean.

    Run this once from the Pacific's edges and once from the Atlantic's edges. The answer is the cells that both searches reach. Each search is a breadth-first search that starts from many cells at once and marks every cell at most once, so each takes O(m · n) time.

    For the grid above, here is what each search marks (T means reached):

    pacific    atlantic
    TTTTT      ....T
    TTTTT      ...TT
    TTT..      ..TTT
    TT...      TTTTT
    T....      TTTTT
    

    The cells marked in both pictures are the seven cells of the answer.

    Step by step

    1. Store the size. rows is heights.len(), and cols is heights[0].len(), since every row has the same length.
    2. Search from the Pacific with Self::reach(&heights, 0, 0). The helper takes an edge row and an edge column and returns a grid of booleans marking every cell whose water can flow to that row or that column. The Pacific touches row 0 and column 0.
    3. Search from the Atlantic with Self::reach(&heights, rows - 1, cols - 1), the last row and the last column. One helper serves both oceans.
    4. Create result as an empty Vec<Vec<i32>>.
    5. Loop over rows, then columns, with r in 0..rows and c in 0..cols. This order is the reading order the answer needs.
    6. Keep a cell when pacific[r][c] and atlantic[r][c] are both true, by pushing vec![r as i32, c as i32].
    7. Return result without a semicolon.
    8. Write reach with heights: &Vec<Vec<i32>>, edge_row: usize and edge_col: usize, returning a Vec<Vec<bool>>. The & borrows the grid, so pacific_atlantic can pass it twice.
    9. Create seen with vec![vec![false; cols]; rows]. The macro clones the inner vector for every row, so each row is its own vector.
    10. Create the queue as a VecDeque<(usize, usize)> of cells, each a row and column tuple.
    11. Seed the search. Loop over every cell, and for each one with r == edge_row || c == edge_col, mark it in seen and push (r, c). These cells touch the ocean, so their water flows straight into it. A cell is marked when it joins the queue, never later, so no cell joins twice.
    12. Create directions as a [(i32, i32); 4] with the moves up, down, left and right. They are i32 because a move can be negative, while indexes are usize.
    13. Loop with while let Some((row, col)) = queue.pop_front() until the queue is empty, and loop over the moves with for &(dr, dc) in &directions.
    14. Compute the neighbor as i32 values: next_row = row as i32 + dr and next_col = col as i32 + dc.
    15. Skip off-grid neighbors by checking that both are at least 0 and below rows as i32 or cols as i32.
    16. Turn them back into usize by shadowing both names in one let with a tuple. The check above makes the cast safe.
    17. Climb when allowed: if the neighbor isn't seen and heights[next_row][next_col] >= heights[row][col], mark it and push it.
    18. Return seen. A true entry marks a cell whose water can reach this ocean.

    The solution in Rust

    use std::collections::VecDeque;
    
    impl Solution {
        fn pacific_atlantic(heights: Vec<Vec<i32>>) -> Vec<Vec<i32>> {
            let rows = heights.len();
            let cols = heights[0].len();
            let pacific = Self::reach(&heights, 0, 0);
            let atlantic = Self::reach(&heights, rows - 1, cols - 1);
            let mut result: Vec<Vec<i32>> = Vec::new();
            for r in 0..rows {
                for c in 0..cols {
                    if pacific[r][c] && atlantic[r][c] {
                        result.push(vec![r as i32, c as i32]);
                    }
                }
            }
            result
        }
    
        fn reach(heights: &Vec<Vec<i32>>, edge_row: usize, edge_col: usize) -> Vec<Vec<bool>> {
            let rows = heights.len();
            let cols = heights[0].len();
            let mut seen = vec![vec![false; cols]; rows];
            let mut queue: VecDeque<(usize, usize)> = VecDeque::new();
            for r in 0..rows {
                for c in 0..cols {
                    if r == edge_row || c == edge_col {
                        seen[r][c] = true;
                        queue.push_back((r, c));
                    }
                }
            }
            let directions: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
            while let Some((row, col)) = queue.pop_front() {
                for &(dr, dc) in &directions {
                    let next_row = row as i32 + dr;
                    let next_col = col as i32 + dc;
                    if next_row >= 0 && next_row < rows as i32 && next_col >= 0 && next_col < cols as i32 {
                        let (next_row, next_col) = (next_row as usize, next_col as usize);
                        if !seen[next_row][next_col] && heights[next_row][next_col] >= heights[row][col] {
                            seen[next_row][next_col] = true;
                            queue.push_back((next_row, next_col));
                        }
                    }
                }
            }
            seen
        }
    }
    

    The Rust details worth a closer look:

    • Signed moves, unsigned indexes. Row and column indexes are usize, which can't be negative. Computing row - 1 at row 0 would underflow: a panic in a debug build and a wrap to a huge number in an optimized build. So the neighbor is computed in i32, range-checked, and only then converted with as usize.
    • Shadowing. let (next_row, next_col) = (next_row as usize, next_col as usize); declares new variables with the same names. The old i32 values are no longer reachable, so the later indexing can't use them by mistake.
    • for &(dr, dc) in &directions. Looping over &directions gives references to the tuples. The pattern &(dr, dc) copies the two numbers out.
    • Destructuring in while let. pop_front() returns an Option<(usize, usize)>. The pattern Some((row, col)) takes the tuple apart, and the loop ends at None.
    • &Vec<Vec<i32>>. The helper only reads the grid, so it borrows it. Rust code more often takes &[Vec<i32>] for this, but the lesson's signature works the same here.
    • vec![vec![false; cols]; rows]. The macro clones its first argument into every place, which works because Vec<bool> is Clone. Each row is a separate vector, so setting one cell doesn't change another row.

    Complexity

    Time: O(m · n). Each search marks a cell once, when it joins the queue, so each cell leaves the queue at most once. Each one looks at four neighbors, which is constant work. The two searches and the final loop each take O(m · n).

    Space: O(m · n). Each search has a seen grid and a queue of at most m · n cells. The two grids pacific and atlantic are alive together. The answer can hold every cell in the worst case.

    Edge cases and common mistakes

    • Using > instead of >=. Water flows between cells of equal height, so the climb must allow equal heights. With >, the ring of 5s in the third example would stop at the corners and miss cells.
    • Climbing the wrong way. The search walks against the flow, to neighbors that are at least as high. Moving to lower neighbors would answer a different question.
    • A single cell. For [[1]] the edge row and column are both 0 for both oceans, so the one cell is marked by both searches.
    • One row or one column. For [[1, 2, 3]] every cell is on the top edge, so the Pacific reaches all of them, and the Atlantic touches the last column and the bottom row, which is the same row. The answer is all three cells. For [[3], [2], [1]] it is all three cells too.
    • Corners. The top-right and bottom-left cells touch both oceans. They are seeded in both searches, so they always appear in the answer.
    • Marking late. Marking a cell when it leaves the queue, instead of when it joins, lets a cell be queued several times.
    • Result order. The final loop goes row by row, so the output is in reading order without sorting.
    • Lakes. A cell lower than all its neighbors, like the middle 1 in the third example, is never marked. The search moves only to cells at least as high as the current one, and every neighbor of the 1 is higher than it, so the search can't step down into it.

    Testing

    • The 5-by-5 grid above gives [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]].
    • [[1]] gives [[0, 0]].
    • [[5, 5, 5], [5, 1, 5], [5, 5, 5]] gives [[0, 0], [0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1], [2, 2]].
    • [[1, 2, 3]] gives [[0, 0], [0, 1], [0, 2]].
    • [[3], [2], [1]] gives [[0, 0], [1, 0], [2, 0]].
    • [[2, 1], [1, 2]] gives [[0, 0], [0, 1], [1, 0], [1, 1]].
    Keep going with Rust500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free