2812. Find the Safest Path in a Grid - Explanation
Description
The square binary grid marks thieves with 1 and empty cells with 0. A cell's safety is its Manhattan distance to the nearest thief: the distance between (r,c) and (x,y) is |r-x| + |c-y|. Travel from (0,0) to (n-1,n-1) using moves up, down, left, or right. You may visit any cell, including a thief's cell. A path's safeness factor is the smallest cell safety encountered anywhere on that path, including its endpoints. Return the largest possible safeness factor. The grid contains at least one thief.
Filled cells contain thieves; hatching marks one safest route through the grid.
Example 1:
Input: grid = [[0,1],[0,0]]
Output: 1Explanation: Take (0,0) → (1,0) → (1,1). The distances to the thief at (0,1) are 1, 2, and 1, so this path has safeness 1. No path can exceed 1 because the starting cell has safety 1.
Example 2:
Input: grid = [[0,0,0],[0,1,0],[0,0,0]]
Output: 1Explanation: Travel along the top and right edges to avoid the center thief. Every visited cell stays at least one step away. Both possible first moves are only one step from the thief, so a safeness factor of 2 is impossible.
Example 3:
Input: grid = [[1]]
Output: 0Explanation: The start and destination are the same cell, and that cell contains a thief. Its distance to the nearest thief is 0, so the answer is 0.
Constraints:
1 <= n <= 400;gridhasnrows andncolumns.- Every entry is
0or1. - At least one cell contains
1.
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Multi-Source BFS - Used to compute minimum distances from all thief cells simultaneously
- Dijkstra's Algorithm - Modified to maximize the minimum distance along a path using a max-heap
- Binary Search - Alternative approach to find the maximum safeness factor by testing thresholds
- 0-1 BFS - An optimization using a deque when edge weights are limited to 0 or 1
- Priority Queue/Heap - Essential data structure for efficient Dijkstra implementation
1. Multi Source BFS + Dijkstra's Algorithm
Intuition
The safeness factor of a path is the minimum distance to any thief along that path. We want to maximize this minimum. First, we need to know the distance from every cell to the nearest thief, which we can compute using multi-source BFS from all thief positions. Then, finding the safest path becomes a modified shortest path problem where we maximize the minimum edge weight along the path, which Dijkstra's algorithm with a max-heap handles well.
Algorithm
- Run multi-source BFS from all cells containing thieves to compute
minDist[r][c]for every cell. - Use a max-heap starting from
(0, 0)with priority equal tominDist[0][0]. - Maintain a visited set to avoid reprocessing cells.
- For each cell popped from the heap:
- If it is the destination
(N-1, N-1), return the current safeness factor. - For each unvisited neighbor, compute the new safeness as
min(current safeness, minDist[neighbor]). - Push the neighbor with this new safeness value.
- If it is the destination
- Return the safeness when reaching the destination.
class Solution:
def maximumSafenessFactor(self, grid: List[List[int]]) -> int:
N = len(grid)
def in_bounds(r, c):
return min(r, c) >= 0 and max(r, c) < N
def precompute():
q = deque()
min_dist = {}
for r in range(N):
for c in range(N):
if grid[r][c]:
q.append([r, c, 0])
min_dist[(r, c)] = 0
while q:
r, c, dist = q.popleft()
nei = [[r+1, c], [r-1, c], [r, c+1], [r, c-1]]
for r2, c2 in nei:
if in_bounds(r2, c2) and (r2, c2) not in min_dist:
min_dist[(r2, c2)] = dist + 1
q.append([r2, c2, dist + 1])
return min_dist
min_dist = precompute()
maxHeap = [(-min_dist[(0, 0)], 0, 0)] # (dist, r, c)
visit = set()
visit.add((0, 0))
while maxHeap:
dist, r, c = heapq.heappop(maxHeap)
dist = -dist
if (r, c) == (N-1, N-1):
return dist
nei = [[r+1, c], [r-1, c], [r, c+1], [r, c-1]]
for r2, c2 in nei:
if in_bounds(r2, c2) and (r2, c2) not in visit:
visit.add((r2, c2))
dist2 = min(dist, min_dist[(r2, c2)])
heapq.heappush(maxHeap, (-dist2, r2, c2))Time & Space Complexity
- Time complexity:
- Space complexity:
2. Multi Source BFS + Dijkstra's Algorithm (Overwriting the Input)
Intuition
This approach is identical to the previous one but optimizes space by reusing the input grid to store the precomputed minimum distances. Additionally, it uses a safeFactor array to track the best safeness factor found so far for each cell, allowing us to skip cells if we have already found a better path to them.
Algorithm
- Overwrite the input grid: set thief cells to
0and others to-1. - Run multi-source BFS to fill each cell with its distance to the nearest thief.
- Initialize a
safeFactorarray to track the best safeness to each cell. - Use a max-heap starting from cell
0with initial safenessminDist[0][0]. - For each cell popped:
- Skip if we already have a better safeness for this cell.
- If it is the destination, return the safeness.
- Update neighbors if we can improve their safeness factor.
- Return
0if no path exists.
class Solution:
def maximumSafenessFactor(self, grid):
N = len(grid)
minDist = grid
directions = [0, 1, 0, -1, 0]
q = deque()
for r in range(N):
for c in range(N):
if grid[r][c] == 1:
q.append(r * N + c)
minDist[r][c] = 0
else:
minDist[r][c] = -1
while q:
node = q.popleft()
r, c = divmod(node, N)
for i in range(4):
r2, c2 = r + directions[i], c + directions[i + 1]
if 0 <= r2 < N and 0 <= c2 < N and minDist[r2][c2] == -1:
minDist[r2][c2] = minDist[r][c] + 1
q.append(r2 * N + c2)
maxHeap = [(-minDist[0][0], 0)]
safeFactor = [0] * (N * N)
safeFactor[0] = minDist[0][0]
while maxHeap:
dist, node = heapq.heappop(maxHeap)
dist = -dist
r, c = divmod(node, N)
if r == N - 1 and c == N - 1:
return dist
if safeFactor[node] > dist:
continue
for i in range(4):
r2, c2 = r + directions[i], c + directions[i + 1]
node2 = r2 * N + c2
if 0 <= r2 < N and 0 <= c2 < N:
dist2 = min(dist, minDist[r2][c2])
if dist2 > safeFactor[node2]:
safeFactor[node2] = dist2
heapq.heappush(maxHeap, (-dist2, node2))
return 0Time & Space Complexity
- Time complexity:
- Space complexity:
3. Multi Source BFS + Binary Search
Intuition
Instead of using Dijkstra to find the optimal safeness, we can binary search on the answer. For a given safeness threshold, we check if there exists a path from start to end using only cells with minDist >= threshold. This check is a simple BFS or DFS. The maximum valid threshold is our answer.
Algorithm
- Precompute
minDistfor all cells using multi-source BFS from thieves. - Binary search on the safeness factor in range
[0, min(minDist[0][0], minDist[N-1][N-1])]. - For each candidate
mid:- Run BFS/DFS to check if we can reach
(N-1, N-1)from(0, 0)using only cells withminDist >= mid. - If reachable, try a higher threshold (
l = mid + 1). - Otherwise, try a lower threshold (
r = mid - 1).
- Run BFS/DFS to check if we can reach
- Return the highest valid threshold found.
class Solution:
def maximumSafenessFactor(self, grid):
N = len(grid)
minDist = grid
directions = [0, 1, 0, -1, 0]
q = deque()
for r in range(N):
for c in range(N):
if grid[r][c] == 1:
q.append(r * N + c)
minDist[r][c] = 0
else:
minDist[r][c] = -1
while q:
node = q.popleft()
r, c = divmod(node, N)
for i in range(4):
r2, c2 = r + directions[i], c + directions[i + 1]
if 0 <= r2 < N and 0 <= c2 < N and minDist[r2][c2] == -1:
minDist[r2][c2] = minDist[r][c] + 1
q.append(r2 * N + c2)
def canReach(threshold):
q = deque([0])
visited = [False] * (N * N)
visited[0] = True
while q:
node = q.popleft()
r, c = divmod(node, N)
if r == N - 1 and c == N - 1:
return True
for i in range(4):
r2, c2 = r + directions[i], c + directions[i + 1]
node2 = r2 * N + c2
if (0 <= r2 < N and 0 <= c2 < N and not visited[node2] and
minDist[r2][c2] >= threshold
):
visited[node2] = True
q.append(node2)
return False
l, r = 0, min(minDist[0][0], minDist[N - 1][N - 1])
while l <= r:
mid = (l + r) // 2
if canReach(mid):
l = mid + 1
else:
r = mid - 1
return l - 1Time & Space Complexity
- Time complexity:
- Space complexity:
4. Breadth First Search (0-1 BFS)
Intuition
0-1 BFS is an optimization when edge weights are only 0 or 1. Here we adapt it: moving to a neighbor with the same or better safeness has cost 0, while moving to a neighbor with worse safeness has cost 1 (we are forced to accept a lower minimum). By adding zero-cost moves to the front and cost-1 moves to the back of a deque, we process cells in order of decreasing safeness factor.
Algorithm
- Precompute
minDistusing multi-source BFS. - Initialize
safeFactor[0] = min(minDist[0][0], minDist[N-1][N-1])and track the running resultres. - Use a deque starting with cell
0. - For each cell popped:
- Update
res = min(res, safeFactor[node]). - If destination reached, break.
- For each unvisited neighbor:
- Compute its safeness as
min(safeFactor[current], minDist[neighbor]). - If it maintains the current result, add to front (zero cost); otherwise add to back.
- Compute its safeness as
- Update
- Return
res.
class Solution:
def maximumSafenessFactor(self, grid):
N = len(grid)
minDist = grid
directions = [0, 1, 0, -1, 0]
q = deque()
for r in range(N):
for c in range(N):
if grid[r][c] == 1:
q.append(r * N + c)
minDist[r][c] = 0
else:
minDist[r][c] = -1
while q:
node = q.popleft()
r, c = divmod(node, N)
for i in range(4):
r2, c2 = r + directions[i], c + directions[i + 1]
if 0 <= r2 < N and 0 <= c2 < N and minDist[r2][c2] == -1:
minDist[r2][c2] = minDist[r][c] + 1
q.append(r2 * N + c2)
safeFactor = [-1] * (N * N)
res = safeFactor[0] = min(minDist[N - 1][N - 1], minDist[0][0])
q.append(0)
while q:
node = q.popleft()
r, c = divmod(node, N)
res = min(res, safeFactor[node])
if r == N - 1 and c == N - 1:
break
for i in range(4):
r2, c2 = r + directions[i], c + directions[i + 1]
node2 = r2 * N + c2
if 0 <= r2 < N and 0 <= c2 < N and safeFactor[node2] == -1:
safeFactor[node2] = min(safeFactor[node], minDist[r2][c2])
if safeFactor[node2] < res:
q.append(node2)
else:
q.appendleft(node2)
return resTime & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Using Single-Source BFS Instead of Multi-Source BFS
To compute the minimum distance from each cell to the nearest thief, you must start BFS from all thief cells simultaneously. A common mistake is running separate BFS from each thief and taking the minimum, which results in O(n^4) time complexity instead of O(n^2).
Confusing Min-Heap with Max-Heap in Dijkstra
Since we want to maximize the minimum distance (safeness factor), we need a max-heap to always process the path with the highest current safeness first. Using a min-heap (standard Dijkstra) will find the path with the lowest safeness instead, giving incorrect results.
Forgetting to Check Start and End Cell Constraints
The safeness of any path is bounded by the minimum distance at both the start cell (0,0) and the destination cell (N-1, N-1). If either cell contains a thief (distance 0), the maximum safeness is 0 regardless of the path taken. Some solutions skip this edge case check.
Sign in to join the discussion