DSA sheet · Graphs · BFS shortest path pattern

Shortest Path in Binary Matrix

This is the first "shortest path" problem of the graph pattern. The grid is a graph, every step costs the same, and the teacher shows why that one fact means plain BFS already gives the shortest path, with no fancy algorithm needed. She also shows a neat space saving: the distance grid doubles as the visited grid, so we never build a separate visited array. She says it's very close to 01 Matrix (in her playlist she recorded 01 Matrix just before this one). The big difference is that here there is one starting cell, not many.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Nodes and edges

A graph is a set of nodes (points, also called vertices) joined by edges (lines). An edge says "you can go directly from this node to that one".

(0)—(1)—(2)
 |       |
(3)—————(4)

Reading a graph and building an adjacency list

Normal graph problems give an edge list like [[0,1],[1,2]] or an adjacency matrix (an n×n table where m[u][v] = 1 means an edge). We usually turn it into an adjacency list: for every node, the list of its neighbours. It answers "where can I go from here?" quickly.

edge list → adjacency list (for normal node/edge graphs)
n = 5
edges = [[0, 1], [1, 2], [0, 3], [3, 4], [2, 4]]
adj = [[] for _ in range(n)]
for u, v in edges:
    adj[u].append(v)      # undirected: add both ways
    adj[v].append(u)
print(adj)   # [[1, 3], [0, 2], [1, 4], [0, 4], [3, 2]]

Grids are graphs too, and need no adjacency list

In a grid, each cell (r, c) is a node. Its neighbours are the cells around it. The teacher's point: we build an adjacency list only when we don't know who the neighbours are. In a grid we always know: just add small offsets to the row and column. So a grid needs no adjacency list; a small list of directions is enough.

  4 directions (this problem uses 8)      8 directions
                (r-1,c)                 (r-1,c-1) (r-1,c) (r-1,c+1)
                   ↑                         ↖      ↑      ↗
       (r,c-1) ← (r,c) → (r,c+1)    (r,c-1) ←   (r,c)   → (r,c+1)
                   ↓                         ↙      ↓      ↘
                (r+1,c)                 (r+1,c-1) (r+1,c) (r+1,c+1)

Visited: why we need it

In a graph you can walk in circles: A → B → A → B… If we don't remember which nodes we already reached, BFS keeps adding them again and never stops. A visited set/array marks "already reached, don't add again". Usually it's a separate array. In this problem we'll get it for free from the distance grid (Part A, step 4).

A connected component is a group of nodes that can all reach each other. If the start and the target are in different components (a wall of 1s separates them), there is no path, and the answer is −1.

BFS (breadth first search): level by level with a queue

BFS starts at one node and visits it, then all nodes 1 step away, then all nodes 2 steps away, and so on. It uses a queue (collections.deque): add at the back with append, remove from the front with popleft. Rule: mark a node visited when you push it, so it can never be pushed twice.

BFS template on an adjacency list
from collections import deque

adj = [[1, 3], [0, 2], [1, 4], [0, 4], [3, 2]]
dist = [-1] * len(adj)           # -1 means "not reached yet" (also our visited mark)
dist[0] = 0
queue = deque([0])
while queue:
    u = queue.popleft()
    for v in adj[u]:
        if dist[v] == -1:        # not visited
            dist[v] = dist[u] + 1
            queue.append(v)
print(dist)   # [0, 1, 2, 1, 2]

DFS (depth first search) instead runs down one path as far as possible (recursion or a stack), then backs up. Good for "reach everything" questions, but it does not find shortest paths, as we'll see.

Why BFS layers give the shortest path (unweighted graphs)

Think of dropping a stone in still water. The ripple reaches everything 1 step away first, then 2 steps, then 3. BFS is that ripple.

layer 1:  S                      the queue always looks like
layer 2:  every cell 1 step from S        [ layer d ... | layer d+1 ... ]
layer 3:  every cell 2 steps from S       never a layer d+2 cell before a layer d+1 cell
  1. The queue holds cells in non-decreasing distance order. We start with only S. Every time we pop a cell at distance d, we push its new neighbours at distance d+1 at the back. So all distance-d cells come out before any distance-(d+1) cell.
  2. Suppose a cell X is first reached at distance d. Could there be a shorter path, say d−1? Then the cell just before X on that shorter path has distance d−2 and would have been popped earlier, and it would have pushed X with d−1. That didn't happen, so no shorter path exists.
  3. So the first time BFS reaches a cell, its distance is the shortest. That's why we never update a visited cell again.
Doubt: does this still work if edges had different costs?
→ No. With weights, a path with more edges can be cheaper (three edges of cost 1 beat one edge of cost 10). Counting layers would be wrong. That needs Dijkstra's algorithm (a heap instead of a queue), which comes later in the sheet. The teacher says exactly this: no weights given → every move costs 1 → BFS; weights given → Dijkstra.

Part A · BFS with a distance grid

LeetCode 1091

1The question in simple words

You get an n×n grid of 0s and 1s. Find the length of the shortest clear path from the top-left cell (0, 0) to the bottom-right cell (n-1, n-1). If there is no clear path, return −1.

A clear path means:

Example 1 → 2
 0  1
 1  0
 one diagonal step:
 cells (0,0),(1,1) = 2
Example 2 → 4
 0  0  0
 1  1  0
 1  1  0
 (0,0)→(0,1)→(1,2)→(2,2)
Example 3 → −1
 1  0  0
 1  1  0
 1  1  0
 start is a wall

The teacher also notes the type of problem: the question names the start cell, so this is a single-source shortest path. (In 01 Matrix no start was named, every 0 was a start, so that one was multi-source.)

2What the constraints tell us

3Intuition: a ripple from the top-left corner

Picture pouring water into the top-left cell. Each second it spreads into every touching 0-cell (8 ways), never into walls. The first second the water reaches the bottom-right cell is the length of the shortest path. Cells reached at the "same second" form one layer. That's exactly BFS.

Before coding, the teacher runs through her usual checklist of 3 questions for any graph problem:

questionanswer herewhy
Adjacency list?NoIt's a grid. From any cell we already know the 8 neighbours from the indexes.
BFS or DFS?BFSWe need the shortest path and every move costs 1 (no edge weights given). BFS gives it directly.
Visited array?Not a separate oneThe distance grid tells us: distance 0 = not visited, positive = visited.

4Building the logic from her examples

Base case first: blocked start or blocked end → −1

If grid[0][0] == 1, we can't even stand on the start. If grid[n-1][n-1] == 1, we can never finish on the end. Either way, no clear path exists.

Rule 1if grid[0][0] == 1 or grid[n-1][n-1] == 1: return -1

Why do we need a distance grid?

The answer is a count of cells, so for every cell we need to remember "how many cells did it take to get here?". We make a separate grid dist of the same size, filled with 0.

Doubt: why not write the distances straight into grid?
→ You could, but the teacher's habit is: don't change the input unless the interviewer asks you to. If they then ask "can you save that extra space?", that's the hint to reuse the given grid.

The space trick: dist also tells us "visited"

The usual plan would be two extra grids: dist and visited. But look at what dist holds:

So dist[r][c] == 0 means "not visited" and dist[r][c] > 0 means "visited". We don't need a separate visited grid.

Key insightDistance 0 = not visited. Positive distance = visited. This only works because the start is given distance 1, not 0.
Doubt: what if I gave the start distance 0 (counting moves) and added 1 at the end?
→ Then the start would look "unvisited" (0), and a neighbour could push it back into the queue. You'd need −1 as the "unvisited" mark instead (that's what 01 Matrix does). Starting at 1 is simpler here, and it matches the question, which counts cells.

When can we step into a neighbour? Three checks

From a popped cell (r, c), for each of the 8 directions we compute nr, nc. We may go there only if all of these hold:

  1. Inside the grid: 0 <= nr < n and 0 <= nc < n. In Example 2 from (0,0), going up or left falls off the grid.
  2. Not a wall: grid[nr][nc] == 0. In Example 2, the cell below (0,0) is 1, so we can't go down.
  3. Not visited: dist[nr][nc] == 0. From (0,1) we could step back to (0,0), but its distance is 1 (positive), so it's already visited. Skip it.
Doubt: why does the order matter in that if?
→ The bounds check must come first. Python's and stops at the first False, so we never read grid[-1][0] or grid[n][0]. (In Python grid[-1] wouldn't even crash, it quietly reads the last row, which is a nasty silent bug.)

What do we do when we step in?

Doubt: why mark it when pushing, not when popping?
→ In Example 2, both (0,2) and (1,2) are reached from (0,1). If we marked only on pop, another cell in the same layer might push (0,2) again, and the same cell would sit in the queue twice. Marking on push means each cell enters the queue at most once.

When do we stop? As soon as we POP the last cell

When the popped cell is (n-1, n-1), its distance was already written when it was pushed, and by the layer argument (Part 0) it is the smallest possible. So return dist[n-1][n-1] right away.

Doubt: why not keep exploring its neighbours, in case something shorter turns up?
→ It can't. BFS reached this cell in the earliest possible layer. Any other route would arrive in the same layer or later, and could only give an equal or bigger count. The teacher's way of saying it: we arrived with the minimal distance, so there's nothing to gain by going on.
Doubt: and if the loop ends without ever popping the last cell?
→ Then the last cell was never reached. Walls cut it off. Her example: the end cell is 0, but every cell around it is 1, so nothing can step into it, not even diagonally. The queue runs dry → return -1.

Why not DFS?

DFS dives down one path and reports the first route it finds to the end. That route can be long and winding. To get the shortest with DFS you'd have to try every path and keep the minimum, which is exponential. BFS reaches cells in order of distance, so its first arrival is the answer.

 0  0  0        DFS might go right, right, down, down = 5 cells
 0  0  0        BFS: (0,0) → (1,1) → (2,2) = 3 cells, found in layer 3
 0  0  0

5Approach steps

  1. If the start or the end is 1 → return −1.
  2. Make dist, an n×n grid of 0s. Set dist[0][0] = 1. Push (0, 0) into a queue.
  3. Write the 8 direction pairs (dr, dc).
  4. While the queue isn't empty: pop (r, c).
  5. If (r, c) is the last cell → return dist[r][c].
  6. For each of the 8 directions: if the new cell is inside, is 0 in grid, and is 0 in dist → set its distance to dist[r][c] + 1 and push it.
  7. If the loop ends → return −1.

6Code (Python)

Shortest Path in Binary Matrix · BFS, dist grid doubles as visited
from collections import deque

class Solution:
    def shortestPathBinaryMatrix(self, grid):
        n = len(grid)
        if grid[0][0] == 1 or grid[n - 1][n - 1] == 1:
            return -1

        dist = [[0] * n for _ in range(n)]     # 0 = not visited yet
        dist[0][0] = 1                         # the start cell counts as 1
        queue = deque([(0, 0)])

        directions = [(-1, 0), (1, 0), (0, -1), (0, 1),       # up, down, left, right
                      (-1, -1), (-1, 1), (1, -1), (1, 1)]     # the 4 diagonals

        while queue:
            r, c = queue.popleft()
            if r == n - 1 and c == n - 1:      # reached the end: first arrival is shortest
                return dist[r][c]
            for dr, dc in directions:
                nr, nc = r + dr, c + dc
                if (0 <= nr < n and 0 <= nc < n
                        and grid[nr][nc] == 0 and dist[nr][nc] == 0):
                    dist[nr][nc] = dist[r][c] + 1   # one more cell than the parent
                    queue.append((nr, nc))

        return -1                              # the end was never reached

7Code line by line

linewhat it means
n = len(grid)The grid is square, so one size is enough for rows and columns.
if grid[0][0] == 1 or grid[n-1][n-1] == 1: return -1Base case: a blocked start or a blocked end means no clear path.
dist = [[0] * n for _ in range(n)]A fresh n×n grid of zeros. Each row is a new list (writing [[0]*n]*n would share one row n times, a classic bug).
dist[0][0] = 1The path so far is just the start cell: length 1. Being positive, it also marks the start visited.
queue = deque([(0, 0)])Single source: only the start goes in.
directions = [...]The 8 (row change, column change) pairs. Each pair is one possible step.
r, c = queue.popleft()Take the oldest cell, which belongs to the current (closest) layer.
if r == n-1 and c == n-1: return dist[r][c]The first time the end comes out of the queue, its distance is the shortest. Stop.
nr, nc = r + dr, c + dcThe neighbour in this direction.
0 <= nr < n and 0 <= nc < nInside the grid. Checked first so we never index outside.
grid[nr][nc] == 0Not a wall.
dist[nr][nc] == 0Not visited yet.
dist[nr][nc] = dist[r][c] + 1One cell longer than the parent's path. Also marks it visited, right at push time.
queue.append((nr, nc))Explore its neighbours later, in the next layer.
return -1The queue emptied without reaching the end: walls cut it off.

8Dry run (her Example 2)

grid            index map
 0 0 0          (0,0) (0,1) (0,2)
 1 1 0          (1,0) (1,1) (1,2)
 1 1 0          (2,0) (2,1) (2,2)
steppoppedneighbours checked (8 ways)queue AFTERdist setanswer
start––(0,0)(0,0)=1–
1(0,0)up/left/diagonals up: outside · (1,0) wall · (1,1) wall · (0,1) ok(0,1)(0,1)=2–
2(0,1)(0,0) visited · (1,0),(1,1) walls · (0,2) ok · (1,2) ok (diagonal) · rest outside(0,2) (1,2)(0,2)=3, (1,2)=3–
3(0,2)(0,1),(1,2) visited · (1,1) wall · rest outside(1,2)nothing new–
4(1,2)(0,1),(0,2) visited · (1,1),(2,1) walls · (2,2) ok(2,2)(2,2)=4–
5(2,2)it's the last cell → stop––4
dist after step 1
 1  2  .
 #  #  .
 #  #  .
dist after step 2 (layer 3)
 1  2  3
 #  #  3
 #  #  .
dist after step 4
 1  2  3
 #  #  3
 #  #  4

# = wall (grid value 1), . = not visited (dist 0). Notice (0,2) and (1,2) got their distance in the same step: they are the same layer, which is what the teacher means by "BFS goes to all children at the same time".

9Complexity & remember

RememberUnweighted grid + "shortest" → BFS. Block check on start/end → −1. dist[0][0] = 1, 0 = unvisited. 8 directions. Mark on push. Return when you pop the end. Queue empty → −1.

Part B · Revision page

Shortest Path in Binary Matrix01 Matrix (her previous video)
sourcessingle: (0,0) is namedmulti: every 0 starts at once
directions8 (question says so)4
"unvisited" markdist = 0 (start is 1)dist = −1 (zeros are 0)
what we returnone number, or −1the whole dist grid
early stopyes, when the end is poppedno, fill every cell
time / spaceO(8n²) / O(n²)O(4mn) / O(mn)
BFSDFS
order of visitingby distance, layer by layerone deep path at a time
first arrival at the endis the shortest (unweighted)can be any path
shortest path with weightsno → use Dijkstrano
If you remember only 5 lines 1. No weights → each step costs 1 → BFS layers = shortest distances.
2. Start or end is 1 → return −1 straight away.
3. dist[0][0] = 1; 0 in dist means "not visited", so no visited grid.
4. Step in only if inside, not a wall, not visited; set parent + 1 and push.
5. Return dist when the end is popped; empty queue → −1.
Mistakes to avoid ✗ only 4 directions (this one needs 8)
✗ counting moves instead of cells (answer for a 1×1 grid of 0 is 1)
✗ forgetting the blocked start/end check
✗ starting dist[0][0] at 0 while using 0 as "unvisited"
✗ marking visited on pop (cells get pushed twice)
✗ using DFS for a shortest path
test it yourself (paste under the solution above)
s = Solution()
print(s.shortestPathBinaryMatrix([[0, 1], [1, 0]]))                    # 2
print(s.shortestPathBinaryMatrix([[0, 0, 0], [1, 1, 0], [1, 1, 0]]))   # 4
print(s.shortestPathBinaryMatrix([[1, 0, 0], [1, 1, 0], [1, 1, 0]]))   # -1
print(s.shortestPathBinaryMatrix([[0]]))                               # 1
print(s.shortestPathBinaryMatrix([[0, 0], [0, 1]]))                    # -1
print(s.shortestPathBinaryMatrix([[0, 1, 0], [1, 1, 0], [0, 0, 0]]))   # -1

Based on this video: Shortest Path in Binary Matrix | BFS