questions S-W
This commit is contained in:
273
backend/data/questions/swim-in-rising-water.yaml
Normal file
273
backend/data/questions/swim-in-rising-water.yaml
Normal file
@@ -0,0 +1,273 @@
|
||||
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
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user