Shortest Path in Binary Matrix

Medium BFS

Problem

Given an n x n binary matrix grid, return the length of the shortest clear path from top-left to bottom-right. If no such path exists, return -1. A clear path is a sequence of cells where each adjacent pair is connected 8-directionally and all cells have value 0.

  • n == grid.length
  • n == grid[i].length
  • 1 ≤ n ≤ 100
  • grid[i][j] is 0 or 1

Example

Input: grid = [[0,0,0],[1,1,0],[1,1,0]]
Output: 4

Starting at (0,0) with distance 1, BFS expands outward layer by layer. First, it visits (0,1). Next layer includes (0,2) and (1,2). From (1,2), BFS reaches (2,2) at distance 4, which is the bottom-right target. Because BFS explores all cells at each distance before moving deeper, the first time the target is reached guarantees the shortest path length.

Approach

Straightforward Solution

A Depth-First Search (DFS) could find a path but does not guarantee shortest length and would require exploring all paths, leading to exponential time complexity.

Core Observation

The shortest path in a grid with uniform step cost is found by exploring neighbors in layers, ensuring the first time a cell is reached is via the shortest path.

Path to Optimal

Recognizing the problem as shortest path on an unweighted graph suggests BFS. BFS explores nodes in order of increasing distance from the start, so the first time the target is reached, the shortest path length is known. Using a queue and a visited set prevents revisiting nodes and infinite loops.

Optimal Approach

Implement BFS starting from (0,0) with distance 1. Use a queue to explore all 8-directional neighbors that are within bounds, unblocked, and unvisited. Mark neighbors as visited when enqueued to avoid duplicates. Return the distance upon reaching the target cell. If the queue empties without reaching the target, return -1.

Time

O(n^2)

Each cell in the n x n grid is visited at most once. Each visit involves checking up to 8 neighbors, resulting in O(n^2) operations.

Space

O(n^2)

The visited set and queue can hold up to O(n^2) cells in the worst case, which is necessary to track explored states and maintain BFS order.

Pattern Spotlight

BFS (Shortest Path on Unweighted Graph)

When a problem asks for the shortest path or minimum steps on a grid or graph with uniform edge weights, BFS guarantees the shortest path by exploring nodes in order of increasing distance, allowing immediate termination upon reaching the target.

Solution

1from collections import deque
2
3class Solution:
4 def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
5 if grid[0][0] == 1:
6 return -1
7
8 def valid(row, col):
9 return 0 <= row < n and 0 <= col < n and grid[row][col] == 0
10
11 n = len(grid)
12 seen = {(0, 0)}
13 queue = deque([(0, 0, 1)])
14 directions = [(0, 1), (1, 0), (1, 1), (-1, -1), (-1, 1), (1, -1), (0, -1), (-1, 0)]
15
16 while queue:
17 row, col, steps = queue.popleft()
18 if (row, col) == (n - 1, n - 1):
19 return steps
20
21 for dx, dy in directions:
22 next_row, next_col = row + dy, col + dx
23 if valid(next_row, next_col) and (next_row, next_col) not in seen:
24 seen.add((next_row, next_col))
25 queue.append((next_row, next_col, steps + 1))
26
27 return -1

Step-by-Step Solution

1

Handle Blocked Start Cell Edge Case

5if grid[0][0] == 1:
6 return -1

Objective

To immediately return -1 if the starting cell is blocked, as no path can begin.

Key Insight

If the top-left cell is blocked (value 1), no path can start, so the algorithm can terminate early without further computation. This early exit handles an important edge case efficiently.

Interview Quick-Check

State & Boundaries

Check the start cell before any processing because a blocked start means no valid path exists.

2

Validate Grid Coordinates and Blockage

8def valid(row, col):
9 return 0 <= row < n and 0 <= col < n and grid[row][col] == 0

Objective

To verify whether a candidate neighbor cell is inside the grid boundaries and unblocked before BFS explores it.

Key Insight

Movement is allowed only to cells that are within the n x n grid and have value 0. Encapsulating these checks in a helper function improves clarity, ensures consistent validation, and prevents out-of-bounds access.

Interview Quick-Check

Core Logic

A valid cell must be inside the grid and contain a 0.

Common Pitfalls & Bugs

Boundary checks must occur before reading grid[row][col] to avoid index errors.

Complexity

This validation runs O(1) per neighbor and ensures BFS only explores legal cells.

3

Initialize BFS State and Movement Directions

11n = len(grid)
12seen = {(0, 0)}
13queue = deque([(0, 0, 1)])
14directions = [(0, 1), (1, 0), (1, 1), (-1, -1), (-1, 1), (1, -1), (0, -1), (-1, 0)]

Objective

To set up the grid size, visited set, BFS queue with starting position and distance, and all 8 possible movement directions.

Key Insight

BFS requires a queue to track the frontier and a visited set to avoid revisiting cells. Initializing with the start cell at distance 1 sets the baseline. Defining all 8 directions upfront simplifies neighbor exploration and ensures diagonal moves are considered.

Interview Quick-Check

Core Logic

The queue stores tuples of (row, col, steps) to track position and path length. The visited set prevents cycles and redundant processing.

Common Pitfalls & Bugs

Forgetting to mark the start cell as visited or to include diagonal directions can cause incorrect results or infinite loops.

4

Explore Neighbors Layer-by-Layer to Find Shortest Path

16while queue:
17 row, col, steps = queue.popleft()
18 if (row, col) == (n - 1, n - 1):
19 return steps
21 for dx, dy in directions:
22 next_row, next_col = row + dy, col + dx
23 if valid(next_row, next_col) and (next_row, next_col) not in seen:
24 seen.add((next_row, next_col))
25 queue.append((next_row, next_col, steps + 1))

Objective

To perform BFS by dequeuing cells, checking for the target, and enqueueing valid, unvisited neighbors with incremented distance.

Key Insight

BFS explores all cells reachable in the current number of steps before moving to cells reachable in more steps. This layer-by-layer expansion guarantees that the first time the target is reached, the path length is minimal. Valid neighbors are those within bounds, unblocked, and not yet visited, ensuring correctness and efficiency.

Interview Quick-Check

Core Logic

Dequeue a cell, check if it is the target, then enqueue all valid neighbors with steps + 1, marking them visited immediately to prevent duplicates.

State & Boundaries

The loop continues until the queue is empty or the target is found, ensuring exhaustive but efficient exploration.

Common Pitfalls & Bugs

Incorrect neighbor coordinate calculation or delayed marking of visited cells can cause revisiting nodes or infinite loops.

5

Return -1 When No Path Exists

27return -1

Objective

To return -1 if BFS completes without reaching the target, indicating no valid path exists.

Key Insight

If the queue empties without finding the target, all reachable cells have been explored and the target is unreachable. Returning -1 signals this impossibility clearly and efficiently.

Interview Quick-Check

State & Boundaries

This return is only reached if the BFS exhausts all reachable cells without encountering the target.

Line Analysis

This solution has 6 Critical lines interviewers watch for.

Line 18 Critical
if (row, col) == (n - 1, n - 1):

Check if the current cell is the target cell.

Identifying the target cell allows immediate termination with the shortest path length, leveraging BFS's layer order.

Line 5 Critical
if grid[0][0] == 1:

Check if the starting cell is blocked.

If the start cell is blocked, no path can begin, so the algorithm must terminate immediately to avoid unnecessary computation.

Line 13 Critical
queue = deque([(0, 0, 1)])

Initialize the BFS queue with the starting cell and initial distance 1.

Starting BFS with the start cell at distance 1 sets the baseline for path length counting and exploration order.

Line 17 Critical
row, col, steps = queue.popleft()

Dequeue the next cell and its current path length.

FIFO dequeue maintains BFS order, guaranteeing layer-by-layer exploration and shortest path discovery.

Line 19 Critical
return steps

Return the number of steps taken to reach the target.

Returning here ensures the shortest path length is reported as soon as the target is reached, which BFS guarantees is minimal.

Line 23 Critical
if valid(next_row, next_col) and (next_row, next_col) not in seen:

Check if the neighbor is valid and unvisited.

This condition ensures BFS only explores cells that are within bounds, unblocked, and not yet visited, preventing cycles and invalid paths.

Line 6 Core
return -1

Return -1 to indicate no path exists from the start.

Returning early here prevents the BFS from running on an impossible input, improving efficiency and correctness.

Line 9 Core
return 0 <= row < n and 0 <= col < n and grid[row][col] == 0

Return whether the cell is within bounds and unblocked.

This condition enforces the problem constraints that only cells with value 0 and valid indices can be traversed.

Line 12 Core
seen = {(0, 0)}

Initialize the visited set with the starting cell.

Marking the start cell as visited prevents revisiting it and infinite loops during BFS.

Line 14 Core
directions = [(0, 1), (1, 0), (1, 1), (-1, -1), (-1, 1), (1, -1), (0, -1), (-1, 0)]

Define all 8 possible movement directions including diagonals.

Including diagonal directions is essential to correctly model the problem's allowed moves and find the shortest path.

Line 24 Core
seen.add((next_row, next_col))

Mark the neighbor cell as visited.

Marking visited upon enqueueing prevents multiple enqueues of the same cell, ensuring O(n^2) time complexity and correctness.

Line 25 Core
queue.append((next_row, next_col, steps + 1))

Enqueue the neighbor cell with incremented path length.

Adding neighbors to the queue with steps + 1 schedules them for future exploration at the correct distance layer.

Line 8 Core
def valid(row, col):

Define a helper function to validate cell coordinates and blockage.

Encapsulating boundary and blockage checks in a function improves code clarity and ensures consistent validation logic.

Line 16 Core
while queue:

Begin BFS loop, processing cells until queue is empty.

The loop ensures all reachable cells are explored in order of increasing distance, which is fundamental to BFS correctness.

Line 22 Core
next_row, next_col = row + dy, col + dx

Calculate the coordinates of the neighbor cell.

Correctly computing neighbor coordinates is essential for valid traversal and avoiding errors.

Line 27 Core
return -1

Return -1 if no path to the target exists after BFS completes.

If BFS exhausts all reachable cells without finding the target, returning -1 correctly signals the absence of any valid path.

Line 21 Standard
for dx, dy in directions:

Iterate over all 8 possible neighbor directions.

Exploring all neighbors is necessary to fully traverse the grid and find the shortest path.

Line 11 Standard
n = len(grid)

Store the grid dimension for boundary checks.

Caching the grid size avoids repeated calls to len(grid) and simplifies boundary validation.

Test Your Understanding

Why does BFS guarantee that the first time the target cell is reached, the shortest path length has been found?

Because BFS explores nodes in layers of increasing distance from the start, it visits all nodes at distance d before any at distance d+1. Therefore, the first time the target is dequeued, it is reached via the shortest possible path.

Related Problems

BFS pattern

Don't just read it. Drill it.

Reconstruct Shortest Path in Binary Matrix from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Shortest Path in Binary Matrix drill

or drill a free problem