Swim in Rising Water looks like a water-physics puzzle, but it is really a shortest-path problem in disguise. The twist: instead of adding up costs along a path, we take the maximum — and that one change turns Dijkstra into the perfect tool.
Problem. You are given an n x n grid where grid[r][c] is the elevation of that cell. At time
t, water covers every cell with elevation ≤ t, and you can swim between two adjacent cells once both
are underwater. Starting at the top-left, return the least time t at which you can reach the
bottom-right.
Example: grid = [[0, 2, 1], [1, 3, 2], [4, 5, 0]] → answer 2. The path 0 → 2 → 1 → 2 → 0 only ever
touches elevation 2, so by time 2 you can swim the whole way.
The slow way first
You could binary-search the answer: pick a time t, then run a flood-fill to ask "can I reach the goal using only cells with elevation ≤ t?" That works and is a fine answer, but it reruns a full search for every candidate t.
The cleaner question: along any path, what number actually matters? Not the sum of elevations — only the single highest cell you are forced to cross. So we want the path that minimizes its maximum cell. That is exactly the shape Dijkstra solves, if we redefine "path cost" as max instead of sum.
The idea: Dijkstra with a max instead of a sum
Run Dijkstra from the top-left. Keep a min-heap of frontier cells, each keyed by the largest elevation on the best path that reaches it so far. Pop the smallest such key, mark the cell visited, and push its unvisited neighbors with an updated running max. The first time we pop the bottom-right, its key is the answer.
Why does popping the goal end it? A min-heap always hands back the smallest key first, so the moment the goal surfaces, no cheaper path to it can still exist. In the example, the goal pops at max 2 while the elevation-4 cell is still sitting unused in the heap.
Walk through it
Step through the animation. Cells turn visited as they pop off the heap. Watch the running max in the side panel: it climbs to 2 and then stays there, because every cell on the winning path is ≤ 2. The expensive 4 cell never gets visited at all — Dijkstra reaches the goal before it ever becomes the cheapest option.
Pseudocode
heap = [(grid[0][0], 0, 0)] # (max elevation so far, row, col)
seen = {(0, 0)}
while heap is not empty:
t, r, c = pop smallest from heap
if (r, c) is the bottom-right cell:
return t # answer = max cell on the best path
for each in-bounds neighbor (nr, nc) not in seen:
add (nr, nc) to seen
push (max(t, grid[nr][nc]), nr, nc) onto the heapThe Python solution
def swim_in_water(grid):
n = len(grid)
pq = [(grid[0][0], 0, 0)] # (max so far, r, c)
seen = {(0, 0)}
while pq:
t, r, c = heapq.heappop(pq)
if (r, c) == (n - 1, n - 1):
return t
for dr, dc in ((0, 1), (1, 0), (0, -1), (-1, 0)):
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and (nr, nc) not in seen:
seen.add((nr, nc))
heapq.heappush(pq, (max(t, grid[nr][nc]), nr, nc))pqis a min-heap of(max-so-far, row, col). Python orders tuples by their first element, so the smallest running max always pops first.- We seed it with the start cell and add
(0, 0)toseenimmediately, so we never push it again. - Lines 6-8 are the heart: pop the cheapest frontier cell, and if it is the goal, that running max is the answer.
- For each in-bounds, unvisited neighbor we push
max(t, grid[nr][nc])— the path cost can only rise to clear a taller cell, never fall. - Marking a cell
seenat push time keeps each cell on the heap once, which is what makes the loop terminate.
Complexity
| Case | Time | Notes |
|---|---|---|
| Binary search + flood fill | O(n² log n) (moderate) | a full search per candidate time |
| Dijkstra (this solution) | O(n² log n) (moderate) | each of n² cells pushed once |
O(n²) (slow)There are n² cells; each is pushed and popped at most once, and every heap operation is O(log n²) = O(log n). The seen set and heap together use O(n²) space. Same big-O as binary search, but a single clean pass instead of repeated searches.
When this pattern shows up
When a grid or graph problem asks for the path that minimizes the maximum edge or cell (a
"minimax path" or "bottleneck shortest path"), reach for Dijkstra with one change: relax with
max(cost_so_far, weight) instead of a sum. "Path With Minimum Effort" and "Minimum Cost to Connect
Points" are close cousins.
Do not push the same cell with multiple keys and hope to fix it later. Mark a cell seen the moment you
push it, so each cell enters the heap exactly once — otherwise the heap bloats and the loop can revisit
cells endlessly.
Practice
In the example grid, the cell with elevation 4 sits in the heap the whole time. Does the algorithm ever visit it, and why?
1. What key does the min-heap order frontier cells by?
2. When does the algorithm return the answer?
3. How does the running max update when moving to a neighbor of elevation e?
4. Why mark a cell as seen at the moment you push it?