292 lines
13 KiB
YAML
292 lines
13 KiB
YAML
title: Swim in Rising Water
|
||
slug: swim-in-rising-water
|
||
difficulty: hard
|
||
leetcode_id: 778
|
||
leetcode_url: https://leetcode.com/problems/swim-in-rising-water/
|
||
categories:
|
||
- graphs
|
||
- binary-search
|
||
- heap
|
||
patterns:
|
||
- binary-search
|
||
- bfs
|
||
- heap
|
||
|
||
function_signature: "def swim_in_water(grid: list[list[int]]) -> int:"
|
||
|
||
test_cases:
|
||
visible:
|
||
- input: { grid: [[0, 2], [1, 3]] }
|
||
expected: 3
|
||
- input: { grid: [[0, 1, 2, 3, 4], [24, 23, 22, 21, 5], [12, 13, 14, 15, 16], [11, 17, 18, 19, 20], [10, 9, 8, 7, 6]] }
|
||
expected: 16
|
||
hidden:
|
||
- input: { grid: [[0]] }
|
||
expected: 0
|
||
- input: { grid: [[3, 2], [0, 1]] }
|
||
expected: 3
|
||
- input: { grid: [[0, 1, 2], [5, 4, 3], [6, 7, 8]] }
|
||
expected: 4
|
||
- input: { grid: [[7, 5, 3], [2, 0, 6], [1, 4, 8]] }
|
||
expected: 7
|
||
|
||
description: |
|
||
You are given an `n x n` integer matrix `grid` where each value `grid[i][j]` represents the elevation at that point `(i, j)`.
|
||
|
||
It starts raining, and water gradually rises over time. At time `t`, the water level is `t`, meaning **any** cell with elevation less than or equal to `t` is submerged or reachable.
|
||
|
||
You can swim from a square to another 4-directionally adjacent square if and only if the elevation of both squares individually are at most `t`. You can swim infinite distances in zero time. Of course, you must stay within the boundaries of the grid during your swim.
|
||
|
||
Return *the minimum time until you can reach the bottom right square* `(n - 1, n - 1)` *if you start at the top left square* `(0, 0)`.
|
||
|
||
constraints: |
|
||
- `n == grid.length`
|
||
- `n == grid[i].length`
|
||
- `1 <= n <= 50`
|
||
- `0 <= grid[i][j] < n^2`
|
||
- Each value `grid[i][j]` is **unique**
|
||
|
||
examples:
|
||
- input: "grid = [[0,2],[1,3]]"
|
||
output: "3"
|
||
explanation: "At time 0, you are at (0, 0). You cannot move anywhere because all adjacent cells have elevation > 0. At time 3, the water level allows you to swim through all cells to reach (1, 1)."
|
||
- input: "grid = [[0,1,2,3,4],[24,23,22,21,5],[12,13,14,15,16],[11,17,18,19,20],[10,9,8,7,6]]"
|
||
output: "16"
|
||
explanation: "The optimal path follows the outer edge of the grid. We need to wait until time 16 so that (0, 0) and (4, 4) are connected through cells with elevations ≤ 16."
|
||
|
||
explanation:
|
||
intuition: |
|
||
Imagine you're standing on a terrain map where each cell has a different elevation. Rain is falling steadily, and the water level rises by 1 unit each second. At time `t`, any cell with elevation ≤ `t` becomes a "lake" you can swim through.
|
||
|
||
The key insight is that this is a **path optimisation problem** where we want to minimise the **maximum elevation** along our path from start to end. We don't care about the sum of elevations or the number of steps — we only care about the single highest point we must traverse.
|
||
|
||
Think of it like this: if you find a path where the highest elevation is `16`, then at time `t = 16`, every cell on that path is underwater and you can swim the entire route. The answer is the **minimum possible maximum elevation** across all valid paths.
|
||
|
||
This problem can be solved in multiple ways:
|
||
- **Min-Heap (Dijkstra-like)**: Greedily expand to the lowest-elevation neighbour, tracking the maximum elevation seen
|
||
- **Binary Search + BFS/DFS**: Binary search on the answer `t`, and check if a path exists using only cells with elevation ≤ `t`
|
||
- **Union-Find**: Sort cells by elevation and union them until start and end are connected
|
||
|
||
approach: |
|
||
We'll use a **Min-Heap (Priority Queue)** approach, similar to Dijkstra's algorithm but optimised for this problem:
|
||
|
||
**Step 1: Initialise the data structures**
|
||
|
||
- `heap`: A min-heap storing `(elevation, row, col)` tuples, starting with `(grid[0][0], 0, 0)`
|
||
- `visited`: A set to track cells we've already processed
|
||
- `result`: Track the maximum elevation encountered so far (initialised to `grid[0][0]`)
|
||
|
||
|
||
|
||
**Step 2: Process cells in order of elevation**
|
||
|
||
- Pop the cell with the **smallest elevation** from the heap
|
||
- Update `result` to be the maximum of current result and this cell's elevation
|
||
- If we've reached `(n-1, n-1)`, return `result` — this is our answer
|
||
|
||
|
||
|
||
**Step 3: Explore neighbours**
|
||
|
||
- For each of the 4 adjacent cells (up, down, left, right):
|
||
- If the neighbour is within bounds and not visited, add it to the heap
|
||
- Mark it as visited to avoid reprocessing
|
||
|
||
|
||
|
||
**Step 4: Return the result**
|
||
|
||
- When we pop the destination cell from the heap, `result` contains the minimum time needed
|
||
|
||
|
||
|
||
The min-heap ensures we always expand the lowest-elevation unvisited cell first. This greedy strategy guarantees that when we reach the destination, we've found the path with the minimum maximum elevation.
|
||
|
||
common_pitfalls:
|
||
- title: Confusing This With Shortest Path
|
||
description: |
|
||
This is NOT a standard shortest path problem where you sum edge weights. Here, the cost of a path is the **maximum** elevation along it, not the sum.
|
||
|
||
Using standard BFS (which finds shortest path by number of edges) will give wrong answers. For example, a path with 10 cells of elevation 5 is better than a path with 2 cells where one has elevation 20.
|
||
wrong_approach: "Standard BFS counting steps"
|
||
correct_approach: "Min-heap tracking maximum elevation"
|
||
|
||
- title: Not Handling the Starting Cell
|
||
description: |
|
||
The starting cell `(0, 0)` has an elevation too! If `grid[0][0] = 5`, you cannot start swimming until time `t = 5`.
|
||
|
||
Always initialise your result/answer with `grid[0][0]`, not `0`.
|
||
wrong_approach: "Initialising answer to 0"
|
||
correct_approach: "Initialising answer to grid[0][0]"
|
||
|
||
- title: Revisiting Cells
|
||
description: |
|
||
Without proper visited tracking, you might add the same cell to the heap multiple times, leading to TLE or incorrect results.
|
||
|
||
Mark cells as visited **when adding to the heap**, not when popping. This prevents duplicate entries.
|
||
wrong_approach: "Marking visited only when popping from heap"
|
||
correct_approach: "Marking visited immediately when adding to heap"
|
||
|
||
- title: Binary Search Without Proper Bounds
|
||
description: |
|
||
If using binary search, the search space is `[max(grid[0][0], grid[n-1][n-1]), n^2 - 1]`. The lower bound must include both the start and end cell elevations since we must traverse both.
|
||
|
||
Using `[0, n^2 - 1]` works but is less efficient.
|
||
|
||
key_takeaways:
|
||
- "**Minimax path problem**: When optimising the maximum (or minimum) value along a path, consider Dijkstra-like approaches with a heap"
|
||
- "**Multiple valid approaches**: This problem can be solved with heap, binary search + BFS, or Union-Find — each offers different insights"
|
||
- "**Greedy expansion**: The min-heap ensures we always process the most promising cell first, similar to Dijkstra's algorithm"
|
||
- "**Related problems**: Path With Minimum Effort (LC 1631), Cheapest Flights Within K Stops — similar minimax/path optimisation patterns"
|
||
|
||
time_complexity: "O(n^2 log n). Each of the n^2 cells is added to the heap at most once, and heap operations are O(log n^2) = O(log n)."
|
||
space_complexity: "O(n^2). The heap and visited set can each hold up to n^2 elements."
|
||
|
||
solutions:
|
||
- approach_name: Min-Heap (Dijkstra-like)
|
||
is_optimal: true
|
||
code: |
|
||
import heapq
|
||
|
||
def swim_in_water(grid: list[list[int]]) -> int:
|
||
n = len(grid)
|
||
# Heap stores (elevation, row, col)
|
||
heap = [(grid[0][0], 0, 0)]
|
||
visited = {(0, 0)}
|
||
# Track the maximum elevation we've had to traverse
|
||
result = grid[0][0]
|
||
# 4 directions: up, down, left, right
|
||
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
|
||
|
||
while heap:
|
||
# Get the cell with the smallest elevation
|
||
elevation, row, col = heapq.heappop(heap)
|
||
# Update our answer with the maximum elevation seen
|
||
result = max(result, elevation)
|
||
|
||
# If we've reached the destination, return the answer
|
||
if row == n - 1 and col == n - 1:
|
||
return result
|
||
|
||
# Explore all 4 neighbours
|
||
for dr, dc in directions:
|
||
new_row, new_col = row + dr, col + dc
|
||
# Check bounds and if not visited
|
||
if 0 <= new_row < n and 0 <= new_col < n and (new_row, new_col) not in visited:
|
||
visited.add((new_row, new_col))
|
||
heapq.heappush(heap, (grid[new_row][new_col], new_row, new_col))
|
||
|
||
return result # Should never reach here for valid input
|
||
explanation: |
|
||
**Time Complexity:** O(n^2 log n) — Each cell is pushed/popped from the heap once, with O(log n^2) per operation.
|
||
|
||
**Space Complexity:** O(n^2) — For the heap and visited set.
|
||
|
||
This approach is a modified Dijkstra's algorithm. Instead of tracking cumulative distances, we track the maximum elevation encountered. The min-heap ensures we always expand the lowest-elevation frontier cell, guaranteeing optimality.
|
||
|
||
- approach_name: Binary Search + BFS
|
||
is_optimal: false
|
||
code: |
|
||
from collections import deque
|
||
|
||
def swim_in_water(grid: list[list[int]]) -> int:
|
||
n = len(grid)
|
||
|
||
def can_reach(threshold: int) -> bool:
|
||
"""Check if we can reach (n-1, n-1) using only cells with elevation <= threshold."""
|
||
if grid[0][0] > threshold:
|
||
return False
|
||
|
||
queue = deque([(0, 0)])
|
||
visited = {(0, 0)}
|
||
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
|
||
|
||
while queue:
|
||
row, col = queue.popleft()
|
||
if row == n - 1 and col == n - 1:
|
||
return True
|
||
|
||
for dr, dc in directions:
|
||
new_row, new_col = row + dr, col + dc
|
||
if (0 <= new_row < n and 0 <= new_col < n and
|
||
(new_row, new_col) not in visited and
|
||
grid[new_row][new_col] <= threshold):
|
||
visited.add((new_row, new_col))
|
||
queue.append((new_row, new_col))
|
||
|
||
return False
|
||
|
||
# Binary search on the answer
|
||
left = max(grid[0][0], grid[n-1][n-1])
|
||
right = n * n - 1
|
||
|
||
while left < right:
|
||
mid = (left + right) // 2
|
||
if can_reach(mid):
|
||
right = mid # Try a smaller threshold
|
||
else:
|
||
left = mid + 1 # Need a larger threshold
|
||
|
||
return left
|
||
explanation: |
|
||
**Time Complexity:** O(n^2 log(n^2)) = O(n^2 log n) — Binary search has O(log n^2) iterations, each running BFS in O(n^2).
|
||
|
||
**Space Complexity:** O(n^2) — For the BFS queue and visited set.
|
||
|
||
This approach reframes the problem: "Given a water level `t`, can we reach the destination?" We binary search on `t` to find the minimum value where the answer is "yes". The BFS checks connectivity using only cells with elevation ≤ `t`.
|
||
|
||
- approach_name: Union-Find
|
||
is_optimal: false
|
||
code: |
|
||
def swim_in_water(grid: list[list[int]]) -> int:
|
||
n = len(grid)
|
||
|
||
# Union-Find data structure
|
||
parent = list(range(n * n))
|
||
rank = [0] * (n * n)
|
||
|
||
def find(x: int) -> int:
|
||
if parent[x] != x:
|
||
parent[x] = find(parent[x]) # Path compression
|
||
return parent[x]
|
||
|
||
def union(x: int, y: int) -> None:
|
||
px, py = find(x), find(y)
|
||
if px == py:
|
||
return
|
||
# Union by rank
|
||
if rank[px] < rank[py]:
|
||
px, py = py, px
|
||
parent[py] = px
|
||
if rank[px] == rank[py]:
|
||
rank[px] += 1
|
||
|
||
# Map elevation to position
|
||
elevation_to_pos = {}
|
||
for r in range(n):
|
||
for c in range(n):
|
||
elevation_to_pos[grid[r][c]] = (r, c)
|
||
|
||
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
|
||
|
||
# Process cells in order of elevation
|
||
for t in range(n * n):
|
||
r, c = elevation_to_pos[t]
|
||
# Union with any adjacent cell that has elevation <= t
|
||
for dr, dc in directions:
|
||
nr, nc = r + dr, c + dc
|
||
if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] <= t:
|
||
union(r * n + c, nr * n + nc)
|
||
|
||
# Check if start and end are connected
|
||
if find(0) == find(n * n - 1):
|
||
return t
|
||
|
||
return n * n - 1 # Should never reach here for valid input
|
||
explanation: |
|
||
**Time Complexity:** O(n^2 α(n^2)) ≈ O(n^2) — We process each cell once, with near-constant time Union-Find operations.
|
||
|
||
**Space Complexity:** O(n^2) — For the parent and rank arrays, plus the elevation map.
|
||
|
||
This approach processes cells in increasing order of elevation. At each step, we union the current cell with any adjacent cells that are already "underwater" (elevation ≤ current time). We stop when the start and end cells become connected. This is theoretically the fastest approach due to the near-constant time Union-Find operations.
|