Skip to content
Reliable Data Engineering
Practice problem medium bfsgraphsshortest-path
Practise with timer, notes and rubric

Shortest Path Through a Grid With Obstacles

Pattern: Graphs: BFS, DFS and Union-Find · Difficulty: Medium · Asked at: Amazon, Meta, Uber, DoorDash

Classic version: LeetCode 1091 · LeetCode 127

Problem

grid is a list of strings; '.' is open and '#' is blocked. Moving up/down/left/right costs 1. Return the minimum number of moves from the top-left to the bottom-right cell, or -1 if unreachable (including when either end is blocked).

Examples

shortest_path(["..",
               ".."])        → 2
shortest_path([".#.",
               ".#.",
               "..."])       → 4

Starter code

def shortest_path(grid: list[str]) -> int:
    pass

Hints

Hint 1

BFS explores cells in order of distance, so the first time you reach the target is via a shortest path.

Hint 2

Store the distance with each queued cell (or process the queue level by level).

Where this shows up in data engineering

BFS levels answer “how many hops?”: degrees of separation in a user graph, how far downstream a broken table’s impact reaches in lineage (blast radius by hop count), and the minimum number of transformations between schemas. With weighted edges (cost, latency) switch to Dijkstra.

Solution

from collections import deque


def shortest_path(grid):
    if not grid or grid[0][0] == "#" or grid[-1][-1] == "#":
        return -1
    rows, cols = len(grid), len(grid[0])
    q = deque([(0, 0, 0)])
    seen = {(0, 0)}
    while q:
        r, c, d = q.popleft()
        if (r, c) == (rows - 1, cols - 1):
            return d
        for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
            if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == "." and (nr, nc) not in seen:
                seen.add((nr, nc))
                q.append((nr, nc, d + 1))
    return -1

Tests

Your solution should pass these:

assert shortest_path(["..", ".."]) == 2
assert shortest_path([".#.", ".#.", "..."]) == 4
assert shortest_path(["."]) == 0
assert shortest_path(["#"]) == -1
assert shortest_path([".#", "#."]) == -1
assert shortest_path(["....", "###.", "....", ".###", "...."]) == 13

Explanation

Why BFS is shortest: it processes all cells at distance d before any at distance d+1 (FIFO queue), so the first arrival at the target is optimal. DFS gives a path, not the shortest.

Complexity: O(R·C) time and space.

Variants: 8-directional moves (add diagonals), weighted cells (Dijkstra with a heap), “remove up to k obstacles” (BFS over states (r, c, k_left)), and bidirectional BFS to roughly square-root the explored area on large graphs.

Follow-up questions

Moving into some cells costs more (e.g. congested roads). What changes?

Dijkstra: a min-heap of (cost, r, c); pop the cheapest, relax neighbours with cost + weight. O(E log V). With only 0/1 weights, a deque-based 0-1 BFS is O(V + E).