Taro Logo

Pacific Atlantic Water Flow

Medium
Meta logo
Meta
2 views
Topics:
Graphs

You are given 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 rainwater can flow to neighboring cells directly north, south, east, and west if the neighboring 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<sub>i</sub>, c<sub>i</sub>] denotes that rainwater can flow from cell (r<sub>i</sub>, c<sub>i</sub>) to both the Pacific and Atlantic oceans.

Example:

Consider the following heights matrix:

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]
]

In this case, the expected output would be:

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

Explain how to efficiently determine the cells from which water can flow to both the Pacific and Atlantic oceans. What is the time and space complexity of your solution? Also, explain how your code handles edge cases, such as an empty matrix or a single-cell matrix.

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What are the dimensions of the matrix, and what is the maximum possible size (rows and columns)?
  2. Can the elevation values in the matrix be negative?
  3. If a cell can reach both oceans, should I include it only once in the result?
  4. Is the matrix guaranteed to be rectangular (i.e., all rows have the same number of columns)?
  5. What should I return if the input matrix is empty (null or has zero rows/columns)?

Brute Force Solution

Approach

The problem asks us to find which locations in a grid can flow to both the Pacific and Atlantic oceans. A brute force approach involves checking every single location to see if it can reach both oceans.

Here's how the algorithm would work step-by-step:

  1. For each location in the grid, imagine starting a journey from that location.
  2. From that starting location, try going up, down, left, and right to neighboring locations, but only if the neighbor is not higher than your current location (water flows downhill or stays at the same level).
  3. Keep exploring from each reachable location by following the same rule until you cannot go any further.
  4. While exploring, check if you ever reach the edge of the grid that represents the Pacific ocean. If you do, mark that the starting location can reach the Pacific.
  5. Also, check if you ever reach the edge of the grid that represents the Atlantic ocean. If you do, mark that the starting location can reach the Atlantic.
  6. If, after exploring everywhere possible from a starting location, you find that it can reach both the Pacific and the Atlantic, then add that location to the answer.
  7. Repeat this entire process (starting a journey and exploring) for every single location in the grid.

Code Implementation

def pacific_atlantic(heights):
    if not heights:
        return []

    number_of_rows = len(heights)
    number_of_columns = len(heights[0])
    reachable_pacific = set()
    reachable_atlantic = set()
    result = []

    def explore(row, column, reachable_set):
        # Add current cell to reachable set.
        reachable_set.add((row, column))

        directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]

        for row_change, column_change in directions:
            new_row = row + row_change
            new_column = column + column_change

            # Check if valid neighbor
            if 0 <= new_row < number_of_rows and 0 <= new_column < number_of_columns \
                    and (new_row, new_column) not in reachable_set \
                    and heights[new_row][new_column] >= heights[row][column]:
                explore(new_row, new_column, reachable_set)

    # Explore from Pacific edge
    for row in range(number_of_rows):
        explore(row, 0, reachable_pacific)

    for column in range(number_of_columns):
        explore(0, column, reachable_pacific)

    # Explore from Atlantic edge
    for row in range(number_of_rows):
        explore(row, number_of_columns - 1, reachable_atlantic)

    for column in range(number_of_columns):
        explore(number_of_rows - 1, column, reachable_atlantic)

    # Find cells reachable from both
    for row in range(number_of_rows):
        for column in range(number_of_columns):
            if (row, column) in reachable_pacific and (row, column) in reachable_atlantic:
                result.append([row, column])

    return result

Big(O) Analysis

Time Complexity
O(m*n*m*n)The brute force solution iterates through each cell in the m x n grid, resulting in m*n starting points. From each starting point, a depth-first search (DFS) or breadth-first search (BFS) explores possible paths. In the worst case, the search from each cell might visit all other cells in the grid again (m*n). Therefore, the overall time complexity becomes O(m*n*m*n), where m and n are the dimensions of the grid.
Space Complexity
O(M * N)The algorithm uses a Depth-First Search (DFS) to explore reachable locations from each cell in the grid. During the exploration from each starting location, we need to keep track of visited locations to avoid cycles. This is done implicitly through the recursion stack and also requires maintaining potentially two boolean matrices of size M x N to track which cells can reach the Pacific and Atlantic oceans respectively. Therefore, the auxiliary space used is proportional to the size of the input grid, resulting in a space complexity of O(M * N), where M is the number of rows and N is the number of columns in the grid.

Optimal Solution

Approach

The key idea is to start from the oceans and work our way inland. We use the property that water can only flow from higher or equal elevation to lower elevation to determine what cells can reach each ocean.

Here's how the algorithm would work step-by-step:

  1. Imagine two separate teams of explorers, one starting from the Pacific Ocean coastline and the other from the Atlantic Ocean coastline.
  2. Each explorer team can only move to adjacent land cells that are at the same height or higher than the cell they are currently on.
  3. The Pacific team explores and marks all land cells reachable from the Pacific Ocean.
  4. Independently, the Atlantic team explores and marks all land cells reachable from the Atlantic Ocean.
  5. Finally, identify the land cells that both teams have marked. These are the cells that can reach both oceans.

Code Implementation

def pacific_atlantic(heights):
    if not heights:
        return []

    number_of_rows = len(heights)
    number_of_columns = len(heights[0])

    pacific_reachable = [[False] * number_of_columns for _ in range(number_of_rows)]
    atlantic_reachable = [[False] * number_of_columns for _ in range(number_of_rows)]

    def depth_first_search(row, column, reachable_matrix, previous_height):
        if row < 0 or row >= number_of_rows or column < 0 or column >= number_of_columns or \
           reachable_matrix[row][column] or heights[row][column] < previous_height:
            return

        reachable_matrix[row][column] = True

        # Explore adjacent cells with higher or equal height.
        depth_first_search(row + 1, column, reachable_matrix, heights[row][column])

        depth_first_search(row - 1, column, reachable_matrix, heights[row][column])

        depth_first_search(row, column + 1, reachable_matrix, heights[row][column])

        depth_first_search(row, column - 1, reachable_matrix, heights[row][column])

    # Start DFS from all border cells connected to the Pacific.
    for column_index in range(number_of_columns):
        depth_first_search(0, column_index, pacific_reachable, -1)

    for row_index in range(number_of_rows):
        depth_first_search(row_index, 0, pacific_reachable, -1)

    # Start DFS from all border cells connected to the Atlantic.
    for column_index in range(number_of_columns):
        depth_first_search(number_of_rows - 1, column_index, atlantic_reachable, -1)

    for row_index in range(number_of_rows):
        depth_first_search(row_index, number_of_columns - 1, atlantic_reachable, -1)

    result = []

    # Cells reachable from both oceans are added to the result.
    for row_index in range(number_of_rows):
        for column_index in range(number_of_columns):
            if pacific_reachable[row_index][column_index] and atlantic_reachable[row_index][column_index]:
                result.append([row_index, column_index])

    return result

Big(O) Analysis

Time Complexity
O(m*n)We perform two separate Depth-First Searches (DFS) or Breadth-First Searches (BFS). The first explores cells reachable from the Pacific Ocean and the second explores cells reachable from the Atlantic Ocean. In the worst case, each cell in the m x n grid can be visited during each of these searches. Thus, the time complexity is proportional to the size of the grid, leading to O(m*n), where m is the number of rows and n is the number of columns. The final step to identify common cells reachable from both oceans takes O(m*n) time, but that doesn't change the overall asymptotic complexity.
Space Complexity
O(M * N)The algorithm uses two boolean matrices, pacificReachable and atlanticReachable, to keep track of cells reachable from each ocean, where M is the number of rows and N is the number of columns in the input matrix. These matrices each have M * N elements, representing the visited status of each cell. Therefore, the auxiliary space used is proportional to the size of the input matrix. Thus the total space complexity is O(M * N).

Edge Cases

CaseHow to Handle
Null or empty matrixReturn an empty list if the input matrix is null or has zero rows/columns to avoid null pointer exceptions and invalid calculations.
Matrix with a single row or columnThe solution should correctly identify cells that flow to both oceans even with only one row or column.
Matrix with all identical valuesAll cells should be reachable from both oceans, leading to a result containing all cells.
Large matrix (potential stack overflow with recursion)Use iterative depth-first search (DFS) or breadth-first search (BFS) instead of recursive DFS to avoid stack overflow errors for large inputs.
Matrix with integer overflow potential during calculationsThe algorithm doesn't perform any arithmetic operations that could lead to an overflow, so this is not a concern.
Matrix with negative height values (invalid input)Treat negative height values as an error or assume all values are non-negative based on problem constraints.
A matrix where no cell can reach both oceansThe algorithm should correctly return an empty list when no cell satisfies the condition.
Connectivity cycles that could cause infinite loopsThe visited set prevents cycles during DFS/BFS by marking cells that are currently being explored.