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.
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:
- The cell at that elevation becomes "active" (submerged)
- We check its 4 neighbors - if they're already submerged (elevation β€ current time), we unite them into the same connected component
- 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, wherep[i]
stores the parent of nodei
. 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:
-
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 indexi * n + j
- Create parent array
-
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.
-
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 elevationt
becomes active. We extract its 2D coordinates from the flattened index. -
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])
- Check if it's within bounds and already submerged (
- For each of the 4 adjacent cells
-
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
- After each union operation, check if
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 EvaluatorExample 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
takesO(nΒ²)
time as we iterate through all grid cells - The main loop runs for at most
nΒ²
iterations (fromt = 0
tot = nΒ² - 1
) - In each iteration, we check 4 neighbors, performing at most 4 union operations
- Each union operation involves two
find
operations, which takeO(Ξ±(nΒ²))
amortized time with path compression - The condition check
find(0) == find(n * n - 1)
also involves twofind
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
storesnΒ²
elements:O(nΒ²)
- The height-to-position mapping array
hi
storesnΒ²
elements:O(nΒ²)
- The recursion stack for
find
operations can go up toO(nΒ²)
depth in the worst case before path compression, but with path compression it becomesO(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]
What's the relationship between a tree and a graph?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!