LeetCode 2814 - Minimum Time Takes to Reach Destination Without Drowning
This problem takes place on a two-dimensional grid where each cell represents a type of terrain. The grid contains: - "S": your starting position. - "D": the destination you want to reach. - ".": an empty cell that can be walked on. - "X": a stone cell that cannot be entered.
Difficulty: 🔴 Hard
Topics: Array, Breadth-First Search, Matrix
Solution
LeetCode 2814 - Minimum Time Takes to Reach Destination Without Drowning
Problem Understanding
This problem takes place on a two-dimensional grid where each cell represents a type of terrain.
The grid contains:
"S": your starting position."D": the destination you want to reach.".": an empty cell that can be walked on."X": a stone cell that cannot be entered."*": a flooded cell.
Every second, two things happen conceptually:
- You may move one step in one of the four cardinal directions.
- Flood water spreads from every flooded cell into adjacent empty cells.
The critical rule is that you cannot enter a flooded cell. Furthermore, you cannot move into a cell at the exact moment it becomes flooded. If a cell floods at time t, then arriving there at time t is already too late.
The destination cell is special because the problem guarantees that it will never be flooded. Water only spreads into empty cells (".").
The goal is to determine the minimum number of seconds required to travel from S to D while always staying ahead of the flood. If no safe path exists, we return -1.
Understanding the Constraints
The grid dimensions satisfy:
2 <= n, m <= 100
This means the grid contains at most:
100 * 100 = 10,000 cells
A graph traversal that visits each cell a constant number of times is completely feasible.
The relatively small grid size strongly suggests a Breadth-First Search solution because BFS naturally computes shortest paths in unweighted grids.
Important Edge Cases
Several situations can cause incorrect solutions:
- The shortest geometric path may become unusable because water reaches it first.
- Multiple flooded cells spread simultaneously, so we must model all water sources together.
- A cell that floods at time
tcannot be entered at timet. - The destination may be surrounded by water or stones even though it never floods itself.
- The start position might become trapped immediately.
- There may be no flood cells at all, reducing the problem to a standard shortest-path search.
The guarantee that exactly one S and one D exist simplifies parsing the grid.
Approaches
Brute Force
A naive approach would explicitly simulate the world second by second.
At each time step:
- Spread the flood.
- Track every position the player could currently occupy.
- Continue until either the destination is reached or no positions remain.
This eventually produces the correct answer because it accurately models the process. However, the state space grows over time, and repeatedly updating the flood for every second introduces unnecessary work.
The main inefficiency is that flood timing is recomputed over and over. We only care about the earliest moment each cell becomes flooded.
Key Insight
Instead of simulating both processes simultaneously, we can separate them.
The crucial observation is that flood propagation is independent of the player's movement.
First, compute:
floodTime[r][c]
which stores the earliest second when each cell becomes flooded.
This can be found using a multi-source BFS starting from every "*" cell.
After we know flood arrival times, we perform another BFS from the start position.
When attempting to move into a cell at time t + 1:
- The cell must not be a stone.
- The cell must not already be flooded.
- The cell must not flood at time
t + 1.
In other words:
arrivalTime < floodTime
must hold.
Once flood arrival times are known, the remaining problem becomes a shortest-path search with timing constraints.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O((nm)^2) in the worst case | O(nm) | Repeatedly simulates flood spreading and player movement |
| Optimal | O(nm) | O(nm) | Two BFS traversals, one for flood and one for player |
Algorithm Walkthrough
Step 1: Locate special cells
Scan the grid once and identify:
- Start position
S - Destination position
D - Every flooded source
*
All flood sources are inserted into a queue for the first BFS.
Step 2: Compute flood arrival times
Create a matrix:
floodTime[r][c]
initialized to infinity.
For every flooded source:
floodTime[source] = 0
Run a multi-source BFS.
Whenever water reaches an adjacent empty cell for the first time:
floodTime[next] = floodTime[current] + 1
Important details:
- Water cannot enter stones.
- Water cannot enter the destination.
- Water only spreads into
"."cells.
At the end, every reachable empty cell knows exactly when it floods.
Step 3: Perform BFS from the start
Create another queue containing:
(startRow, startCol, 0)
where the third value is elapsed time.
Maintain a visited matrix so each cell is processed once.
Step 4: Explore neighbors
For each state:
(row, col, currentTime)
consider the four neighboring cells.
The arrival time becomes:
nextTime = currentTime + 1
A move is allowed if:
- The cell is inside the grid.
- The cell is not a stone.
- The cell has not been visited.
- The cell is the destination, or
nextTime < floodTime[nextRow][nextCol].
The strict inequality is critical because arriving at the exact flooding moment means drowning.
Step 5: Return when destination is reached
Because BFS explores states in increasing order of time, the first time we reach D is guaranteed to be the minimum travel time.
Return that value immediately.
Step 6: Handle failure
If BFS finishes without reaching the destination, return:
-1
Why it works
The first BFS computes the earliest flooding time for every cell. Since BFS explores in increasing distance order and all flood sources start simultaneously, the recorded value is exactly the first second when water reaches that cell.
The second BFS explores all safe paths in increasing order of travel time. A move is permitted only if the traveler arrives strictly before the flood. Therefore every state processed by the BFS represents a valid position at that moment in time. Since BFS always finds the shortest path in an unweighted graph, the first time the destination is reached is the minimum safe travel time.
Python Solution
from collections import deque
from typing import List
class Solution:
def minimumSeconds(self, land: List[List[str]]) -> int:
rows, cols = len(land), len(land[0])
INF = 10**9
flood_time = [[INF] * cols for _ in range(rows)]
flood_queue = deque()
start = None
destination = None
for r in range(rows):
for c in range(cols):
if land[r][c] == "*":
flood_time[r][c] = 0
flood_queue.append((r, c))
elif land[r][c] == "S":
start = (r, c)
elif land[r][c] == "D":
destination = (r, c)
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
# Multi-source BFS for flood spread.
while flood_queue:
r, c = flood_queue.popleft()
for dr, dc in directions:
nr, nc = r + dr, c + dc
if not (0 <= nr < rows and 0 <= nc < cols):
continue
if land[nr][nc] != ".":
continue
if flood_time[nr][nc] != INF:
continue
flood_time[nr][nc] = flood_time[r][c] + 1
flood_queue.append((nr, nc))
sr, sc = start
dr, dc = destination
queue = deque([(sr, sc, 0)])
visited = [[False] * cols for _ in range(rows)]
visited[sr][sc] = True
while queue:
r, c, time = queue.popleft()
if (r, c) == (dr, dc):
return time
for drow, dcol in directions:
nr, nc = r + drow, c + dcol
if not (0 <= nr < rows and 0 <= nc < cols):
continue
if visited[nr][nc]:
continue
if land[nr][nc] == "X":
continue
next_time = time + 1
if land[nr][nc] == "D":
visited[nr][nc] = True
queue.append((nr, nc, next_time))
continue
if next_time >= flood_time[nr][nc]:
continue
visited[nr][nc] = True
queue.append((nr, nc, next_time))
return -1
Implementation Explanation
The first section scans the grid and gathers all flooded cells, the start position, and the destination.
The flood_time matrix stores the earliest flooding time for every cell. A multi-source BFS begins with all flood sources simultaneously. Because every source starts at time zero, BFS naturally computes the minimum flooding time for each reachable empty cell.
After flood timing is known, a second BFS searches for the traveler's shortest safe route. Each queue entry contains the current position and elapsed time.
When moving into a neighboring cell, the algorithm computes the arrival time and checks whether that time is strictly less than the flood arrival time. This enforces the rule that entering a cell exactly when it floods is not allowed.
Since BFS explores states by increasing time, the first visit to the destination is the optimal answer.
Go Solution
package main
import "container/list"
func minimumSeconds(land [][]string) int {
rows := len(land)
cols := len(land[0])
const INF = int(1e9)
floodTime := make([][]int, rows)
for i := range floodTime {
floodTime[i] = make([]int, cols)
for j := range floodTime[i] {
floodTime[i][j] = INF
}
}
type Cell struct {
r int
c int
}
floodQueue := list.New()
var startR, startC int
var destR, destC int
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
switch land[r][c] {
case "*":
floodTime[r][c] = 0
floodQueue.PushBack(Cell{r, c})
case "S":
startR, startC = r, c
case "D":
destR, destC = r, c
}
}
}
dirs := [][2]int{
{1, 0},
{-1, 0},
{0, 1},
{0, -1},
}
for floodQueue.Len() > 0 {
front := floodQueue.Front()
floodQueue.Remove(front)
cur := front.Value.(Cell)
for _, d := range dirs {
nr := cur.r + d[0]
nc := cur.c + d[1]
if nr < 0 || nr >= rows || nc < 0 || nc >= cols {
continue
}
if land[nr][nc] != "." {
continue
}
if floodTime[nr][nc] != INF {
continue
}
floodTime[nr][nc] = floodTime[cur.r][cur.c] + 1
floodQueue.PushBack(Cell{nr, nc})
}
}
type State struct {
r int
c int
time int
}
queue := list.New()
queue.PushBack(State{startR, startC, 0})
visited := make([][]bool, rows)
for i := range visited {
visited[i] = make([]bool, cols)
}
visited[startR][startC] = true
for queue.Len() > 0 {
front := queue.Front()
queue.Remove(front)
cur := front.Value.(State)
if cur.r == destR && cur.c == destC {
return cur.time
}
for _, d := range dirs {
nr := cur.r + d[0]
nc := cur.c + d[1]
if nr < 0 || nr >= rows || nc < 0 || nc >= cols {
continue
}
if visited[nr][nc] {
continue
}
if land[nr][nc] == "X" {
continue
}
nextTime := cur.time + 1
if land[nr][nc] == "D" {
visited[nr][nc] = true
queue.PushBack(State{nr, nc, nextTime})
continue
}
if nextTime >= floodTime[nr][nc] {
continue
}
visited[nr][nc] = true
queue.PushBack(State{nr, nc, nextTime})
}
}
return -1
}
Go-Specific Notes
The Go solution mirrors the Python algorithm exactly. Since Go does not provide a built-in deque, container/list is used as the BFS queue. The flood timing matrix is initialized with a large sentinel value (1e9) representing infinity. All time values remain well within Go's integer range because the grid contains at most 10,000 cells.
Worked Examples
Example 1
D . *
. . .
. S .
Flood BFS
| Cell | Flood Time |
|---|---|
| (0,2) | 0 |
| (0,1) | 1 |
| (1,2) | 1 |
| (0,0) | 2 |
| (1,1) | 2 |
| (2,2) | 2 |
| (1,0) | 3 |
| (2,1) | 3 |
| (2,0) | 4 |
Destination is not flooded.
Person BFS
| Time | Reachable Position |
|---|---|
| 0 | (2,1) |
| 1 | (2,0), (1,1) |
| 2 | (1,0) |
| 3 | (0,0)=D |
Answer:
3
Example 2
D X *
. . .
. . S
Flood Times
| Cell | Flood Time |
|---|---|
| (0,2) | 0 |
| (1,2) | 1 |
| (1,1) | 2 |
| (2,2) | 2 |
| (1,0) | 3 |
| (2,1) | 3 |
| (2,0) | 4 |
Any route toward the destination requires entering cells that flood before or at arrival time.
Result:
-1
Example 3
D . . . * .
. X . X . .
. . . . S .
The flood BFS computes the earliest flooding times throughout the grid.
The traveler BFS then explores all safe positions in increasing travel time order.
The shortest safe route reaches the destination after:
6 seconds
Therefore the answer is:
6
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(nm) | Each BFS visits every cell at most once |
| Space | O(nm) | Flood matrix, visited matrix, and BFS queues |
Both BFS traversals process each cell at most one time. Since the grid contains n * m cells, the total running time is linear in the size of the grid. The auxiliary memory consists primarily of the flood timing matrix, the visited matrix, and the queues, all of which require linear space.
Test Cases
sol = Solution()
assert sol.minimumSeconds([
["D", ".", "*"],
[".", ".", "."],
[".", "S", "."]
]) == 3 # Example 1
assert sol.minimumSeconds([
["D", "X", "*"],
[".", ".", "."],
[".", ".", "S"]
]) == -1 # Example 2
assert sol.minimumSeconds([
["D", ".", ".", ".", "*", "."],
[".", "X", ".", "X", ".", "."],
[".", ".", ".", ".", "S", "."]
]) == 6 # Example 3
assert sol.minimumSeconds([
["D", "."],
["S", "."]
]) == 1 # No flood, direct path
assert sol.minimumSeconds([
["D", ".", "."],
["X", "X", "."],
["S", ".", "."]
]) == 4 # Must travel around obstacles
assert sol.minimumSeconds([
["D", ".", "*"],
["X", "X", "."],
["S", ".", "."]
]) == -1 # Flood blocks all routes
assert sol.minimumSeconds([
["D", "."],
[".", "S"]
]) == 1 # Smallest practical grid
assert sol.minimumSeconds([
["D", ".", ".", "."],
[".", "X", "X", "."],
[".", ".", ".", "."],
["S", ".", ".", "*"]
]) == -1 # Destination unreachable before flood
assert sol.minimumSeconds([
["D", ".", ".", "."],
[".", ".", ".", "."],
[".", ".", ".", "."],
["S", ".", ".", "."]
]) == 3 # No flood, shortest path BFS
Test Summary
| Test | Why |
|---|---|
| Example 1 | Basic successful escape |
| Example 2 | Flood overtakes every path |
| Example 3 | Larger grid with obstacles and flood |
| No flood direct path | Reduces to ordinary BFS |
| Obstacle detour | Verifies shortest-path behavior |
| Flood blocks route | Tests timing constraints |
| Smallest practical grid | Boundary dimensions |
| Flood near destination | Ensures safe arrival checks |
| Empty open grid | Standard shortest path |
Edge Cases
Arriving Exactly When a Cell Floods
One of the easiest mistakes is allowing a move into a cell at the same second that water arrives. The problem explicitly forbids this. If a cell floods at time 5, arriving at time 5 means drowning.
The implementation enforces:
arrivalTime < floodTime
instead of:
arrivalTime <= floodTime
which correctly handles this subtle rule.
Multiple Flood Sources
Flood may start from several locations simultaneously. Running a separate BFS from each flood source would be inefficient and could produce complicated logic for combining results.
The multi-source BFS begins with every "*" cell already in the queue at time 0. This naturally computes the earliest flood arrival time for every location.
Destination Adjacent to Flood
The destination never floods, but neighboring cells can. A naive implementation might incorrectly propagate water into the destination or require a flood-time check before entering it.
The solution treats "D" specially. During flood BFS, water never enters the destination. During traveler BFS, reaching "D" is always allowed as long as the move itself is valid.
Start Position Becoming Trapped
Sometimes the traveler begins with several available moves, but every neighboring cell floods too quickly. A path may exist geometrically yet still be impossible temporally.
Because every move is checked against the precomputed flood arrival times, unsafe moves are rejected immediately. If no valid states remain in the BFS queue, the algorithm correctly returns -1.