Problem Walkthrough

Shortest Path Binary Matrix LeetCode 1091

Solve LeetCode 1091 Shortest Path in Binary Matrix using standard BFS that explores all 8 directions including diagonals, guaranteed to find the shortest clear path in an unweighted grid because BFS processes cells level by level and the first time it reaches the destination is provably optimal.

8 min read|

Shortest Path Binary Matrix

LeetCode 1091

Problem Overview — Binary Grid, 8 Directions, Clear Path

LeetCode 1091 Shortest Path in Binary Matrix gives you an n x n grid where each cell contains either 0 (clear) or 1 (blocked). You must find the shortest clear path from the top-left cell (0, 0) to the bottom-right cell (n-1, n-1). A clear path consists only of cells with value 0, and you may move in all 8 directions: up, down, left, right, and all four diagonals. The length of the path is measured by the number of cells visited, including the start and end cells.

If no clear path exists — because the start or end cell is blocked, or because the grid is fully surrounded by 1-cells — you must return -1. For a 1x1 grid where grid[0][0] is 0, the answer is 1 because the start is also the destination. The inclusion of diagonal movement is the defining feature of this problem, distinguishing it from standard 4-direction grid BFS problems like Number of Islands.

The key observation is that this is an unweighted graph problem: every cell-to-cell transition costs exactly 1, regardless of direction. This means BFS — which processes nodes layer by layer at increasing distances — is the ideal algorithm. Dijkstra with a priority queue would also work but is unnecessary overhead when all edge weights are equal.

  • Given: n x n binary grid where 0 = clear cell, 1 = blocked cell
  • Start: (0, 0) top-left corner; end: (n-1, n-1) bottom-right corner
  • Movement: 8 directions — horizontal, vertical, and diagonal
  • Path: sequence of adjacent 0-cells from start to end
  • Return: length of shortest clear path (number of cells), or -1 if no path exists
  • Path length counts cells, not edges — starting cell contributes 1 to the count
  • Constraints: 1 <= n <= 100, grid[i][j] is 0 or 1

Why BFS Gives the Shortest Path

BFS explores the graph in concentric waves radiating outward from the source. In the first wave it visits all cells reachable in exactly 1 step. In the second wave it visits all cells reachable in exactly 2 steps. In general, when BFS first reaches any cell, it has done so via the shortest possible path. This level-by-level expansion is the fundamental reason BFS yields optimal results for unweighted shortest paths.

The formal argument is by contradiction: if BFS first reached the destination at distance d but there existed a shorter path of length d' < d, then BFS would have visited the destination during wave d' instead of wave d, which contradicts the assumption that BFS first reached it at wave d. This argument holds as long as all transitions have equal cost — the defining property of an unweighted graph.

DFS does not have this property. DFS follows a single branch as deep as it can go, potentially finding a very long path to the destination before it backtracks to explore shorter alternatives. DFS can tell you whether a path exists, but not that the first path found is the shortest. For shortest-path queries on unweighted graphs, BFS is the canonical and correct choice.

💡

BFS = Shortest Path in Unweighted Graphs

BFS guaranteeing the shortest path in unweighted graphs is a fundamental fact of graph theory, not just a trick. The level-by-level expansion means the first time BFS reaches any node, it has done so via the minimum number of edges. This property holds for any graph — grids, trees, general adjacency lists — as long as all edge weights are equal. Memorizing this principle unlocks a large family of grid shortest-path problems where BFS is always the right starting point.

BFS Implementation — Queue, Distance Tracking, Visited

The BFS implementation initializes a queue with the starting cell (0, 0) at distance 1 — because the path length counts cells, and the starting cell itself counts as 1. Before doing anything else, check the edge cases: if grid[0][0] == 1 or grid[n-1][n-1] == 1, return -1 immediately since no clear path can exist. For a 1x1 grid where grid[0][0] == 0, return 1 immediately.

For each cell (r, c, dist) popped from the queue, check all 8 neighbors. For each neighbor (nr, nc) that is in bounds, equals 0, and has not been visited, add it to the queue with distance dist + 1. If the neighbor is the destination (n-1, n-1), return dist + 1 immediately — this is the shortest path by the BFS guarantee.

Mark cells as visited immediately when adding them to the queue, not when popping them. This is a crucial detail: if you wait until a cell is popped to mark it visited, other queue entries can enqueue the same cell multiple times before it is processed. Marking on enqueue ensures each cell appears in the queue at most once, keeping time complexity at O(n^2).

  1. 1Check edge cases: if grid[0][0] == 1 or grid[n-1][n-1] == 1, return -1
  2. 2If n == 1 and grid[0][0] == 0, return 1
  3. 3Initialize queue with (0, 0, 1) — row, col, current path length
  4. 4Mark grid[0][0] = 1 (in-place visited marking) or use a visited set
  5. 5While queue is not empty: pop (r, c, dist)
  6. 6For each of 8 directions (dr, dc): compute nr = r + dr, nc = c + dc
  7. 7Skip if out of bounds or grid[nr][nc] != 0
  8. 8If (nr, nc) == (n-1, n-1): return dist + 1
  9. 9Mark grid[nr][nc] = 1; enqueue (nr, nc, dist + 1)
  10. 10If queue exhausts without reaching destination: return -1

8 Directions vs 4 Directions — Diagonal Movement

Most grid BFS problems restrict movement to 4 directions: up, down, left, right. LeetCode 1091 expands this to all 8 directions by adding the four diagonals: up-left (-1,-1), up-right (-1,+1), down-left (+1,-1), and down-right (+1,+1). The implementation change is minimal — just replace the 4-element direction array with an 8-element one — but the problem structure changes significantly.

With 8-directional movement, the shortest path can be much shorter than with 4-directional movement. For example, crossing from corner to corner of an n x n all-zero grid takes 2n-1 steps with 4-direction movement but only n steps with 8-direction movement (moving diagonally all the way). This means 8-direction BFS can skip over cells that 4-direction BFS must visit, leading to shorter paths and potentially faster termination.

The standard direction array for 8-direction BFS is: [(-1,-1), (-1,0), (-1,+1), (0,-1), (0,+1), (+1,-1), (+1,0), (+1,+1)]. This covers all combinations of row delta in {-1, 0, +1} and column delta in {-1, 0, +1} except (0, 0). The BFS loop structure is otherwise identical to 4-direction BFS — only this array changes.

ℹ️

Mark Visited When Enqueuing, Not When Dequeuing

A common BFS bug is marking a cell as visited only when it is popped from the queue rather than when it is added. If you mark on dequeue, multiple queue entries for the same cell can accumulate before any of them is processed. This does not affect correctness — BFS still finds the shortest path — but it inflates the queue size and can cause O(n^4) time in the worst case instead of O(n^2). Always mark cells as visited the moment they are enqueued to guarantee linear-time BFS.

Edge Cases and Early Termination

The most common source of wrong answers on LeetCode 1091 is missing the early termination checks for blocked start or end cells. If grid[0][0] == 1, no path can start and you must return -1 immediately. If grid[n-1][n-1] == 1, no path can end at the destination and you must return -1 immediately. Failing to check these before starting BFS will produce incorrect results because BFS will terminate with an empty queue and return -1 correctly by accident, but only if the destination cell is never reached — missing the edge case where start is blocked but destination is clear.

The 1x1 grid case is also tricky: if n == 1, the start and destination are the same cell. If grid[0][0] == 0, the path length is 1 (a single cell). If grid[0][0] == 1, return -1. Some implementations handle this naturally through the main BFS loop, but it is safer to check explicitly before starting.

For visited marking, modifying the grid in-place by setting grid[r][c] = 1 after visiting is memory-efficient and avoids allocating a separate boolean matrix. This is acceptable because the problem does not require the grid to be unchanged after the call. If you need to preserve the original grid, use a separate visited boolean array of size n x n initialized to false.

  1. 1Check grid[0][0]: if == 1, return -1 (start is blocked)
  2. 2Check grid[n-1][n-1]: if == 1, return -1 (destination is blocked)
  3. 3Handle n == 1: if grid[0][0] == 0, return 1 (start equals destination)
  4. 4Mark start as visited before beginning BFS to prevent revisiting (0,0)
  5. 5In-place marking: set grid[r][c] = 1 when enqueuing — avoids extra O(n^2) space
  6. 6Early exit: as soon as (n-1,n-1) is reached, return current distance immediately

Code Walkthrough — Python and Java

In Python, use collections.deque for O(1) popleft operations. Initialize with deque([(0, 0, 1)]) and immediately check the two edge cases. The direction list is dirs = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]. The main loop calls queue.popleft() and iterates over dirs. Time complexity is O(n^2) because each cell is visited at most once. Space complexity is O(n^2) for the queue in the worst case, plus O(1) extra if marking in-place.

In Java, use ArrayDeque<int[]> where each entry is new int[]{row, col, dist}. Use int[][] dirs = {{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}}. Call queue.poll() in the main loop. Java's ArrayDeque is faster than LinkedList for BFS queues. The solution is about 25 lines and handles all constraints cleanly within the n <= 100 limit.

A subtle but important point: the path length counts cells, not edges. The starting cell at (0,0) already contributes 1 to the path length. This is why the queue is initialized with distance 1, not 0. When the destination is reached from a cell at distance dist, the answer is dist + 1 — adding 1 for the destination cell itself. Getting this count wrong is the most common off-by-one error on this problem.

⚠️

Path Length Counts Cells, Not Edges

LeetCode 1091 defines path length as the number of cells visited, including both the start and end. This means the minimum possible answer is 1 (a 1x1 grid), and the starting cell already contributes 1 to the count before any movement. Initialize your BFS queue with distance 1, not 0. When you reach the destination from a neighbor at distance dist, return dist + 1. Initializing with 0 and returning dist when reaching the destination will give answers that are 1 too small.

Ready to master algorithm patterns?

YeetCode flashcards help you build pattern recognition through active recall and spaced repetition.

Start practicing now