183. BFS Shortest Hops
Problem
Return the minimum number of edges from start to target in an unweighted adjacency-list graph. Vertices are valid indices; edges may be directed. Return -1 when unreachable and zero when start equals target. Cycles and duplicate edges are allowed.
Examples
Example 1
shortest_hops([[1,2],[3],[4],[],[3]],0,3)
=> 2The two-edge branch reaches the target first.
Example 2
shortest_hops([[1],[],[]],0,2)
=> -1No path reaches the target.
Example 3
shortest_hops([[]],0,0)
=> 0No edge is needed.
Reveal the worked solution and reasoning
Why this mechanism works
Each edge costs one, so processing all distance-d vertices before distance-(d+1) vertices makes first discovery optimal. Use FIFO order and assign a distance when enqueuing. A vertex needs no later improvement because every competing route arrives from an equal or greater depth.
State and boundaries
dist[v] is -1 until discovery, then its shortest hop count. The queue is ordered by nondecreasing assigned distance.
Protocol
- Set start distance to zero and enqueue it.
- Pop the next vertex.
- Return if it is target.
- For each undiscovered neighbor assign current distance+1 and enqueue.
- Return -1 on exhaustion.
Visualization
0 -> 1 -> 3
\-> 2 -> 4 -> 3
BFS layers: [0], [1,2], [3,4]
distance to 3 is 2Python implementation
from collections import deque
def shortest_hops(adj, start, target):
dist = [-1] * len(adj)
dist[start] = 0
queue = deque([start])
while queue:
u = queue.popleft()
if u == target:
return dist[u]
for v in adj[u]:
if dist[v] == -1:
dist[v] = dist[u] + 1
queue.append(v)
return -1
Worked execution
shortest_hops([[1,2],[3],[4],[],[3]],0,3)
L4 shortest_hops: dist = [-1] * len(adj) adj=[[1, 2], [3], [4], [], [3]]; start=0; target=3; dist=[-1, -1, -1, -1, -1] L5 shortest_hops: dist[start] = 0 adj=[[1, 2], [3], [4], [], [3]]; start=0; target=3; dist=[0, -1, -1, -1, -1] L6 shortest_hops: queue = deque([start]) adj=[[1, 2], [3], [4], [], [3]]; start=0; target=3; dist=[0, -1, -1, -1, -1]; queue=deque([0]) L8 shortest_hops: u = queue.popleft() adj=[[1, 2], [3], [4], [], [3]]; start=0; target=3; dist=[0, -1, -1, -1, -1]; queue=deque([]); u=0 L11 shortest_hops: for v in adj[u]: adj=[[1, 2], [3], [4], [], [3]]; start=0; target=3; dist=[0, -1, -1, -1, -1]; queue=deque([]); u=0; v=1 L13 shortest_hops: dist[v] = dist[u] + 1 adj=[[1, 2], [3], [4], [], [3]]; start=0; target=3; dist=[0, 1, -1, -1, -1]; queue=deque([]); u=0; v=1 L14 shortest_hops: queue.append(v) adj=[[1, 2], [3], [4], [], [3]]; start=0; target=3; dist=[0, 1, -1, -1, -1]; queue=deque([1]); u=0; v=1 L11 shortest_hops: for v in adj[u]: adj=[[1, 2], [3], [4], [], [3]]; start=0; target=3; dist=[0, 1, -1, -1, -1]; queue=deque([1]); u=0; v=2 ... 7 intermediate state changes omitted ... L11 shortest_hops: for v in adj[u]: adj=[[1, 2], [3], [4], [], [3]]; start=0; target=3; dist=[0, 1, 1, 2, -1]; queue=deque([3]); u=2; v=4 L13 shortest_hops: dist[v] = dist[u] + 1 adj=[[1, 2], [3], [4], [], [3]]; start=0; target=3; dist=[0, 1, 1, 2, 2]; queue=deque([3]); u=2; v=4 L14 shortest_hops: queue.append(v) adj=[[1, 2], [3], [4], [], [3]]; start=0; target=3; dist=[0, 1, 1, 2, 2]; queue=deque([3, 4]); u=2; v=4 L8 shortest_hops: u = queue.popleft() adj=[[1, 2], [3], [4], [], [3]]; start=0; target=3; dist=[0, 1, 1, 2, 2]; queue=deque([4]); u=3; v=4
Result: 2. The two-edge branch reaches the target first.
shortest_hops([[1],[],[]],0,2)
L4 shortest_hops: dist = [-1] * len(adj) adj=[[1], [], []]; start=0; target=2; dist=[-1, -1, -1] L5 shortest_hops: dist[start] = 0 adj=[[1], [], []]; start=0; target=2; dist=[0, -1, -1] L6 shortest_hops: queue = deque([start]) adj=[[1], [], []]; start=0; target=2; dist=[0, -1, -1]; queue=deque([0]) L8 shortest_hops: u = queue.popleft() adj=[[1], [], []]; start=0; target=2; dist=[0, -1, -1]; queue=deque([]); u=0 L11 shortest_hops: for v in adj[u]: adj=[[1], [], []]; start=0; target=2; dist=[0, -1, -1]; queue=deque([]); u=0; v=1 L13 shortest_hops: dist[v] = dist[u] + 1 adj=[[1], [], []]; start=0; target=2; dist=[0, 1, -1]; queue=deque([]); u=0; v=1 L14 shortest_hops: queue.append(v) adj=[[1], [], []]; start=0; target=2; dist=[0, 1, -1]; queue=deque([1]); u=0; v=1 L8 shortest_hops: u = queue.popleft() adj=[[1], [], []]; start=0; target=2; dist=[0, 1, -1]; queue=deque([]); u=1; v=1
Result: -1. No path reaches the target.
Why the solution is correct
Inductively, all vertices at distance at most d are discovered before processing the next layer. A newly discovered neighbor has a length-(d+1) route, and a shorter route would already have discovered it. Therefore every assigned distance is minimal, including the returned target distance.
Time and space
O(V+E) time and O(V) auxiliary distance/queue storage.
Common mistakes
A DFS visitation distance is not generally shortest. This first-discovery proof fails when edges have unequal weights.
Reasoning card
State: dist[v] is -1 until discovery, then its shortest hop count. The queue is ordered by nondecreasing assigned distance.
Invariant: Queue order is nondecreasing distance.
Decision to rehearse: Queue stores nodes with their distance; mark on enqueue.
Finish: Return -1 on exhaustion
Transfer exercise
Count shortest paths by accumulating counts when another edge reaches a vertex at its already-known optimal distance.