1091. Shortest Path in Binary Matrix
Problem Description
You are given an n x n
binary matrix grid
where each cell contains either a 0
or a 1
. Your task is to find the length of the shortest clear path from the top-left corner (0, 0)
to the bottom-right corner (n-1, n-1)
.
A clear path must satisfy these conditions:
- Every cell visited along the path must contain a
0
- You can move in 8 directions from any cell - horizontally, vertically, and diagonally (to any of the 8 neighboring cells that share an edge or corner)
- The path starts at cell
(0, 0)
and ends at cell(n-1, n-1)
The length of a clear path is counted as the total number of cells visited along the path, including both the starting and ending cells.
If no valid clear path exists from the top-left to the bottom-right corner, return -1
.
For example:
- If the starting cell
(0, 0)
contains a1
, no path is possible, so return-1
- If the ending cell
(n-1, n-1)
contains a1
, no path can reach it, so return-1
- The minimum possible path length is
1
(whenn = 1
and the single cell contains0
)
The solution uses BFS (Breadth-First Search) to explore all possible paths level by level, ensuring the first path found to the destination is the shortest. It marks visited cells by changing their value from 0
to 1
to avoid revisiting them, and tracks the path length by counting the number of levels traversed in the BFS.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The binary matrix can be viewed as a graph where each cell is a node, and edges connect to the 8 adjacent cells (if they contain 0s).
Is it a tree?
- No: The graph is not a tree because there can be multiple paths between cells, and cycles are possible.
Is the problem related to directed acyclic graphs?
- No: The graph is undirected (you can move in any of the 8 directions from any cell), and it may contain cycles.
Is the problem related to shortest paths?
- Yes: We need to find the shortest clear path from the top-left corner to the bottom-right corner.
Is the graph Weighted?
- No: All moves between adjacent cells have the same cost (each step counts as 1 in the path length). The graph is unweighted.
BFS
- Yes: Since we need to find the shortest path in an unweighted graph, BFS is the optimal choice.
Conclusion: The flowchart suggests using BFS (Breadth-First Search) for finding the shortest path in this unweighted grid graph. BFS explores cells level by level, guaranteeing that the first time we reach the destination, we've found the shortest path.
Intuition
When we need to find the shortest path in a grid, we should think about how to explore all possible paths systematically. The key insight is that this problem is essentially a graph traversal problem where each cell with value 0
is a node, and we can move to any of its 8 neighboring cells.
Why BFS over DFS? Consider how these algorithms explore paths:
- DFS would go deep into one path completely before backtracking to try another path. It might find a valid path, but not necessarily the shortest one.
- BFS explores all paths level by level - first all cells at distance 1, then all cells at distance 2, and so on. This guarantees that the first time we reach the destination, we've found the shortest path.
Think of it like ripples in water - when you drop a stone at the starting point (0, 0)
, the ripples spread outward uniformly in all directions. The first ripple that reaches the destination (n-1, n-1)
represents the shortest path.
The 8-directional movement means we treat diagonal moves the same as horizontal/vertical moves (each counts as 1 step). This is different from some maze problems where diagonal moves might cost more or aren't allowed at all.
To avoid revisiting cells and getting stuck in cycles, we need to mark cells as visited. A clever optimization is to reuse the input grid itself - we can mark visited cells by changing their value from 0
to 1
. This serves two purposes:
- Prevents revisiting the same cell (avoiding infinite loops)
- Ensures we only explore valid paths (cells with value
0
)
The BFS implementation uses a queue to process cells level by level. We track the current path length by counting how many levels we've traversed. Each level represents one additional step in our path, so when we finally reach the destination, the level count gives us the shortest path length.
Learn more about Breadth-First Search patterns.
Solution Approach
Let's walk through the BFS implementation step by step:
Initial Setup and Edge Case Handling:
First, we check if the starting cell grid[0][0]
contains a 1
. If it does, there's no valid path, so we immediately return -1
.
Initialization:
- Mark the starting cell as visited by setting
grid[0][0] = 1
- Initialize a queue (deque) with the starting position
(0, 0)
- Set
ans = 1
to track the path length (starting cell counts as 1)
BFS Level-by-Level Traversal: The main BFS loop continues while the queue is not empty:
-
Process Current Level: For each level, we process all cells currently in the queue using
for _ in range(len(q))
. This ensures we handle all cells at the current distance before moving to the next distance level. -
Check for Destination: For each cell
(i, j)
popped from the queue, we check if we've reached the bottom-right corner withif i == j == n - 1
. If yes, return the current path lengthans
. -
Explore 8 Neighbors: For the current cell, we explore all 8 adjacent cells using nested loops:
for x in range(i - 1, i + 2): for y in range(j - 1, j + 2):
This generates all combinations where
x
is in[i-1, i, i+1]
andy
is in[j-1, j, j+1]
, covering all 8 directions including diagonals. -
Validate and Mark Neighbors: For each neighbor
(x, y)
:- Check if it's within bounds:
0 <= x < n and 0 <= y < n
- Check if it's unvisited and valid:
grid[x][y] == 0
- If both conditions are met:
- Mark as visited:
grid[x][y] = 1
- Add to queue for next level:
q.append((x, y))
- Mark as visited:
- Check if it's within bounds:
-
Increment Path Length: After processing all cells at the current level, increment
ans += 1
to account for moving one step further.
Return Value:
If the queue becomes empty without reaching the destination, no valid path exists, so return -1
.
Time Complexity: O(nΒ²)
- In the worst case, we visit every cell in the n x n
grid once.
Space Complexity: O(nΒ²)
- The queue can contain at most O(nΒ²)
cells in the worst case when many cells are reachable at the same distance level.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the BFS solution approach.
Consider this 3Γ3 grid:
grid = [[0,0,0], [1,1,0], [1,1,0]]
Initial Setup:
- Check
grid[0][0]
= 0 β (valid starting point) - Mark start as visited:
grid[0][0] = 1
- Initialize queue with
(0,0)
andans = 1
Level 1 (ans = 1):
- Process
(0,0)
from queue - Check if
(0,0)
is destination(2,2)
? No - Explore 8 neighbors of
(0,0)
:(-1,-1)
: out of bounds, skip(-1,0)
: out of bounds, skip(-1,1)
: out of bounds, skip(0,-1)
: out of bounds, skip(0,0)
: already visited (value = 1), skip(0,1)
: valid and unvisited (value = 0), mark as 1, add to queue(1,-1)
: out of bounds, skip(1,0)
: value = 1, skip(1,1)
: value = 1, skip
- Queue now contains:
[(0,1)]
- Grid state after Level 1:
[[1,1,0], [1,1,0], [1,1,0]]
- Increment
ans = 2
Level 2 (ans = 2):
- Process
(0,1)
from queue - Check if
(0,1)
is destination(2,2)
? No - Explore 8 neighbors of
(0,1)
:- Only
(0,2)
is valid (unvisited with value 0) - Mark
grid[0][2] = 1
and add to queue
- Only
- Queue now contains:
[(0,2)]
- Grid state after Level 2:
[[1,1,1], [1,1,0], [1,1,0]]
- Increment
ans = 3
Level 3 (ans = 3):
- Process
(0,2)
from queue - Check if
(0,2)
is destination(2,2)
? No - Explore 8 neighbors of
(0,2)
:- Only
(1,2)
is valid (unvisited with value 0) - Mark
grid[1,2] = 1
and add to queue
- Only
- Queue now contains:
[(1,2)]
- Grid state after Level 3:
[[1,1,1], [1,1,1], [1,1,0]]
- Increment
ans = 4
Level 4 (ans = 4):
- Process
(1,2)
from queue - Check if
(1,2)
is destination(2,2)
? No - Explore 8 neighbors of
(1,2)
:- Only
(2,2)
is valid (unvisited with value 0) - Mark
grid[2][2] = 1
and add to queue
- Only
- Queue now contains:
[(2,2)]
- Grid state after Level 4:
[[1,1,1], [1,1,1], [1,1,1]]
- Increment
ans = 5
Level 5 (ans = 5):
- Process
(2,2)
from queue - Check if
(2,2)
is destination(2,2)
? Yes! - Return
ans = 5
The shortest path is: (0,0) β (0,1) β (0,2) β (1,2) β (2,2)
with length 5.
Note how BFS guarantees this is the shortest path - we explored all cells reachable in 1 step, then 2 steps, and so on. The first time we reached the destination, we had found the optimal solution.
Solution Implementation
1from collections import deque
2from typing import List
3
4class Solution:
5 def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
6 # Check if starting cell is blocked
7 if grid[0][0] == 1:
8 return -1
9
10 n = len(grid)
11
12 # Mark starting cell as visited
13 grid[0][0] = 1
14
15 # Initialize BFS queue with starting position
16 queue = deque([(0, 0)])
17
18 # Path length starts at 1
19 path_length = 1
20
21 # BFS to find shortest path
22 while queue:
23 # Process all cells at current distance level
24 level_size = len(queue)
25 for _ in range(level_size):
26 row, col = queue.popleft()
27
28 # Check if we've reached the destination
29 if row == n - 1 and col == n - 1:
30 return path_length
31
32 # Explore all 8 adjacent cells (including diagonals)
33 for next_row in range(row - 1, row + 2):
34 for next_col in range(col - 1, col + 2):
35 # Check if the cell is within bounds and unvisited (value 0)
36 if (0 <= next_row < n and
37 0 <= next_col < n and
38 grid[next_row][next_col] == 0):
39
40 # Mark cell as visited
41 grid[next_row][next_col] = 1
42
43 # Add to queue for next level exploration
44 queue.append((next_row, next_col))
45
46 # Increment path length after processing current level
47 path_length += 1
48
49 # No path found
50 return -1
51
1class Solution {
2 public int shortestPathBinaryMatrix(int[][] grid) {
3 // Check if starting cell is blocked
4 if (grid[0][0] == 1) {
5 return -1;
6 }
7
8 int n = grid.length;
9
10 // Mark starting cell as visited
11 grid[0][0] = 1;
12
13 // Initialize BFS queue with starting position
14 Deque<int[]> queue = new ArrayDeque<>();
15 queue.offer(new int[] {0, 0});
16
17 // BFS traversal with level tracking for shortest path
18 int pathLength = 1;
19 while (!queue.isEmpty()) {
20 // Process all cells at current distance level
21 int levelSize = queue.size();
22 for (int i = 0; i < levelSize; i++) {
23 int[] currentCell = queue.poll();
24 int row = currentCell[0];
25 int col = currentCell[1];
26
27 // Check if we've reached the destination
28 if (row == n - 1 && col == n - 1) {
29 return pathLength;
30 }
31
32 // Explore all 8 adjacent cells (including diagonals)
33 for (int nextRow = row - 1; nextRow <= row + 1; nextRow++) {
34 for (int nextCol = col - 1; nextCol <= col + 1; nextCol++) {
35 // Check if next cell is within bounds and unvisited
36 if (nextRow >= 0 && nextRow < n &&
37 nextCol >= 0 && nextCol < n &&
38 grid[nextRow][nextCol] == 0) {
39
40 // Mark as visited to avoid revisiting
41 grid[nextRow][nextCol] = 1;
42
43 // Add to queue for next level processing
44 queue.offer(new int[] {nextRow, nextCol});
45 }
46 }
47 }
48 }
49 // Increment path length after processing current level
50 pathLength++;
51 }
52
53 // No path found to destination
54 return -1;
55 }
56}
57
1class Solution {
2public:
3 int shortestPathBinaryMatrix(vector<vector<int>>& grid) {
4 // Check if starting cell is blocked
5 if (grid[0][0] == 1) {
6 return -1;
7 }
8
9 int n = grid.size();
10
11 // Mark starting cell as visited
12 grid[0][0] = 1;
13
14 // BFS queue to store coordinates
15 queue<pair<int, int>> bfsQueue;
16 bfsQueue.emplace(0, 0);
17
18 // Track path length (starting from 1)
19 int pathLength = 1;
20
21 // BFS traversal
22 while (!bfsQueue.empty()) {
23 // Process all nodes at current level
24 int currentLevelSize = bfsQueue.size();
25
26 for (int i = 0; i < currentLevelSize; ++i) {
27 // Get current cell coordinates
28 auto [row, col] = bfsQueue.front();
29 bfsQueue.pop();
30
31 // Check if we reached the destination
32 if (row == n - 1 && col == n - 1) {
33 return pathLength;
34 }
35
36 // Explore all 8 adjacent cells
37 for (int newRow = row - 1; newRow <= row + 1; ++newRow) {
38 for (int newCol = col - 1; newCol <= col + 1; ++newCol) {
39 // Check if the new position is valid and unvisited
40 if (newRow >= 0 && newRow < n &&
41 newCol >= 0 && newCol < n &&
42 grid[newRow][newCol] == 0) {
43
44 // Mark as visited
45 grid[newRow][newCol] = 1;
46
47 // Add to queue for next level
48 bfsQueue.emplace(newRow, newCol);
49 }
50 }
51 }
52 }
53
54 // Increment path length after processing current level
55 ++pathLength;
56 }
57
58 // No valid path found
59 return -1;
60 }
61};
62
1/**
2 * Finds the shortest path in a binary matrix from top-left to bottom-right
3 * @param grid - 2D binary matrix where 0 represents an open cell and 1 represents a blocked cell
4 * @returns The length of the shortest clear path, or -1 if no path exists
5 */
6function shortestPathBinaryMatrix(grid: number[][]): number {
7 // Check if starting position is blocked
8 if (grid[0][0] === 1) {
9 return -1;
10 }
11
12 const gridSize = grid.length;
13 const targetRow = gridSize - 1;
14 const targetCol = gridSize - 1;
15
16 // Mark starting position as visited
17 grid[0][0] = 1;
18
19 // Initialize BFS queue with starting position
20 let currentQueue: number[][] = [[0, 0]];
21
22 // BFS traversal - each iteration represents one step in the path
23 for (let pathLength = 1; currentQueue.length > 0; pathLength++) {
24 const nextQueue: number[][] = [];
25
26 // Process all cells at current distance
27 for (const [row, col] of currentQueue) {
28 // Check if we've reached the destination
29 if (row === targetRow && col === targetCol) {
30 return pathLength;
31 }
32
33 // Explore all 8 adjacent cells (including diagonals)
34 for (let nextRow = row - 1; nextRow <= row + 1; nextRow++) {
35 for (let nextCol = col - 1; nextCol <= col + 1; nextCol++) {
36 // Check if cell is valid and unvisited (value is 0)
37 if (grid[nextRow]?.[nextCol] === 0) {
38 // Mark cell as visited
39 grid[nextRow][nextCol] = 1;
40 // Add to next level of BFS
41 nextQueue.push([nextRow, nextCol]);
42 }
43 }
44 }
45 }
46
47 // Move to next level of BFS
48 currentQueue = nextQueue;
49 }
50
51 // No path found
52 return -1;
53}
54
Time and Space Complexity
Time Complexity: O(nΒ²)
The algorithm uses BFS to traverse the grid. In the worst case, we visit each cell at most once. Since there are n Γ n
cells in the grid, and for each cell we explore up to 8 neighbors (constant time), the total time complexity is O(nΒ²)
.
Breaking down the operations:
- The outer while loop runs until the queue is empty
- Each cell
(i, j)
is added to the queue at most once (since we mark it as visited by settinggrid[x][y] = 1
) - For each cell, we check at most 8 adjacent cells (3Γ3 grid centered at current cell minus the cell itself)
- Checking boundaries and adding to queue are
O(1)
operations
Therefore: O(nΒ² Γ 8) = O(nΒ²)
Space Complexity: O(nΒ²)
The space complexity consists of:
- Queue space: In the worst case, the queue can contain
O(nΒ²)
elements. This happens when we have a large connected component of zeros, and at some level of BFS, many cells are in the queue simultaneously. The maximum would be when cells form a diagonal pattern, potentially havingO(nΒ²)
cells in the queue. - We modify the input grid in-place for marking visited cells, so no additional space is needed for tracking visited cells.
- Other variables (
ans
,i
,j
,x
,y
) useO(1)
space.
Therefore, the overall space complexity is O(nΒ²)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Modifying the Input Grid Without Restoration
The Problem:
The solution modifies the input grid
directly by marking visited cells with 1
. This is a destructive operation that permanently changes the input data. If the caller needs to use the grid again after calling this function, they'll find it has been modified, which can lead to unexpected bugs.
Example Scenario:
grid = [[0,0,0],[1,1,0],[1,1,0]] solution = Solution() result1 = solution.shortestPathBinaryMatrix(grid) # Returns 4 result2 = solution.shortestPathBinaryMatrix(grid) # Returns -1 (grid is now all 1s!)
Solution: Create a separate visited tracking structure instead of modifying the input:
def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
if grid[0][0] == 1:
return -1
n = len(grid)
visited = set()
visited.add((0, 0))
queue = deque([(0, 0)])
path_length = 1
while queue:
level_size = len(queue)
for _ in range(level_size):
row, col = queue.popleft()
if row == n - 1 and col == n - 1:
return path_length
for next_row in range(row - 1, row + 2):
for next_col in range(col - 1, col + 2):
if (0 <= next_row < n and
0 <= next_col < n and
grid[next_row][next_col] == 0 and
(next_row, next_col) not in visited):
visited.add((next_row, next_col))
queue.append((next_row, next_col))
path_length += 1
return -1
2. Not Checking if the Destination Cell is Blocked
The Problem:
While the code checks if the starting cell is blocked, it doesn't explicitly check if the destination cell grid[n-1][n-1]
is blocked before starting the BFS. This can lead to unnecessary computation when the destination is unreachable.
Solution: Add an early check for both start and end cells:
def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
n = len(grid)
# Check if start or end is blocked
if grid[0][0] == 1 or grid[n-1][n-1] == 1:
return -1
# Handle single cell grid
if n == 1:
return 1
# Continue with BFS...
3. Inefficient Neighbor Generation Including Current Cell
The Problem:
The nested loops for x in range(i - 1, i + 2): for y in range(j - 1, j + 2):
generate 9 positions including the current cell (i, j)
. Since the current cell is already visited, checking it again is wasteful.
Solution: Skip the current cell explicitly:
for next_row in range(row - 1, row + 2):
for next_col in range(col - 1, col + 2):
# Skip the current cell
if next_row == row and next_col == col:
continue
if (0 <= next_row < n and
0 <= next_col < n and
grid[next_row][next_col] == 0):
# Process neighbor...
Or use a predefined directions array:
directions = [(-1,-1), (-1,0), (-1,1), (0,-1), (0,1), (1,-1), (1,0), (1,1)] for dx, dy in directions: next_row, next_col = row + dx, col + dy if (0 <= next_row < n and 0 <= next_col < n and grid[next_row][next_col] == 0): # Process neighbor...
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!