LeetCode 2258 - Escape the Spreading Fire
This problem asks us to determine how long we can safely wait before starting to move from the top-left corner of a grid to the bottom-right corner while a fire spreads across the grid over time.
Difficulty: 🔴 Hard
Topics: Array, Binary Search, Breadth-First Search, Matrix
Solution
Problem Understanding
This problem asks us to determine how long we can safely wait before starting to move from the top-left corner of a grid to the bottom-right corner while a fire spreads across the grid over time.
The grid contains three types of cells:
0represents empty grass that both the player and the fire may move through.1represents a fire source.2represents a wall that blocks both movement and fire spread.
The player begins at (0, 0) and wants to reach the safehouse at (m - 1, n - 1).
The sequence of events for every minute is extremely important:
- The player moves first.
- After the move, the fire spreads to adjacent cells.
The fire spreads in four directions, up, down, left, and right, exactly like a standard breadth-first search expansion.
The goal is to compute the maximum number of minutes the player may remain standing still at the starting cell before beginning movement, while still being able to reach the safehouse safely.
There are three possible outcomes:
- Return
-1if escape is impossible even when starting immediately. - Return
10^9if escape is always possible regardless of waiting time. - Otherwise, return the largest valid waiting time.
One subtle but extremely important rule is that arriving at the safehouse at the exact same time the fire reaches it is still considered safe. This exception applies only to the destination cell. For all other cells, the player must arrive strictly before the fire.
The constraints are large:
m * n <= 2 * 10^4
This immediately tells us that expensive repeated simulations over the entire grid could become problematic. We need something close to linear or near-linear complexity per major operation.
Several edge cases are especially important:
- The fire may already block all paths immediately.
- The fire may be completely trapped by walls.
- The player and fire may reach cells simultaneously.
- The safehouse allows equal arrival time, but intermediate cells do not.
- The starting cell itself may eventually catch fire before movement begins.
A naive implementation often fails because it mishandles timing comparisons or forgets the special rule for the destination cell.
Approaches
Brute Force Approach
The most straightforward solution is to try every possible waiting time one by one.
For a chosen waiting time t:
- Simulate the fire spreading for
tminutes. - Start a BFS for the player.
- At every minute:
- Move the player.
- Spread the fire.
- Check whether the player reaches the safehouse safely.
This approach is conceptually simple because it directly follows the problem statement. Unfortunately, it is far too slow.
The maximum useful waiting time can be very large, potentially up to 10^9. Even if we cap the search at some practical limit based on grid size, repeatedly simulating both fire and player movement becomes extremely expensive.
The major inefficiency is recomputing the fire spread from scratch for every tested waiting time.
Key Insight
The crucial optimization is to precompute the earliest time the fire reaches every cell.
This transforms the problem from a dynamic simulation into a timing comparison problem.
We can perform a multi-source BFS starting from every fire cell simultaneously. This gives us:
fire_time[r][c] = earliest minute fire reaches cell
Once we know fire arrival times, we can test whether a particular waiting time is feasible using another BFS for the player.
During the player's BFS:
-
For intermediate cells:
-
player arrival time must be strictly less than fire arrival time
-
For the destination:
-
player arrival time may be less than or equal to fire arrival time
Now we can binary search the answer because feasibility is monotonic:
- If waiting
xminutes works, then all smaller waiting times also work. - If waiting
xminutes fails, then all larger waiting times also fail.
This converts the problem into:
- One BFS for fire preprocessing
- Multiple BFS checks during binary search
This is efficient enough for the constraints.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O((m × n)^2) or worse | O(m × n) | Repeatedly simulates fire and player movement |
| Optimal | O(m × n × log(m × n)) | O(m × n) | Precompute fire times and binary search waiting time |
Algorithm Walkthrough
- Precompute fire arrival times using multi-source BFS.
We create a matrix fire_time initialized to infinity. Every initial fire cell is inserted into the BFS queue with time 0.
Using BFS guarantees that the first time we visit a cell is the earliest possible time the fire can reach it. 2. Define a feasibility check function.
The function can_escape(wait) determines whether the player can survive after waiting wait minutes before moving.
3. Handle the starting cell carefully.
If the fire reaches (0, 0) before or exactly when the player starts moving, escape is impossible.
Since the player waits first, we require:
wait < fire_time[0][0]
unless the fire never reaches the start. 4. Run BFS for the player.
The player's initial time is wait.
For each move:
- moving to a neighbor increases time by
1 - walls cannot be entered
- visited cells are skipped
- Compare player arrival time with fire arrival time.
For normal cells:
player_time < fire_time[cell]
For the safehouse:
player_time <= fire_time[safehouse]
This directly implements the special rule from the statement. 6. Return whether the safehouse is reachable.
If BFS reaches the destination under valid timing constraints, the waiting time is feasible. 7. Binary search the maximum valid waiting time.
Since feasibility is monotonic:
- valid waits form a prefix
- invalid waits form a suffix
We binary search over waiting times. 8. Handle the infinite-wait case.
If the maximum feasible wait is very large, specifically at least m * n, then the fire can never meaningfully block the player.
In this case, return:
10^9
Why it works
The fire BFS correctly computes the earliest possible fire arrival time for every reachable cell because BFS explores shortest paths in an unweighted grid.
The player BFS then checks whether the player can stay ahead of the fire along some path. Since every move costs exactly one minute, BFS again guarantees shortest arrival times.
Binary search is valid because waiting longer can never improve survivability. Once a waiting time becomes impossible, all larger waiting times are also impossible.
Together, these properties guarantee correctness.
Python Solution
from collections import deque
from typing import List
class Solution:
def maximumMinutes(self, grid: List[List[int]]) -> int:
rows = len(grid)
cols = len(grid[0])
INF = 10**18
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
fire_time = [[INF] * cols for _ in range(rows)]
queue = deque()
# Multi-source BFS for fire
for r in range(rows):
for c in range(cols):
if grid[r][c] == 1:
fire_time[r][c] = 0
queue.append((r, c))
while queue:
r, c = queue.popleft()
for dr, dc in directions:
nr = r + dr
nc = c + dc
if (
0 <= nr < rows
and 0 <= nc < cols
and grid[nr][nc] != 2
and fire_time[nr][nc] == INF
):
fire_time[nr][nc] = fire_time[r][c] + 1
queue.append((nr, nc))
def can_escape(wait: int) -> bool:
# Fire reaches start before or at departure
if wait >= fire_time[0][0]:
return False
queue = deque([(0, 0, wait)])
visited = [[False] * cols for _ in range(rows)]
visited[0][0] = True
while queue:
r, c, time = queue.popleft()
if r == rows - 1 and c == cols - 1:
return True
for dr, dc in directions:
nr = r + dr
nc = c + dc
next_time = time + 1
if not (0 <= nr < rows and 0 <= nc < cols):
continue
if grid[nr][nc] == 2 or visited[nr][nc]:
continue
fire_arrival = fire_time[nr][nc]
# Destination allows equal arrival
if nr == rows - 1 and nc == cols - 1:
if next_time > fire_arrival:
continue
else:
if next_time >= fire_arrival:
continue
visited[nr][nc] = True
queue.append((nr, nc, next_time))
return False
left = 0
right = rows * cols + 1
answer = -1
while left <= right:
mid = (left + right) // 2
if can_escape(mid):
answer = mid
left = mid + 1
else:
right = mid - 1
if answer == rows * cols + 1:
return 10**9
return answer
The implementation begins by computing the earliest fire arrival time for every cell. This is done using a multi-source BFS where all fire cells are inserted into the queue initially.
The fire_time matrix stores the minimum minute at which fire reaches each cell. Unreachable cells remain at infinity.
The helper function can_escape(wait) checks whether escape is possible after waiting wait minutes. It immediately rejects cases where the starting cell burns before movement begins.
The player's BFS tracks both position and current time. Every movement increases time by one minute.
The timing comparisons are the most important part of the implementation:
- Intermediate cells require strict inequality.
- The destination allows equality.
Finally, binary search determines the largest feasible waiting time efficiently.
Go Solution
package main
import "container/list"
func maximumMinutes(grid [][]int) int {
rows := len(grid)
cols := len(grid[0])
const INF int = 1_000_000_000
directions := [][]int{
{1, 0},
{-1, 0},
{0, 1},
{0, -1},
}
fireTime := make([][]int, rows)
for i := range fireTime {
fireTime[i] = make([]int, cols)
for j := range fireTime[i] {
fireTime[i][j] = INF
}
}
type Node struct {
r, c int
}
queue := list.New()
// Multi-source BFS for fire
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if grid[r][c] == 1 {
fireTime[r][c] = 0
queue.PushBack(Node{r, c})
}
}
}
for queue.Len() > 0 {
front := queue.Front()
queue.Remove(front)
cur := front.Value.(Node)
for _, d := range directions {
nr := cur.r + d[0]
nc := cur.c + d[1]
if nr < 0 || nr >= rows || nc < 0 || nc >= cols {
continue
}
if grid[nr][nc] == 2 {
continue
}
if fireTime[nr][nc] != INF {
continue
}
fireTime[nr][nc] = fireTime[cur.r][cur.c] + 1
queue.PushBack(Node{nr, nc})
}
}
canEscape := func(wait int) bool {
if wait >= fireTime[0][0] {
return false
}
type State struct {
r, c, t int
}
visited := make([][]bool, rows)
for i := range visited {
visited[i] = make([]bool, cols)
}
queue := list.New()
queue.PushBack(State{0, 0, wait})
visited[0][0] = true
for queue.Len() > 0 {
front := queue.Front()
queue.Remove(front)
cur := front.Value.(State)
if cur.r == rows-1 && cur.c == cols-1 {
return true
}
for _, d := range directions {
nr := cur.r + d[0]
nc := cur.c + d[1]
nt := cur.t + 1
if nr < 0 || nr >= rows || nc < 0 || nc >= cols {
continue
}
if grid[nr][nc] == 2 || visited[nr][nc] {
continue
}
fireArrival := fireTime[nr][nc]
if nr == rows-1 && nc == cols-1 {
if nt > fireArrival {
continue
}
} else {
if nt >= fireArrival {
continue
}
}
visited[nr][nc] = true
queue.PushBack(State{nr, nc, nt})
}
}
return false
}
left := 0
right := rows*cols + 1
answer := -1
for left <= right {
mid := (left + right) / 2
if canEscape(mid) {
answer = mid
left = mid + 1
} else {
right = mid - 1
}
}
if answer == rows*cols+1 {
return 1_000_000_000
}
return answer
}
The Go implementation closely mirrors the Python version, but there are several language-specific differences.
Go does not provide a built-in deque, so container/list is used for BFS queues.
Structs are used to represent BFS states cleanly. This avoids using nested slices or arrays for coordinates and timestamps.
The INF value is represented using a large integer constant instead of Python's arbitrary-precision integers.
Slices are initialized manually because Go does not support Python-style list comprehensions.
Worked Examples
Example 1
grid =
[
[0,2,0,0,0,0,0],
[0,0,0,2,2,1,0],
[0,2,0,0,1,2,0],
[0,0,2,2,2,0,2],
[0,0,0,0,0,0,0]
]
Step 1: Compute fire arrival times
The fire starts from:
(1,5)(2,4)
Multi-source BFS computes the earliest fire reach times.
A simplified view:
| Cell | Fire Arrival |
|---|---|
| (1,5) | 0 |
| (2,4) | 0 |
| nearby cells | 1 |
| farther cells | increasing times |
Walls remain unreachable.
Step 2: Binary search waiting time
Suppose we test wait = 3.
The player starts moving at time 3.
Step 3: Player BFS
The BFS explores paths while checking:
player_time < fire_time
for intermediate cells.
Eventually, the player reaches the destination before the fire closes all escape routes.
So wait = 3 succeeds.
Step 4: Test larger waits
Testing wait = 4 fails because the fire blocks critical cells before the player arrives.
Thus the answer is:
3
Example 2
grid =
[
[0,0,0,0],
[0,1,2,0],
[0,2,0,0]
]
The fire expands rapidly into all possible paths.
Even with wait = 0, every route becomes unsafe before the player can reach the destination.
The BFS never reaches the safehouse.
Result:
-1
Example 3
grid =
[
[0,0,0],
[2,2,0],
[1,2,0]
]
The fire is trapped behind walls.
The fire BFS cannot reach the path from start to destination.
Therefore, all path cells have infinite fire arrival time.
The player can wait arbitrarily long.
Result:
1000000000
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m × n × log(m × n)) | One fire BFS plus binary search with BFS checks |
| Space | O(m × n) | Fire times, visited arrays, and BFS queues |
The fire preprocessing BFS visits each cell at most once, giving O(m × n) complexity.
Each feasibility check also performs a BFS over the grid, again costing O(m × n).
Binary search performs O(log(m × n)) checks, so the total complexity becomes:
O(m × n × log(m × n))
This is efficient enough for grids containing up to 2 × 10^4 cells.
Test Cases
sol = Solution()
# Example 1
assert sol.maximumMinutes([
[0,2,0,0,0,0,0],
[0,0,0,2,2,1,0],
[0,2,0,0,1,2,0],
[0,0,2,2,2,0,2],
[0,0,0,0,0,0,0]
]) == 3 # standard mixed scenario
# Example 2
assert sol.maximumMinutes([
[0,0,0,0],
[0,1,2,0],
[0,2,0,0]
]) == -1 # impossible escape
# Example 3
assert sol.maximumMinutes([
[0,0,0],
[2,2,0],
[1,2,0]
]) == 1000000000 # fire fully trapped
# Minimal grid with direct path
assert sol.maximumMinutes([
[0,0],
[0,0]
]) == 1000000000 # no fire anywhere
# Fire adjacent to start
assert sol.maximumMinutes([
[0,1],
[0,0]
]) == -1 # start immediately threatened
# Fire adjacent to destination
assert sol.maximumMinutes([
[0,0,0],
[0,0,1],
[0,0,0]
]) >= 0 # validates destination timing logic
# Narrow corridor
assert sol.maximumMinutes([
[0,2,0],
[0,2,0],
[0,0,0]
]) == 1000000000 # fire absent, constrained path
# Completely blocked route
assert sol.maximumMinutes([
[0,2,0],
[2,2,0],
[0,0,0]
]) == -1 # no path exists
# Fire reaches destination exactly with player
assert sol.maximumMinutes([
[0,0,0],
[1,2,0],
[0,0,0]
]) >= 0 # tests equal-arrival destination rule
| Test | Why |
|---|---|
| Example 1 | Standard mixed fire and wall scenario |
| Example 2 | Impossible escape case |
| Example 3 | Infinite waiting scenario |
| Minimal grid | Verifies behavior without fire |
| Fire adjacent to start | Tests immediate danger |
| Fire adjacent to destination | Tests safehouse timing |
| Narrow corridor | Tests constrained movement |
| Completely blocked route | Tests disconnected paths |
| Equal arrival at destination | Verifies special safehouse rule |
Edge Cases
Fire Never Reaches the Safehouse
One tricky scenario occurs when walls completely isolate the fire from the player's path. In this case, the fire BFS leaves many cells with infinite arrival time.
A buggy implementation might still impose artificial timing limits and incorrectly reject large waiting times.
This implementation handles the case correctly because unreachable fire times remain infinity, allowing the player BFS to move freely forever. When the binary search detects arbitrarily large feasible waits, the algorithm returns 10^9.
Player and Fire Reach Destination Simultaneously
The destination cell has a special rule:
player_time <= fire_time[destination]
Many incorrect solutions accidentally require strict inequality everywhere.
This implementation explicitly checks whether the current cell is the safehouse and applies the relaxed comparison only there.
Fire Reaches the Starting Cell Before Movement Begins
If the player waits too long, the starting cell itself may ignite before movement begins.
This is easy to overlook because many BFS implementations only check timing after movement starts.
This solution correctly rejects such waits immediately using:
if wait >= fire_time[0][0]:
return False
That guarantees the player never begins from a burning cell.