LeetCode 1091 - Shortest Path in Binary Matrix
The problem asks us to find the shortest path from the top-left corner (0, 0) to the bottom-right corner (n-1, n-1) of a given n x n binary matrix grid. A cell with a value of 0 is passable, while a cell with a value of 1 is blocked.
Difficulty: 🟡 Medium
Topics: Array, Breadth-First Search, Matrix
Solution
Problem Understanding
The problem asks us to find the shortest path from the top-left corner (0, 0) to the bottom-right corner (n-1, n-1) of a given n x n binary matrix grid. A cell with a value of 0 is passable, while a cell with a value of 1 is blocked. The path can move in any of the eight directions: up, down, left, right, and the four diagonals. The length of the path is the total number of cells visited, including both the start and end cells. If no path exists, we must return -1.
The input grid represents the terrain of the matrix, and the output is the integer length of the shortest path. Constraints tell us that 1 <= n <= 100, which means a matrix can have up to 10,000 cells, so a brute-force search that explores every path recursively would be too slow. Each cell can only be 0 or 1, simplifying the check for traversable paths.
Important edge cases include situations where either the start (0, 0) or end (n-1, n-1) cell is blocked, which immediately means no path exists. Another edge case is a matrix of size 1 x 1, which is trivially a path of length 1 if the only cell is 0.
Approaches
The brute-force approach would involve recursively exploring every possible path from (0, 0) to (n-1, n-1), backtracking whenever a cell is blocked or already visited. This guarantees correctness but is too slow, because the number of possible paths grows exponentially with n. The time complexity would be O(2^(n^2)), which is infeasible for n = 100.
The optimal solution leverages Breadth-First Search (BFS). BFS is ideal because it explores all paths in increasing order of length, ensuring that the first time we reach the bottom-right corner, it is along the shortest possible path. We maintain a queue to store cells to visit and mark cells as visited by updating the grid or using a separate visited matrix. BFS guarantees that we do not revisit cells unnecessarily.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^(n^2)) | O(n^2) | Recursively explore all paths, too slow for n=100 |
| Optimal (BFS) | O(n^2) | O(n^2) | Level-order traversal finds shortest path efficiently |
Algorithm Walkthrough
- Check if the start
(0, 0)or end(n-1, n-1)is blocked. If either is1, return-1. - Initialize a queue for BFS and add the starting cell
(0, 0)with an initial path length of1. - Create a list of 8 directions representing all possible moves from a cell: up, down, left, right, and the four diagonals.
- While the queue is not empty, pop the front element
(r, c, path_length). - If
(r, c)is the bottom-right corner, returnpath_length. - For each of the 8 directions, calculate the neighbor cell
(nr, nc). Check if it is within bounds and passable (value0). - Mark the neighbor as visited (e.g., set
grid[nr][nc] = 1) and add(nr, nc, path_length + 1)to the queue. - If the queue is exhausted without reaching the target, return
-1.
Why it works: BFS explores all cells in increasing order of path length. By marking cells visited, we avoid cycles and redundant work. When we reach the destination, the path length is guaranteed to be minimal.
Python Solution
from collections import deque
from typing import List
class Solution:
def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
n = len(grid)
if grid[0][0] != 0 or grid[n-1][n-1] != 0:
return -1
directions = [(-1, -1), (-1, 0), (-1, 1),
(0, -1), (0, 1),
(1, -1), (1, 0), (1, 1)]
queue = deque([(0, 0, 1)]) # row, col, path_length
grid[0][0] = 1 # mark visited
while queue:
r, c, length = queue.popleft()
if r == n - 1 and c == n - 1:
return length
for dr, dc in directions:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] == 0:
grid[nr][nc] = 1
queue.append((nr, nc, length + 1))
return -1
The code first checks for blocked start or end cells. The BFS queue stores each cell with the current path length. Each step, neighbors are calculated and added to the queue if valid, and cells are marked visited to prevent revisiting. The first time the target is reached, the shortest path length is returned.
Go Solution
package main
func shortestPathBinaryMatrix(grid [][]int) int {
n := len(grid)
if grid[0][0] != 0 || grid[n-1][n-1] != 0 {
return -1
}
directions := [8][2]int{
{-1, -1}, {-1, 0}, {-1, 1},
{0, -1}, {0, 1},
{1, -1}, {1, 0}, {1, 1},
}
type cell struct {
r, c, length int
}
queue := []cell{{0, 0, 1}}
grid[0][0] = 1
for len(queue) > 0 {
curr := queue[0]
queue = queue[1:]
if curr.r == n-1 && curr.c == n-1 {
return curr.length
}
for _, d := range directions {
nr, nc := curr.r+d[0], curr.c+d[1]
if nr >= 0 && nr < n && nc >= 0 && nc < n && grid[nr][nc] == 0 {
grid[nr][nc] = 1
queue = append(queue, cell{nr, nc, curr.length + 1})
}
}
}
return -1
}
The Go implementation mirrors the Python solution. We define a cell struct to store row, column, and path length. We use a slice as a queue, popping the first element and appending neighbors. Cells are marked visited by setting them to 1.
Worked Examples
Example 1: grid = [[0,1],[1,0]]
| Step | Queue | Current Cell | Action |
|---|---|---|---|
| 1 | [(0,0,1)] | (0,0) | Add (1,1,2) |
| 2 | [(1,1,2)] | (1,1) | Reached target, return 2 |
Example 2: grid = [[0,0,0],[1,1,0],[1,1,0]]
| Step | Queue | Current Cell | Action |
|---|---|---|---|
| 1 | [(0,0,1)] | (0,0) | Add (0,1,2),(1,0 blocked),(1,1 blocked),(1,1 diagonal blocked) |
| 2 | [(0,1,2),(0,2,2)] | (0,1) | Add (1,2,3) |
| 3 | [(0,2,2),(1,2,3)] | (0,2) | Add (1,2 already queued),(1,3 out of bounds),(1,1 blocked) |
| 4 | [(1,2,3)] | (1,2) | Add (2,2,4) |
| 5 | [(2,2,4)] | (2,2) | Reached target, return 4 |
Example 3: grid = [[1,0,0],[1,1,0],[1,1,0]]
| Step | Queue | Current Cell | Action |
|---|---|---|---|
| 1 | [] | - | Start blocked, return -1 |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n^2) | Each cell is visited at most once, 8 neighbors checked each time |
| Space | O(n^2) | BFS queue can grow up to the size of the grid |
Since BFS explores each cell only once, the algorithm is efficient for the maximum grid size 100 x 100.
Test Cases
# provided examples
assert Solution().shortestPathBinaryMatrix([[0,1],[1,0]]) == 2 # shortest path exists
assert Solution().shortestPathBinaryMatrix([[0,0,0],[1,1,0],[1,