2812. Find the Safest Path in a Grid - Explanation

Problem Link



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

  1. Run multi-source BFS from all cells containing thieves to compute minDist[r][c] for every cell.
  2. Use a max-heap starting from (0, 0) with priority equal to minDist[0][0].
  3. Maintain a visited set to avoid reprocessing cells.
  4. 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.
  5. 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: O(n2logn)O(n ^ 2 \log n)
  • Space complexity: O(n2)O(n ^ 2)

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

  1. Overwrite the input grid: set thief cells to 0 and others to -1.
  2. Run multi-source BFS to fill each cell with its distance to the nearest thief.
  3. Initialize a safeFactor array to track the best safeness to each cell.
  4. Use a max-heap starting from cell 0 with initial safeness minDist[0][0].
  5. 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.
  6. Return 0 if 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 0

Time & Space Complexity

  • Time complexity: O(n2logn)O(n ^ 2 \log n)
  • Space complexity: O(n2)O(n ^ 2)

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

  1. Precompute minDist for all cells using multi-source BFS from thieves.
  2. Binary search on the safeness factor in range [0, min(minDist[0][0], minDist[N-1][N-1])].
  3. For each candidate mid:
    • Run BFS/DFS to check if we can reach (N-1, N-1) from (0, 0) using only cells with minDist >= mid.
    • If reachable, try a higher threshold (l = mid + 1).
    • Otherwise, try a lower threshold (r = mid - 1).
  4. 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 - 1

Time & Space Complexity

  • Time complexity: O(n2logn)O(n ^ 2 \log n)
  • Space complexity: O(n2)O(n ^ 2)

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

  1. Precompute minDist using multi-source BFS.
  2. Initialize safeFactor[0] = min(minDist[0][0], minDist[N-1][N-1]) and track the running result res.
  3. Use a deque starting with cell 0.
  4. 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.
  5. 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 res

Time & Space Complexity

  • Time complexity: O(n2)O(n ^ 2)
  • Space complexity: O(n2)O(n ^ 2)

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.