Facebook Pixel

778. Swim in Rising Water

Problem Description

You have an n x n grid where each cell contains an integer representing the elevation at that position. When it starts raining, the water level rises uniformly over time. At any time t, the water level is at height t, which means any cell with elevation ≀ t becomes submerged (accessible for swimming).

You start at the top-left corner (0, 0) and want to reach the bottom-right corner (n-1, n-1). You can swim from one cell to any of its 4 adjacent cells (up, down, left, right) only if both cells have elevations ≀ t at time t. Swimming takes no time - you can travel infinite distances instantly as long as the cells are accessible.

The goal is to find the minimum time t at which you can swim from the starting position to the ending position. In other words, what's the smallest water level needed so that there exists a valid path from (0, 0) to (n-1, n-1) where all cells along the path are submerged?

For example, if you have a 2x2 grid:

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

At time t = 3, all cells are submerged (have elevation ≀ 3), allowing you to swim from (0,0) to (1,1). The answer would be 3 since you need to wait until the water reaches level 3 to have a complete path.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The problem involves an nΓ—n grid where we need to find a path from one cell to another. Each cell can be considered a node, and the 4-directional adjacency represents edges. The water level constraint determines which edges are traversable at time t.

Is it a tree?

  • No: The grid structure has cycles (you can move in 4 directions and potentially return to the same cell), so it's not a tree structure.

Is the problem related to directed acyclic graphs?

  • No: The grid allows bidirectional movement between adjacent cells, and cycles are possible, so it's not a DAG.

Is the problem related to shortest paths?

  • No: We're not looking for the shortest path in terms of distance. Instead, we're looking for the minimum time (water level) that allows any valid path to exist from start to end.

Does the problem involve connectivity?

  • Yes: The core question is about connectivity - at what minimum water level do the start and end positions become connected through submerged cells? We need to determine when cells with elevation ≀ t form a connected component containing both (0,0) and (n-1, n-1).

Disjoint Set Union

  • Yes: The flowchart leads us to DSU (Disjoint Set Union), which is exactly what the solution uses. The algorithm processes cells in order of increasing elevation (time), unions adjacent submerged cells, and checks when the start and end positions belong to the same connected component.

Conclusion: The flowchart correctly identifies this as a connectivity problem best solved with Disjoint Set Union. While DFS could be used (by trying different water levels and checking connectivity), DSU is more efficient as it incrementally builds connected components as the water rises.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Think about what happens as the water level rises from 0 to higher values. Initially, at time t = 0, only cells with elevation 0 are accessible. As time increases to t = 1, cells with elevation ≀ 1 become accessible, and so on.

The key insight is that we're looking for the exact moment when a path first becomes available from start to end. Before this critical time, the start and end positions are in separate "islands" of accessible cells. At the critical time, these islands merge into one connected region.

This naturally leads to a Union-Find (Disjoint Set Union) approach. We can simulate the water rising by processing cells in order of their elevation. For each elevation level:

  1. The cell at that elevation becomes "active" (submerged)
  2. We check its 4 neighbors - if they're already submerged (elevation ≀ current time), we unite them into the same connected component
  3. After each union operation, we check if the start (0, 0) and end (n-1, n-1) are now in the same connected component

The moment they become connected is our answer - that's the minimum water level needed.

Why is this more efficient than trying different water levels with DFS? Instead of repeatedly checking connectivity at different water levels (which would involve multiple graph traversals), we build the connectivity incrementally. We process each cell exactly once in elevation order, and union operations are nearly constant time with path compression. This gives us an elegant O(nΒ² log n) solution (dominated by sorting if needed) or O(nΒ²) if we process elevations from 0 to nΒ²-1 in order.

The solution cleverly uses the array hi to map each elevation value to its position in the grid, allowing us to process cells in the exact order they become submerged.

Learn more about Depth-First Search, Breadth-First Search, Union Find, Binary Search and Heap (Priority Queue) patterns.

Solution Approach

The implementation uses Union-Find (Disjoint Set Union) to efficiently track when the start and end positions become connected as the water level rises.

Data Structures:

  • p: Parent array for Union-Find, where p[i] stores the parent of node i. Initially, each cell is its own parent.
  • hi: Maps each elevation value to its flattened grid position. Since elevations range from 0 to nΒ²-1, hi[elevation] = row * n + col.

Algorithm Steps:

  1. Initialize Union-Find structure:

    • Create parent array p where each cell (flattened to 1D index) is initially its own parent
    • A cell at position (i, j) is mapped to index i * n + j
  2. Build elevation-to-position mapping:

    for i, row in enumerate(grid):
        for j, h in enumerate(row):
            hi[h] = i * n + j

    This allows us to process cells in order of increasing elevation.

  3. Process cells by increasing elevation (time):

    for t in range(n * n):
        i, j = hi[t] // n, hi[t] % n

    At time t, the cell with elevation t becomes active. We extract its 2D coordinates from the flattened index.

  4. Union with adjacent submerged cells:

    • For each of the 4 adjacent cells (x, y):
      • Check if it's within bounds and already submerged (grid[x][y] <= t)
      • If yes, union it with the current cell using p[find(x * n + y)] = find(hi[t])
  5. Check connectivity:

    • After each union operation, check if find(0) == find(n * n - 1)
    • If the start (index 0) and end (index nΒ²-1) are in the same component, return current time t

Find function with path compression:

def find(x):
    if p[x] != x:
        p[x] = find(p[x])  # Path compression
    return p[x]

The algorithm guarantees we find the minimum time because we process cells in strictly increasing order of elevation. The first moment the start and end become connected is the minimum water level needed for a valid path to exist.

Time Complexity: O(nΒ² Γ— Ξ±(nΒ²)) where Ξ± is the inverse Ackermann function, practically O(nΒ²) Space Complexity: O(nΒ²) for the parent array and elevation mapping

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small 3x3 grid example to illustrate the solution approach:

Grid:
[[0, 6, 7],
 [2, 8, 1],
 [3, 4, 5]]

Step 1: Initialize Union-Find

  • Create parent array p where each cell is its own parent
  • p = [0, 1, 2, 3, 4, 5, 6, 7, 8] (each index points to itself)
  • Flattened indices: (0,0)β†’0, (0,1)β†’1, (0,2)β†’2, (1,0)β†’3, (1,1)β†’4, (1,2)β†’5, (2,0)β†’6, (2,1)β†’7, (2,2)β†’8

Step 2: Build elevation-to-position mapping

hi[0] = 0 (position 0,0)
hi[1] = 5 (position 1,2)
hi[2] = 3 (position 1,0)
hi[3] = 6 (position 2,0)
hi[4] = 7 (position 2,1)
hi[5] = 8 (position 2,2)
hi[6] = 1 (position 0,1)
hi[7] = 2 (position 0,2)
hi[8] = 4 (position 1,1)

Step 3: Process cells by increasing elevation

Time t=0: Cell (0,0) with elevation 0 becomes active

  • Check neighbors: No neighbors are submerged yet
  • Components: {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}
  • Start (0) and end (8) not connected

Time t=1: Cell (1,2) with elevation 1 becomes active

  • Check neighbors: No neighbors are submerged yet
  • Components: {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}
  • Start (0) and end (8) not connected

Time t=2: Cell (1,0) with elevation 2 becomes active

  • Check neighbor (0,0): elevation 0 ≀ 2, so union them
  • Union operation: p[find(3)] = find(0) β†’ Components merge
  • Components: {0,3}, {1}, {2}, {4}, {5}, {6}, {7}, {8}
  • Start (0) and end (8) not connected

Time t=3: Cell (2,0) with elevation 3 becomes active

  • Check neighbor (1,0): elevation 2 ≀ 3, so union them
  • Union operation: p[find(6)] = find(3) β†’ Components merge
  • Components: {0,3,6}, {1}, {2}, {4}, {5}, {7}, {8}
  • Start (0) and end (8) not connected

Time t=4: Cell (2,1) with elevation 4 becomes active

  • Check neighbor (2,0): elevation 3 ≀ 4, so union them
  • Union operation: p[find(7)] = find(6) β†’ Components merge
  • Components: {0,3,6,7}, {1}, {2}, {4}, {5}, {8}
  • Start (0) and end (8) not connected

Time t=5: Cell (2,2) with elevation 5 becomes active

  • Check neighbor (2,1): elevation 4 ≀ 5, so union them
  • Check neighbor (1,2): elevation 1 ≀ 5, so union them
  • Union operations merge components
  • Components: {0,3,6,7,8,5}, {1}, {2}, {4}
  • Start (0) and end (8) are now connected!
  • Return t=5

The minimum water level needed is 5. At this level, we can swim from (0,0) β†’ (1,0) β†’ (2,0) β†’ (2,1) β†’ (2,2), forming a valid path where all cells have elevation ≀ 5.

Solution Implementation

1class Solution:
2    def swimInWater(self, grid: List[List[int]]) -> int:
3        """
4        Find minimum time to swim from top-left to bottom-right.
5        Uses Union-Find to connect cells as water level rises.
6      
7        Args:
8            grid: 2D grid where grid[i][j] represents elevation at (i, j)
9      
10        Returns:
11            Minimum time required to reach destination
12        """
13      
14        def find(node: int) -> int:
15            """Find root of the set containing node with path compression"""
16            if parent[node] != node:
17                parent[node] = find(parent[node])  # Path compression
18            return parent[node]
19      
20        n = len(grid)
21      
22        # Initialize Union-Find structure
23        # Each cell (i, j) is mapped to index i * n + j
24        parent = list(range(n * n))
25      
26        # Create mapping from elevation to cell position
27        # elevation_to_position[h] stores the flattened index of cell with elevation h
28        elevation_to_position = [0] * (n * n)
29        for row_idx, row in enumerate(grid):
30            for col_idx, elevation in enumerate(row):
31                elevation_to_position[elevation] = row_idx * n + col_idx
32      
33        # Process cells in order of increasing elevation (time)
34        for time in range(n * n):
35            # Get coordinates of cell with current elevation
36            current_cell_idx = elevation_to_position[time]
37            current_row = current_cell_idx // n
38            current_col = current_cell_idx % n
39          
40            # Check all four adjacent cells
41            directions = [(0, -1), (0, 1), (1, 0), (-1, 0)]  # left, right, down, up
42            for delta_row, delta_col in directions:
43                neighbor_row = current_row + delta_row
44                neighbor_col = current_col + delta_col
45              
46                # If neighbor is valid and has elevation <= current time, union them
47                if (0 <= neighbor_row < n and 
48                    0 <= neighbor_col < n and 
49                    grid[neighbor_row][neighbor_col] <= time):
50                  
51                    neighbor_idx = neighbor_row * n + neighbor_col
52                    # Union the two cells
53                    parent[find(neighbor_idx)] = find(current_cell_idx)
54              
55                # Check if start and end are connected
56                start_idx = 0  # Top-left corner (0, 0)
57                end_idx = n * n - 1  # Bottom-right corner (n-1, n-1)
58                if find(start_idx) == find(end_idx):
59                    return time
60      
61        return -1  # Should never reach here for valid input
62
1class Solution {
2    // Parent array for Union-Find data structure
3    private int[] parent;
4
5    public int swimInWater(int[][] grid) {
6        int n = grid.length;
7      
8        // Initialize Union-Find parent array
9        // Each cell initially points to itself as parent
10        parent = new int[n * n];
11        for (int i = 0; i < parent.length; i++) {
12            parent[i] = i;
13        }
14      
15        // Create a mapping from elevation value to its position in grid
16        // heightToIndex[h] stores the flattened index of cell with height h
17        int[] heightToIndex = new int[n * n];
18        for (int row = 0; row < n; row++) {
19            for (int col = 0; col < n; col++) {
20                int height = grid[row][col];
21                int flattenedIndex = row * n + col;
22                heightToIndex[height] = flattenedIndex;
23            }
24        }
25      
26        // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
27        int[] directions = {-1, 0, 1, 0, -1};
28      
29        // Process cells in order of increasing elevation (time)
30        for (int time = 0; time < n * n; time++) {
31            // Get the position of cell with current elevation
32            int currentIndex = heightToIndex[time];
33            int currentRow = currentIndex / n;
34            int currentCol = currentIndex % n;
35          
36            // Connect current cell with all adjacent cells that have been processed
37            // (cells with elevation <= current time)
38            for (int dir = 0; dir < 4; dir++) {
39                int nextRow = currentRow + directions[dir];
40                int nextCol = currentCol + directions[dir + 1];
41              
42                // Check if adjacent cell is within bounds and already accessible
43                if (nextRow >= 0 && nextRow < n && nextCol >= 0 && nextCol < n 
44                    && grid[nextRow][nextCol] <= time) {
45                    // Union the two cells in the disjoint set
46                    int nextIndex = nextRow * n + nextCol;
47                    parent[find(nextIndex)] = find(currentIndex);
48                }
49              
50                // Check if start (0,0) and end (n-1,n-1) are connected
51                if (find(0) == find(n * n - 1)) {
52                    return time;
53                }
54            }
55        }
56      
57        // This should never be reached given problem constraints
58        return -1;
59    }
60
61    // Find operation with path compression for Union-Find
62    private int find(int x) {
63        if (parent[x] != x) {
64            // Path compression: make x point directly to root
65            parent[x] = find(parent[x]);
66        }
67        return parent[x];
68    }
69}
70
1class Solution {
2public:
3    vector<int> parent;  // Union-Find parent array
4  
5    int swimInWater(vector<vector<int>>& grid) {
6        int n = grid.size();
7      
8        // Initialize Union-Find structure
9        // Each cell is initially its own parent
10        parent.resize(n * n);
11        for (int i = 0; i < parent.size(); ++i) {
12            parent[i] = i;
13        }
14      
15        // Create a mapping from elevation value to its position
16        // heightToIndex[h] stores the flattened index of cell with elevation h
17        vector<int> heightToIndex(n * n);
18        for (int i = 0; i < n; ++i) {
19            for (int j = 0; j < n; ++j) {
20                heightToIndex[grid[i][j]] = i * n + j;
21            }
22        }
23      
24        // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
25        vector<int> directions = {-1, 0, 1, 0, -1};
26      
27        // Process cells in increasing order of elevation (time)
28        for (int time = 0; time < n * n; ++time) {
29            // Get the position of the cell with elevation equal to current time
30            int currentIndex = heightToIndex[time];
31            int row = currentIndex / n;
32            int col = currentIndex % n;
33          
34            // Check all 4 adjacent cells
35            for (int k = 0; k < 4; ++k) {
36                int newRow = row + directions[k];
37                int newCol = col + directions[k + 1];
38              
39                // If adjacent cell is valid and has elevation <= current time,
40                // union it with current cell
41                if (newRow >= 0 && newRow < n && newCol >= 0 && newCol < n && 
42                    grid[newRow][newCol] <= time) {
43                    int adjacentIndex = newRow * n + newCol;
44                    parent[find(adjacentIndex)] = find(currentIndex);
45                }
46              
47                // Check if start (0,0) and end (n-1,n-1) are connected
48                if (find(0) == find(n * n - 1)) {
49                    return time;
50                }
51            }
52        }
53      
54        return -1;  // Should never reach here for valid input
55    }
56  
57private:
58    // Find operation with path compression for Union-Find
59    int find(int x) {
60        if (parent[x] != x) {
61            parent[x] = find(parent[x]);  // Path compression
62        }
63        return parent[x];
64    }
65};
66
1/**
2 * Finds the minimum time required to swim from top-left to bottom-right
3 * in a grid where each cell has an elevation value representing the time
4 * when that cell becomes accessible
5 * @param grid - 2D array where grid[i][j] represents the elevation/time value
6 * @returns The minimum time required to reach the destination
7 */
8function swimInWater(grid: number[][]): number {
9    const rows: number = grid.length;
10    const cols: number = grid[0].length;
11  
12    // Track visited cells to avoid revisiting
13    const visited: boolean[][] = Array.from(
14        { length: rows }, 
15        () => new Array(cols).fill(false)
16    );
17  
18    // Track the maximum elevation encountered so far
19    let maxElevation: number = 0;
20  
21    // Priority queue: [row, col, elevation]
22    // Using array as min-heap, sorted by elevation
23    const priorityQueue: number[][] = [[0, 0, grid[0][0]]];
24  
25    // Four directions: right, left, down, up
26    const directions: number[][] = [
27        [0, 1],   // right
28        [0, -1],  // left
29        [1, 0],   // down
30        [-1, 0]   // up
31    ];
32  
33    // Process cells in order of increasing elevation
34    while (priorityQueue.length > 0) {
35        // Get the cell with minimum elevation
36        const currentCell: number[] | undefined = priorityQueue.shift();
37        if (!currentCell) break;
38      
39        const [currentRow, currentCol]: number[] = currentCell;
40      
41        // Update maximum elevation encountered
42        maxElevation = Math.max(grid[currentRow][currentCol], maxElevation);
43      
44        // Check if we reached the destination
45        if (currentRow === rows - 1 && currentCol === cols - 1) {
46            break;
47        }
48      
49        // Explore all four adjacent cells
50        for (const [deltaRow, deltaCol] of directions) {
51            const nextRow: number = currentRow + deltaRow;
52            const nextCol: number = currentCol + deltaCol;
53          
54            // Check if the next cell is valid and unvisited
55            if (nextRow >= 0 && nextRow < rows && 
56                nextCol >= 0 && nextCol < cols && 
57                !visited[nextRow][nextCol]) {
58              
59                // Mark as visited
60                visited[nextRow][nextCol] = true;
61              
62                // Add to priority queue with its elevation
63                priorityQueue.push([nextRow, nextCol, grid[nextRow][nextCol]]);
64            }
65        }
66      
67        // Sort by elevation to maintain min-heap property
68        priorityQueue.sort((a: number[], b: number[]) => a[2] - b[2]);
69    }
70  
71    return maxElevation;
72}
73

Time and Space Complexity

Time Complexity: O(nΒ² Γ— Ξ±(nΒ²)) where n is the dimension of the grid and Ξ± is the inverse Ackermann function.

  • Creating the position mapping array hi takes O(nΒ²) time as we iterate through all grid cells
  • The main loop runs for at most nΒ² iterations (from t = 0 to t = nΒ² - 1)
  • In each iteration, we check 4 neighbors, performing at most 4 union operations
  • Each union operation involves two find operations, which take O(Ξ±(nΒ²)) amortized time with path compression
  • The condition check find(0) == find(n * n - 1) also involves two find operations
  • Total: O(nΒ²) + O(nΒ² Γ— 4 Γ— Ξ±(nΒ²)) = O(nΒ² Γ— Ξ±(nΒ²))

Since Ξ±(nΒ²) grows extremely slowly and is effectively constant for all practical values, the time complexity can be considered O(nΒ²) for practical purposes.

Space Complexity: O(nΒ²)

  • The parent array p stores nΒ² elements: O(nΒ²)
  • The height-to-position mapping array hi stores nΒ² elements: O(nΒ²)
  • The recursion stack for find operations can go up to O(nΒ²) depth in the worst case before path compression, but with path compression it becomes O(log(nΒ²)) = O(log n)
  • Total: O(nΒ²) + O(nΒ²) + O(log n) = O(nΒ²)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Union Operation Without Find

A critical mistake is performing union operations without properly using the find function to get the root representatives:

Incorrect:

# Wrong: directly assigning parents without finding roots
parent[neighbor_idx] = current_cell_idx

Correct:

# Correct: union the roots of both sets
parent[find(neighbor_idx)] = find(current_cell_idx)

Without using find, you're not actually merging the entire connected components but just reassigning individual nodes, which breaks the Union-Find structure.

2. Checking Connectivity at Wrong Time

The connectivity check is placed inside the neighbor loop, which causes premature termination:

Issue: The check happens after each individual union operation rather than after processing all neighbors of the current cell. This could theoretically return an incorrect result if the path requires multiple unions at the same time level.

Solution: Move the connectivity check outside the neighbor loop:

for time in range(n * n):
    current_cell_idx = elevation_to_position[time]
    current_row = current_cell_idx // n
    current_col = current_cell_idx % n
  
    # Process all neighbors first
    for delta_row, delta_col in directions:
        neighbor_row = current_row + delta_row
        neighbor_col = current_col + delta_col
      
        if (0 <= neighbor_row < n and 
            0 <= neighbor_col < n and 
            grid[neighbor_row][neighbor_col] <= time):
            neighbor_idx = neighbor_row * n + neighbor_col
            parent[find(neighbor_idx)] = find(current_cell_idx)
  
    # Then check connectivity once after all unions
    if find(0) == find(n * n - 1):
        return time

3. Confusion Between Elevation Values and Time

The problem states elevations are unique and range from 0 to nΒ²-1, which conveniently matches the time progression. However, if the elevation values were arbitrary (not necessarily 0 to nΒ²-1), the current approach would fail.

More Robust Solution for Arbitrary Elevations:

# Sort all cells by elevation
cells = []
for i in range(n):
    for j in range(n):
        cells.append((grid[i][j], i, j))
cells.sort()

# Process in order of increasing elevation
for elevation, row, col in cells:
    # Union with already-processed neighbors
    for dr, dc in directions:
        nr, nc = row + dr, col + dc
        if (0 <= nr < n and 0 <= nc < n and 
            grid[nr][nc] <= elevation):
            # Union operation
            parent[find(nr * n + nc)] = find(row * n + col)
  
    # Check connectivity
    if find(0) == find(n * n - 1):
        return elevation

4. Missing Path Compression Optimization

While the find function includes path compression, ensure it's consistently applied. Without it, the find operation could degrade to O(n) in worst case, making the overall complexity O(nΒ³).

Always use path compression:

def find(x):
    if parent[x] != x:
        parent[x] = find(parent[x])  # Compress path
    return parent[x]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the relationship between a tree and a graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More