Swim in Rising Water

Picture a square pool. Water rises over time. Each cell has a height. You can only move into a cell once the water level reaches its height. You want to swim from the top-left corner to the bottom-right corner. The question is the earliest time you can arrive. This looks like a maze, but it is really a clever shortest path problem.

🎯 The Problem

Water rises over a grid of heights, and you want the earliest time you can swim across.

  • The grid is n by n. Each cell holds a number, its elevation, which is the height of that cell.
  • At time t, the water level is t. You can stand on any cell whose elevation is at most t.
  • You start at the top-left cell (0, 0) and want to reach the bottom-right cell.
  • You may move up, down, left, or right. Swimming between cells takes no time.
  • The only limit is the water level. Return the least time when a path from start to end exists.
Input: grid = [[0,2],[1,3]]
Output: 3
Explanation: At time 3 the water covers every cell on the path.
You go (0,0) -> (0,1) -> (1,1), and the highest cell on it is 3.

The cost of a path is the highest elevation along it. We want the path whose highest cell is as small as possible. That is called a minimax path. Minimax means we minimize the maximum cell we must cross. Here is the grid as a small graph of moves.

(0,0)=0

(0,1)=2

(1,0)=1

(1,1)=3

🐢 Approach 1: Binary Search Plus BFS (Brute Force)

The idea in one line: guess a water level, test if you can cross at that level, then narrow the guess.

The idea:

  • Binary search means guess a value, check if it works, then cut the range in half.
  • Guess a water level t. The answer sits between the smallest and largest elevation.
  • For each guess, run a breadth-first search. BFS explores the grid level by level from the start.

How it works:

  • The BFS only steps into cells whose elevation is at most t.
  • If BFS reaches the end, then t is enough, so try a smaller t.
  • If not, try a bigger t.

Why it is weak:

  • It runs a fresh BFS for every guess.
  • The same cells get explored again and again across guesses.
  • One pass can do the same work.

Here is the binary-search-plus-BFS code:

swim_rising_water_binary_bfs.py
from collections import deque
def swim_in_water(grid):
n = len(grid)
def can(t):
if grid[0][0] > t: return False
q, seen = deque([(0, 0)]), {(0, 0)}
while q:
r, c = q.popleft()
if r == n - 1 and c == n - 1: return True
for nr, nc in ((r+1,c),(r-1,c),(r,c+1),(r,c-1)):
if 0 <= nr < n and 0 <= nc < n and (nr, nc) not in seen and grid[nr][nc] <= t:
seen.add((nr, nc)); q.append((nr, nc))
return False
lo, hi = grid[0][0], n * n
while lo < hi:
mid = (lo + hi) // 2
if can(mid): hi = mid
else: lo = mid + 1
return lo

⚡ Approach 2: Dijkstra-Style Search (Best)

The idea in one line: always step to the reachable cell whose worst crossing so far is smallest, until you reach the end.

The idea:

  • This is shaped like Dijkstra’s algorithm, which normally finds the path with the smallest total cost.
  • Change the cost rule. Instead of adding costs along the path, take the maximum cell on the path.
  • So the cost of reaching a cell is the highest elevation you had to cross to get there.

How it works:

  • Use a min-heap that always hands you the cell with the smallest such cost.
  • Push the top-left cell with its own elevation as the cost.
  • Pull the cell with the smallest cost. That cost is the earliest time to reach it.
  • For each neighbor, its cost is the larger of the current cost and that neighbor’s elevation.
  • Push each neighbor with that new cost. Mark cells visited so none is processed twice.
  • The first time you pull the bottom-right cell, its cost is the earliest arrival time.

Why it is fast:

  • One sweep, no repeated BFS runs.
  • Each cell is settled once, and each heap step costs about log of the cell count.

Here is the order the heap pulls cells on the example. Each label shows the reach cost, the highest cell crossed so far.

Pull (0,0), cost 0

Pull (1,0), cost 1

Pull (0,1), cost 2

Pull (1,1), cost 3, end reached

Steps to Solve

  1. Make a min-heap of (cost, row, col). Push the start with its own elevation as the cost.
  2. Keep a visited grid so each cell is processed once.
  3. Pull the cell with the smallest cost. If it is the end cell, return its cost.
  4. Mark it visited.
  5. For each of the four neighbors not visited, the new cost is the larger of the current cost and that neighbor’s elevation.
  6. Push each neighbor with its new cost. Repeat until the end is pulled.

This Python version uses heapq as the min-heap and stops the moment it pulls the bottom-right cell.

swim.py
import heapq
def swim_in_water(grid):
n = len(grid)
visited = [[False] * n for _ in range(n)]
heap = [(grid[0][0], 0, 0)] # (cost, row, col)
while heap:
cost, r, c = heapq.heappop(heap) # smallest reach cost
if visited[r][c]:
continue
visited[r][c] = True
if r == n - 1 and c == n - 1:
return cost # reached the end
for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)):
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and not visited[nr][nc]:
next_cost = max(cost, grid[nr][nc]) # highest cell so far
heapq.heappush(heap, (next_cost, nr, nc))
return -1
grid = [[0, 2], [1, 3]]
print(swim_in_water(grid))

The output of the above code will be:

3

Let us read the Python version line by line and see the reason behind each part.

visited = [[False] * n for _ in range(n)]
heap = [(grid[0][0], 0, 0)]

visited stops us from processing the same cell twice. The heap starts with the top-left cell. Its cost is its own elevation, because we must wait for the water to reach the start before we can stand on it. We store (cost, row, col) so the heap orders by cost first.

cost, r, c = heapq.heappop(heap)
if visited[r][c]:
continue
visited[r][c] = True

We pull the cell with the smallest reach cost. If we already settled it, we skip this stale copy. Otherwise this cost is the earliest time we can be on this cell. That is true because the heap always hands us the cheapest reachable cell next, just like Dijkstra’s.

if r == n - 1 and c == n - 1:
return cost

The moment we pull the bottom-right cell, its cost is the earliest arrival time for the whole grid. So we return it right away.

next_cost = max(cost, grid[nr][nc])
heapq.heappush(heap, (next_cost, nr, nc))

This is the minimax twist. To reach the neighbor, the water must cover both the path so far and the neighbor’s own elevation. So we take the larger of the two with max. We do not add. We push the neighbor with this new cost so the heap can order it correctly.

⏱️ Time and Space Complexity

The grid has cells. The Dijkstra-style search pushes each cell a constant number of times and each heap operation costs about log of the number of cells. So the total time is O(n² log n). The space holds the visited grid and the heap, which is O(n²). The binary search plus BFS approach is also good, at O(n² log n), but it repeats a full BFS for every guess.

Approach Time Complexity Space Complexity
Binary search plus BFS O(n² log n) O(n²)
Dijkstra-style min-heap O(n² log n) O(n²)

Tip

The key change from normal Dijkstra’s is one word: replace the plus with a max. Whenever a problem asks for the path whose worst step is as small as possible, this minimax trick applies.

🧩 Key Takeaways

  • ✅ The cost of a path is its highest cell, so we want the minimax path.
  • ✅ A Dijkstra-style min-heap finds it by always pulling the cell with the smallest reach cost.
  • ✅ The reach cost of a neighbor is the max of the current cost and the neighbor’s elevation.
  • ✅ The first time you pull the end cell, its cost is the earliest arrival time.
  • ✅ It runs in O(n² log n) time, the same as binary search plus BFS but in one pass.

Check Your Knowledge

4 questions Show quiz Hide quiz

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What is the cost of a path in Swim in Rising Water?

    Why: You can only cross a cell once the water reaches its height, so the path cost is its highest cell.

  2. 2

    How does the Dijkstra-style version differ from normal Dijkstra's?

    Why: Normal Dijkstra's adds edge costs. Here we take the maximum, which is the minimax rule.

  3. 3

    When does the search return the answer?

    Why: The first time the end cell is popped, its stored cost is the earliest arrival time.

  4. 4

    What is the time complexity of the min-heap approach?

    Why: There are n² cells and each heap operation costs about log n, giving O(n² log n).

🚀 What’s Next?