DEV Community

Timevolt
Timevolt

Posted on

The Matrix: Finding the Shortest Path with BFS

Ever felt like Neo dodging bullets, wishing you could see the whole grid and know the fastest way out?

The Quest Begins (The "Why")

I still remember the first time I faced a coding interview that asked for the minimum number of steps to get from the top‑left corner of a maze to the bottom‑right, moving only up, down, left or right through open cells. My first instinct was to throw a recursive DFS at it, hoping the call stack would magically uncover the optimal route. After a few minutes of watching the algorithm explore dead‑ends that were miles away from the goal, I realized I was basically wandering the Matrix without a map—lots of effort, zero guarantee of the shortest path.

That frustration sparked a question: Why does breadth‑first search (BFS) guarantee the shortest path in an unweighted graph? Once I internalized the answer, the algorithm stopped being a rote pattern and started feeling like a super‑power I could wield whenever I needed to navigate grids, social networks, or even game levels.

The Revelation (The Insight)

What BFS Actually Does

Imagine you drop a pebble into a still pond. The ripples expand outward uniformly—first the ring right next to the impact, then the next ring, and so on. BFS does exactly that with a graph: it explores all vertices at distance k from the source before touching any vertex at distance k + 1.

Because every edge has the same weight (think of each step in a maze as costing 1), the first time we reach a vertex we have necessarily used the fewest possible edges to get there. Any later discovery would involve a longer detour, which BFS would have already postponed until after all shorter possibilities were exhausted.

Why the Queue Matters

The queue is the mechanical heart of BFS. When we pop a vertex, we push all its undiscovered neighbors to the back of the line. This ensures that vertices discovered earlier (shorter distance) are processed earlier. If we used a stack (as DFS does), we’d dive deep before checking siblings, which is why DFS can’t promise optimality in unweighted graphs.

In short, BFS works because it processes vertices in non‑decreasing order of their distance from the start, and the first time we see a target we know we’ve hit the minimum distance.

Wielding the Power (Code & Examples)

Problem 1 – Shortest Path in a Binary Matrix

Given an n × n binary matrix where 0 represents a free cell and 1 a blocker, find the length of the shortest clear path from the top‑left to the bottom‑right, moving in 8 directions. Return -1 if no such path exists.

This is a classic LeetCode problem (1091).

The Struggle (Naive DFS)

def shortest_path_dfs(grid):
    n = len(grid)
    if grid[0][0] or grid[n-1][n-1]:
        return -1

    best = float('inf')
    visited = [[False]*n for _ in range(n)]

    def dfs(r, c, dist):
        nonlocal best
        if dist >= best:               # prune obvious losers
            return
        if r == n-1 and c == n-1:
            best = min(best, dist)
            return
        visited[r][c] = True
        for dr in (-1,0,1):
            for dc in (-1,0,1):
                if dr == 0 and dc == 0: continue
                nr, nc = r+dr, c+dc
                if 0 <= nr < n and 0 <= nc < n and not grid[nr][nc] and not visited[nr][nc]:
                    dfs(nr, nc, dist+1)
        visited[r][c] = False   # backtrack

    dfs(0,0,1)
    return best if best != float('inf') else -1
Enter fullscreen mode Exit fullscreen mode

The DFS version explores every possible route, often revisiting the same cell many times via different paths. In the worst case it’s exponential—O(4^{n²})—and definitely not interview‑friendly.

The Victory (BFS)

from collections import deque

def shortest_path_bfs(grid):
    n = len(grid)
    if grid[0][0] or grid[n-1][n-1]:
        return -1

    q = deque()
    q.append((0, 0, 1))               # (row, col, distance)
    visited = [[False]*n for _ in range(n)]
    visited[0][0] = True

    while q:
        r, c, d = q.popleft()
        if r == n-1 and c == n-1:
            return d
        for dr in (-1,0,1):
            for dc in (-1,0,1):
                if dr == 0 and dc == 0: continue
                nr, nc = r+dr, c+dc
                if 0 <= nr < n and 0 <= nc < n and not grid[nr][nc] and not visited[nr][nc]:
                    visited[nr][nc] = True
                    q.append((nr, nc, d+1))
    return -1
Enter fullscreen mode Exit fullscreen mode

Why this is O(n) – each cell is enqueued at most once, and we examine its 8 neighbours. So the runtime is O(V + E) where V = n² and E ≤ 8V, which simplifies to O(n²). In interview speak we often call this O(n) relative to the number of cells.

The key takeaway: the moment we pop the target from the queue we already know we’ve taken the fewest steps—no extra work needed.

Problem 2 – Number of Islands

Given a 2‑D grid of '1's (land) and '0's (water), count the number of islands. An island is surrounded by water and formed by connecting adjacent lands horizontally or vertically.

This problem appears on LeetCode (200) and is a perfect showcase for BFS as a flood‑fill tool.

BFS‑Based Solution

def num_islands(grid):
    if not grid: return 0
    rows, cols = len(grid), len(grid[0])
    islands = 0

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == '1':
                islands += 1
                # BFS to sink the whole island
                q = deque([(r, c)])
                grid[r][c] = '0'          # mark as visited
                while q:
                    x, y = q.popleft()
                    for dx, dy in [(1,0),(-1,0),(0,1),(0,-1)]:
                        nx, ny = x+dx, y+dy
                        if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == '1':
                            grid[nx][ny] = '0'
                            q.append((nx, ny))
    return islands
Enter fullscreen mode Exit fullscreen mode

Again, each cell is touched at most once, yielding O(rows × cols) time and O(min(rows, cols)) space for the queue (worst‑case when the grid is all land).

Why This New Power Matters

Mastering BFS changes how you approach any problem that involves exploring states level‑by‑level:

  • Pathfinding in games – think of a character navigating a dungeon; BFS gives you the optimal move count without heavy heuristics.
  • Social networks – finding the shortest introduction chain between two people (the classic “six degrees of separation”) is just BFS on a friendship graph.
  • Web crawlers – breadth‑first crawling ensures you discover pages closer to the seed URL first, which is often desirable for politeness policies.

The beauty is that once you grasp the why—the queue enforces distance ordering—you can adapt BFS to weighted graphs (by using a deque for 0‑1 BFS) or even to infinite state spaces (like puzzle solving) with minor tweaks.

Your Turn – A Mini Quest

Grab a piece of paper (or your favorite IDE) and solve the “Shortest Path in a Binary Matrix” problem without looking at the solution above. Try the BFS version first, then, if you feel daring, attempt a Dijkstra approach and compare the runtimes.

When you get it right, notice that satisfying click when the algorithm returns the exact number of steps—it’s like Neo finally seeing the code of the Matrix.

Happy coding, and may your queues always be full of promising nodes!

Top comments (0)