Swim in Rising Water

Hard Heaps

Problem

Given an N x N grid of integers representing elevation heights, return the minimum time required to swim from the top-left corner to the bottom-right corner, where you can only swim to adjacent squares with elevation less than or equal to the current time.

  • 2 ≤ N ≤ 50
  • grid.length == N
  • grid[i].length == N
  • 0 ≤ grid[i][j] < N*N
  • All values in grid are unique

Example

Input: grid = [[0,2],[1,3]]
Output: 3

At time 0, you can only stand on grid[0][0] with elevation 0. You cannot move to grid[0][1] or grid[1][0] because their elevations (2 and 1) are higher than 0. As time increases, the water level rises. At time 1, you can move to grid[1][0] (elevation 1). At time 2, grid[0][1] becomes accessible. Finally, at time 3, grid[1][1] is accessible, allowing you to reach the bottom-right corner. The minimum time to reach the destination is 3.

Approach

Straightforward Solution

A brute-force approach would try all paths from start to end, tracking the maximum elevation on each path, which is exponential and infeasible for larger grids.

Core Observation

The minimum time to reach the destination is governed by the highest elevation encountered along any path from start to end. This means the problem reduces to finding a path minimizing the maximum elevation on that path.

Path to Optimal

Recognizing that the problem is to minimize the maximum elevation along a path suggests using a best-first search approach. By using a min-heap (priority queue) keyed on elevation, the algorithm always explores the next reachable cell with the lowest elevation, effectively simulating the water rising and expanding the reachable area. This approach guarantees that the first time the destination is reached, the path used has the minimal possible maximum elevation.

Optimal Approach

Initialize a min-heap with the starting cell and its elevation. Maintain a visited set to avoid revisiting cells. Repeatedly pop the cell with the lowest elevation from the heap, update the current maximum elevation along the path, and push all unvisited neighbors into the heap with their elevations. When the destination cell is popped, return the maximum elevation encountered, which corresponds to the minimum time needed to swim across.

Time

O(N^2 log N)

Each cell is pushed and popped from the min-heap at most once. The heap operations take O(log (N^2)) = O(2 log N) = O(log N) time. Since there are N^2 cells, total complexity is O(N^2 log N).

Space

O(N^2)

The visited set and min-heap can each hold up to all N^2 cells in the worst case, resulting in O(N^2) auxiliary space.

Pattern Spotlight

Heap / Priority Queue (Best-First Search on Grid)

When a problem requires minimizing the maximum cost along a path in a grid or graph, use a priority queue to always expand the next node with the lowest cost, ensuring the first time the target is reached corresponds to the optimal path.

Solution

1import heapq
2
3class Solution:
4 def swimInWater(self, grid: list[list[int]]) -> int:
5 N = len(grid)
6 visit = set()
7 min_heap = [[grid[0][0], 0, 0]]
8 directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]
9 visit.add((0, 0))
10
11 while min_heap:
12 t, r, c = heapq.heappop(min_heap)
13
14 if r == N - 1 and c == N - 1:
15 return t
16
17 for dr, dc in directions:
18 nei_r, nei_c = r + dr, c + dc
19 if (nei_r < 0 or nei_c < 0 or
20 nei_r == N or nei_c == N or
21 (nei_r, nei_c) in visit):
22 continue
23
24 visit.add((nei_r, nei_c))
25 new_time = max(t, grid[nei_r][nei_c])
26 heapq.heappush(min_heap, [new_time, nei_r, nei_c])

Step-by-Step Solution

1

Initialize Grid Size, Visited Set, and Min-Heap with Starting Cell

5N = len(grid)
6visit = set()
7min_heap = [[grid[0][0], 0, 0]]
8directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]
9visit.add((0, 0))

Objective

To set up the initial state for the best-first search, including grid dimensions, tracking visited cells, and the priority queue seeded with the starting position.

Key Insight

Starting with the top-left cell in the min-heap keyed by its elevation allows the algorithm to begin exploring from the lowest elevation point. The visited set prevents revisiting cells, ensuring efficiency and correctness by avoiding cycles and redundant work.

Interview Quick-Check

Core Logic

The min-heap stores cells prioritized by their elevation, enabling the algorithm to always explore the next lowest elevation cell.

State & Boundaries

The visited set ensures each cell is processed only once, preventing infinite loops and redundant heap insertions.

Common Pitfalls & Bugs

Forgetting to add the starting cell to the visited set can cause it to be processed multiple times.

2

Expand Reachable Cells by Popping Lowest Elevation and Adding Neighbors

11while min_heap:
12 t, r, c = heapq.heappop(min_heap)
14 if r == N - 1 and c == N - 1:
15 return t
17 for dr, dc in directions:
18 nei_r, nei_c = r + dr, c + dc
19 if (nei_r < 0 or nei_c < 0 or
20 nei_r == N or nei_c == N or
21 (nei_r, nei_c) in visit):
22 continue
24 visit.add((nei_r, nei_c))
25 new_time = max(t, grid[nei_r][nei_c])
26 heapq.heappush(min_heap, [new_time, nei_r, nei_c])

Objective

To iteratively explore the grid by always moving to the adjacent cell with the lowest elevation not yet visited, updating the maximum elevation along the path.

Key Insight

By always choosing the next cell with the lowest elevation, the algorithm simulates the water rising and expanding the reachable area in order of increasing elevation. The maximum elevation encountered so far is tracked implicitly by taking the max between the current path elevation and the neighbor's elevation. This ensures that when the destination is reached, the maximum elevation along that path is minimal.

Interview Quick-Check

Core Logic

The max operation on elevations ensures the path's maximum elevation is correctly tracked as the water level rises.

State & Boundaries

Boundary checks prevent out-of-bounds access, and the visited set prevents revisiting cells.

Common Pitfalls & Bugs

Adding neighbors to the heap before marking them visited can cause duplicates; marking visited immediately upon adding prevents this.

Complexity

Each cell is pushed into the heap once, and heap operations cost O(log N^2) = O(log N), ensuring efficient traversal.

Line Analysis

This solution has 6 Critical lines interviewers watch for.

Line 12 Critical
t, r, c = heapq.heappop(min_heap)

Pop the cell with the lowest elevation from the min-heap.

This operation selects the next cell to explore with the minimal elevation, which is critical to the best-first search strategy that guarantees minimal maximum elevation path.

Line 7 Critical
min_heap = [[grid[0][0], 0, 0]]

Initialize a min-heap with the starting cell's elevation and coordinates.

The min-heap prioritizes exploration by elevation, enabling the algorithm to always expand the lowest elevation cell next, which is essential for finding the minimal maximum elevation path.

Line 14 Critical
if r == N - 1 and c == N - 1:

Check if the current cell is the destination.

Detecting the destination allows immediate termination, as the first time it is popped corresponds to the minimal maximum elevation path.

Line 19 Critical
if (nei_r < 0 or nei_c < 0 or

Reject invalid neighbor coordinates and skip cells that have already been visited.

This guard clause filters out neighbors that are either outside the grid bounds or already processed. Without it, you risk index errors (out of bounds) and redundant work or cycles (revisiting), which can break correctness or blow up runtime.

Line 20 Critical
nei_r == N or nei_c == N or

Enforce grid boundary constraints on the neighbor row and column.

Negative indices and indices equal to N are outside the valid range [0, N-1]. Explicitly checking these boundary conditions prevents illegal grid access and ensures the search only considers real cells.

Line 25 Critical
new_time = max(t, grid[nei_r][nei_c])

Calculate the new time as the maximum of current time and neighbor's elevation.

This step tracks the maximum elevation along the path, which corresponds to the minimal time needed to reach that cell given rising water levels.

Line 6 Core
visit = set()

Initialize a set to track visited cells.

The visited set prevents revisiting cells, which avoids infinite loops and redundant processing, ensuring each cell is processed once.

Line 15 Core
return t

Return the elevation value as the minimum time to swim across.

Returning the elevation at the destination cell represents the minimal time needed, since it is the highest elevation encountered on the optimal path.

Line 21 Core
(nei_r, nei_c) in visit):

Skip neighbors that are already present in the visited set.

The visited set encodes which cells have already been committed to exploration so they are not re-added and re-expanded. This preserves efficiency and prevents repeated processing of the same coordinate.

Line 24 Core
visit.add((nei_r, nei_c))

Mark the neighbor cell as visited.

Marking visited immediately upon adding to the heap prevents duplicate entries and ensures each cell is processed once.

Line 26 Core
heapq.heappush(min_heap, [new_time, nei_r, nei_c])

Push the neighbor cell with its updated time into the min-heap.

Adding the neighbor to the heap with its effective elevation ensures it will be explored in the correct order, maintaining the best-first search invariant.

Line 9 Core
visit.add((0, 0))

Mark the starting cell as visited.

Marking the start cell visited immediately prevents it from being added multiple times to the heap, ensuring correctness and efficiency.

Line 22 Core
continue

Continue to the next direction without processing this neighbor.

Immediately continuing avoids executing the downstream logic (marking visited, computing time, pushing to heap) for an invalid or already-seen neighbor. This keeps the algorithm correct and the loop body clean.

Line 11 Standard
while min_heap:

Continue processing while there are cells in the min-heap.

The loop ensures the algorithm explores all reachable cells in order of increasing elevation until the destination is found.

Line 8 Standard
directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]

Define the four possible movement directions (up, down, left, right).

These directions represent all valid adjacent moves, enabling systematic exploration of neighbors.

Line 17 Standard
for dr, dc in directions:

Iterate over all four possible neighbor directions.

Exploring neighbors systematically ensures all reachable cells are considered for expansion.

Line 18 Standard
nei_r, nei_c = r + dr, c + dc

Calculate the neighbor's row and column indices.

Computing neighbor coordinates is necessary for boundary and visited checks before adding to the heap.

Line 5 Basic
N = len(grid)

Store the size of the grid in variable N.

Caching the grid size avoids repeated calls to len(grid) and simplifies boundary checks throughout the algorithm.

Test Your Understanding

Why does popping the destination cell from the min-heap guarantee that the path found has the minimal maximum elevation?

Because the min-heap always expands the cell with the lowest elevation next, the first time the destination is popped corresponds to the path with the smallest possible maximum elevation. Any other path with a lower maximum elevation would have been explored earlier.

Related Problems

Heaps pattern

Don't just read it. Drill it.

Reconstruct Swim in Rising Water from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Swim in Rising Water drill

or drill a free problem