Problem Walkthrough

Swim in Rising Water LeetCode 778 Guide

Solve LeetCode 778 Swim in Rising Water by reframing it as a minimax path problem, then applying a Dijkstra-like min-heap that always expands the lowest-elevation frontier cell, or using binary search combined with BFS to check path feasibility at each candidate time value.

9 min read|

Swim in Rising Water

LeetCode 778

Problem Overview — Grid, Water Level, and Goal

LeetCode 778 Swim in Rising Water presents an n x n grid where grid[i][j] is the elevation at cell (i, j). All elevations are a permutation of 0 to n*n-1, so every cell has a unique height. At time t, the water level rises to t: you can swim from one cell to an adjacent cell (up, down, left, right) if and only if both cells have elevation at most t. You start at (0, 0) and want to reach (n-1, n-1).

The goal is to find the minimum time t such that there exists a path from (0, 0) to (n-1, n-1) where every cell along the path has elevation at most t. Because water must rise to cover both the cell you are leaving and the cell you are entering, the bottleneck of any path is the maximum elevation along it. You want to choose the path that minimizes this maximum elevation.

The answer is always at least max(grid[0][0], grid[n-1][n-1]) because you must wait until water covers both the start and the destination before you can swim at all. For a 1x1 grid the answer is grid[0][0]. For larger grids the answer is determined by the most constrained cell on the optimal route through the grid.

  • Given: n x n grid where grid[i][j] is elevation, all values are a permutation of [0, n*n-1]
  • At time t: water rises to level t; you can swim between adjacent cells only if both have elevation <= t
  • Start at (0, 0), reach (n-1, n-1)
  • Return: minimum t such that a valid swimming path exists
  • Constraints: 1 <= n <= 50, grid[i][j] is a permutation of [0, n*n-1]
  • Answer >= max(grid[0][0], grid[n-1][n-1]) since you must wait for both endpoints

Reframing as a Minimax Path Problem

The key insight is that the cost of using a path is determined not by the sum of elevations along it but by the maximum elevation among all cells on the path. You want the path from (0,0) to (n-1,n-1) that minimizes this maximum. This is called the minimax path problem: minimize the maximum edge (or node) weight along a path between two vertices in a graph.

Translating into graph terms: model each grid cell as a node, and each adjacency as an edge whose weight is the maximum of the two endpoint elevations. Then the answer is the minimum over all paths of the maximum edge weight on that path. Equivalently, think of each node having a cost equal to its elevation, and the path cost is the maximum node cost seen. Both formulations are equivalent and solved by the same algorithms.

Once you recognize the minimax path structure, two standard algorithms apply: a modified Dijkstra using a min-heap where the priority of a state is the maximum elevation seen so far (not a sum), and a binary search on the answer combined with BFS or DFS to check feasibility. Both run efficiently on an n x n grid.

💡

This is NOT Shortest Path by Sum

The standard Dijkstra algorithm minimizes the sum of edge weights along a path. Here you must minimize the maximum edge (or node) weight — a fundamentally different objective. Replacing the sum relaxation with a max relaxation in Dijkstra still produces a correct greedy algorithm because the greedy substructure holds for minimax paths just as it does for minimum-sum paths. Do not accidentally sum the elevations; track the running maximum.

Dijkstra-Like Min-Heap Approach

The Dijkstra-like approach uses a min-heap (priority queue) where each entry is (cost, row, col) and cost represents the maximum elevation encountered so far on the path to (row, col). The algorithm starts by pushing (grid[0][0], 0, 0) onto the heap and marking (0, 0) as visited. It then repeatedly pops the minimum-cost entry, and if that entry is (n-1, n-1), returns the cost as the answer.

For each popped cell (cost, r, c), the algorithm explores all four neighbors. For each unvisited neighbor (nr, nc), the new cost to reach it is max(cost, grid[nr][nc]): either the current running maximum or the neighbor's elevation, whichever is larger. This new cost is pushed onto the heap. The visited set ensures each cell is processed at most once, and because the heap always pops the minimum-cost entry next, the first time (n-1, n-1) is popped its cost is guaranteed to be optimal.

Time complexity is O(n^2 log n) because there are n^2 cells each inserted into the heap at most once, and each heap operation costs O(log(n^2)) = O(log n). Space complexity is O(n^2) for the heap and visited set. This is typically faster in practice than binary search plus BFS because it terminates as soon as the destination is reached rather than running full BFS passes for each candidate.

  1. 1Initialize min-heap with (grid[0][0], 0, 0) — starting cell with its own elevation as initial cost
  2. 2Initialize visited set, mark (0, 0) as visited
  3. 3While heap is not empty: pop (cost, r, c) — the minimum-cost frontier cell
  4. 4If (r, c) == (n-1, n-1): return cost — first time destination is reached is optimal
  5. 5For each neighbor (nr, nc) in 4 directions: skip if out of bounds or visited
  6. 6new_cost = max(cost, grid[nr][nc]) — extend running max to include neighbor elevation
  7. 7Push (new_cost, nr, nc) onto heap; mark (nr, nc) as visited
  8. 8Return -1 if heap exhausted (unreachable, though guaranteed reachable by problem constraints)

Why the Greedy Min-Heap Works Here

Standard Dijkstra works because the minimum-sum path obeys optimal substructure and the greedy choice of always expanding the cheapest known path is never regretted — a shorter prefix cannot be found later since all edge weights are non-negative. The same reasoning applies here when "cost" is the running maximum instead of a running sum.

Formally, when the heap pops (cost, r, c), it means no other path to (r, c) in the heap has a lower maximum elevation. This is because if there were a better path to (r, c) with a lower maximum, it would have been pushed with a smaller priority and would have been popped first. Once a cell is popped, its optimal cost is finalized — exactly like Dijkstra with non-negative weights.

The max operation satisfies the same monotonicity property as addition for Dijkstra: extending a path can only keep the cost the same or increase it (since max(x, y) >= x for any y >= 0). This monotonicity is the key property that allows greedy expansion by minimum current cost to always yield correct results for the entire path.

ℹ️

Replacing Sum with Max Still Satisfies Greedy Substructure

Dijkstra's algorithm requires that relaxation be monotone: the cost of a path can never decrease by extending it. For sum-of-weights, adding a non-negative edge weight never decreases the sum. For max-of-weights, taking the max with a non-negative elevation never decreases the running maximum. Both operations are monotone, so both support the greedy invariant that popping the minimum-priority cell from the heap finalizes its optimal cost.

Binary Search + BFS Alternative

The binary search approach exploits the monotone structure of the problem: if a valid path exists at time t, it also exists at all times t' > t. This monotonicity means we can binary search on the answer in the range [0, n*n - 1]. For each candidate time mid, run a BFS or DFS from (0, 0) using only cells with elevation <= mid, and check whether (n-1, n-1) is reachable.

If the BFS succeeds (destination reachable), the answer is at most mid, so search the lower half. If BFS fails, the answer is greater than mid, so search the upper half. After O(log(n^2)) = O(log n) iterations of binary search, each costing O(n^2) for the BFS, total time is O(n^2 log n). This matches the Dijkstra approach asymptotically but has a larger constant due to running multiple full BFS passes.

The binary search approach is conceptually simpler and easier to verify by inspection: the BFS is completely standard, and the binary search predicate is a clean "can we reach destination using only cells with elevation <= t?" predicate. It is a good choice when clarity is prioritized. The Dijkstra approach is more elegant and typically faster because it terminates early once the destination is reached.

  1. 1Set lo = max(grid[0][0], grid[n-1][n-1]), hi = n*n - 1
  2. 2While lo < hi: mid = (lo + hi) // 2
  3. 3Run BFS/DFS from (0, 0) using only cells where grid[r][c] <= mid
  4. 4If (n-1, n-1) is reachable: hi = mid (answer is at most mid)
  5. 5Else: lo = mid + 1 (answer must be greater)
  6. 6Return lo — the minimum time where a valid path exists

Code Walkthrough — Python and Java

In Python, the Dijkstra min-heap solution uses heapq. Initialize heap = [(grid[0][0], 0, 0)] and visited = set(). In the main loop, heappop gives the minimum-cost entry. Push neighbors with heappush(heap, (max(cost, grid[nr][nc]), nr, nc)). The code is around 15 lines and handles all edge cases automatically. For the binary search version, use collections.deque for BFS; the predicate function is a 10-line BFS, and the binary search loop adds 8 more lines.

In Java, use PriorityQueue<int[]> with a comparator on the first element for the min-heap. Initialize with new int[]{grid[0][0], 0, 0}. Use a boolean[][] visited array. The four-direction exploration uses a standard int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}} array. Java's PriorityQueue.poll() returns the minimum-priority element. The solution is about 25 lines and runs efficiently within the n <= 50 constraints with room to spare.

A common mistake is using grid[nr][nc] alone as the heap priority instead of max(current_cost, grid[nr][nc]). If you only push the neighbor's raw elevation, you lose track of the running maximum along the path and will incorrectly compute the answer for paths that pass through high-elevation cells before descending. The heap entry must always represent the maximum elevation seen on the entire path from (0,0) to the current cell.

⚠️

Don't Forget the Starting Cell's Elevation

The starting cell (0,0) has elevation grid[0][0], which may be greater than zero. The initial heap entry must be (grid[0][0], 0, 0) — not (0, 0, 0). Similarly, the answer is at least grid[0][0] because you must wait until water rises to cover the starting cell. A common off-by-one error initializes cost to 0 and only updates it when moving to neighbors, which produces a wrong answer when grid[0][0] is the maximum elevation on the optimal path.

Ready to master algorithm patterns?

YeetCode flashcards help you build pattern recognition through active recall and spaced repetition.

Start practicing now