Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Graphs  ›  Lesson

Breadth-First Search & Degrees of Separation

Breadth First Search 20 minExploring outward one ring at a time
You're building a piece ofTunebox — recommendations
This piece — degrees_between(): How many hops separate two listeners.
Scenario Ana opens the profile of someone she's never met, and Tunebox shows "2nd-degree connection". That number is a breadth-first search through the follow graph.
Your task
Build degrees_between(graph, start, goal). `graph` is Tunebox's follow graph as an adjacency dict; follow the arrows in their direction and return the fewest hops from `start` to `goal` — 0 if they're the same listener, -1 if there's no route. A listener with no key follows nobody. Example: degrees_between({"ana": ["ben"], "ben": ["cal"], "cal": []}, "ana", "cal") → 2.

Breadth-First Search & Degrees of Separation

Ana opens Dia's profile and Tunebox says 2nd-degree connection — Ana follows someone who follows Dia. Working that out means exploring the follow graph outward from Ana: everyone one hop away, then everyone two hops away, until Dia turns up. That ripple is breadth-first search (BFS), the algorithm behind degrees of separation, "people you may know", web crawlers and maze solvers.

Exploring level by level

Picture a stone dropped in water. Ring 0 is Ana. Ring 1 is everyone she follows. Ring 2 is everyone they follow — minus anyone already in an earlier ring.

python

Three rings: ana, then ben cal, then dia eli. Eli follows Ana back, but Ana is already seen, so the walk doesn't go round in circles.

The queue

Keeping explicit rings works, but classic BFS uses a single queue — first in, first out. Discoveries join the back; you explore from the front. Everything found earlier (closer) is explored before anything found later (further), so the rings fall out on their own.

What does this print?
from collections import deque
queue = deque(["ana"])
queue.append("ben")
queue.append("cal")
print(queue.popleft(), queue.pop())

❓ Cross-question — "Why deque? A list has append and pop(0)." It does, but list.pop(0) shifts every remaining item one place left — O(n) per call — so BFS over a big graph quietly turns quadratic. deque.popleft() is O(1).

Coming from Java/JS: deque plays the role of Java's ArrayDeque used as a Queue (offer / poll). In JS, array.shift() has the same O(n) cost as pop(0); Python ships the fix in the standard library.

The visited set

Graphs have cycles and shared neighbours, so the same listener can be discovered more than once. A visited set makes sure each one is queued once — but when you mark them matters. This version marks listeners as they leave the queue:

broken — fix it

Each listener should appear once, but Dia appears twice. Fix where the visited set is updated.

Mark on enqueue, not on dequeue. Then every vertex enters the queue exactly once.

Shortest paths in an unweighted graph

Here's BFS's superpower. Because it explores ring by ring, the first time it reaches a listener is along a route with the fewest possible hops. So a dist dict filled in at discovery holds shortest distances — and doubles as the visited set. Step through it:

step through it
1from collections import deque
2graph = {"ana": ["ben", "dia"], "ben": ["cal"], "cal": ["dia"], "dia": []}
3dist = {"ana": 0}
4queue = deque(["ana"])
5while queue:
6 name = queue.popleft()
7 for nxt in graph[name]:
8 if nxt not in dist:
9 dist[nxt] = dist[name] + 1
10 queue.append(nxt)
11print(dist)

Dia is reachable two ways — directly, and the long way through Ben and Cal. BFS records 1, because the direct follow is found in ring 1, before the long way round can get there.

❓ Cross-question — "Why not depth-first search? It visits everything too." It does, but DFS charges down one branch first, so the first route it finds to Dia might be the long one. Only BFS's ring-by-ring order makes "found first" mean "fewest hops".

Reconstructing the path

A distance says how far. To show how — "Ana → Cal → Eli" — record a parent pointer for every discovery: who you were exploring when you found them.

python

Each listener points back one step toward Ana along a shortest route. Walking those pointers from the goal gives the route backwards, so reverse it at the end:

python

Unreachable listeners

Follows are one-way, so some listeners simply can't be reached. BFS needs no special code for that: the queue runs dry, and they never get a distance.

What happens when the last line runs?
from collections import deque
graph = {"ana": ["ben"], "ben": ["ana"], "cal": ["ana"]}
dist = {"ana": 0}
queue = deque(["ana"])
while queue:
    name = queue.popleft()
    for nxt in graph[name]:
        if nxt not in dist:
            dist[nxt] = dist[name] + 1
            queue.append(nxt)
print(dist["cal"])

What BFS costs

Operation Cost
Full BFS from one listener O(V + E) — each vertex queued once, each edge checked once
Memory (queue + visited) O(V)
deque.append / deque.popleft O(1)
list.pop(0) — the slow way O(n)
Distance lookup afterwards O(1)

Idioms & real-world patterns

Stop as soon as you've found the goal

If you only care about one listener, return the moment you discover them — there's no need to explore the rest of Tunebox:

python

graph.get(name, []) is the other habit worth stealing: a listener with no key simply follows nobody, instead of crashing the search.

One whole ring at a time

When you need to act per level — "everyone exactly two hops away" — drain the queue in batches of len(queue). Items added during the batch wait for the next one:

python

⚡ Advanced — why BFS's distances are guaranteed shortest

At every moment the queue holds some listeners at distance d, followed only by listeners at distance d + 1 — never anything else. It starts true (just the start, at 0), and every pop of a d can only append d + 1s behind it. So listeners leave the queue in non-decreasing distance order, and the first discovery of anyone comes from the earliest possible ring. That invariant is exactly what breaks once edges carry different weights — which is the Dijkstra lesson's starting point.

⚡ Advanced — bidirectional BFS

On a huge graph, run two searches — one out from the start, one back from the goal — and stop when their frontiers meet. With every listener following about b others and the goal d hops away, one search touches roughly b^d listeners; two half-depth searches touch roughly 2·b^(d/2). For b = 200 and d = 4 that's 80,000 instead of 1.6 billion. (The backward search needs follows reversed: who follows whom.)


🎯 Your turn

Write degrees_between(graph, start, goal) — the fewest hops from start to goal following the arrows of the follow graph, 0 if they're the same listener, and -1 if there's no route. A listener with no key follows nobody:

  • degrees_between({"ana": ["ben"], "ben": ["cal"], "cal": []}, "ana", "cal")2
  • degrees_between({"ana": ["ben"], "ben": [], "cal": []}, "ana", "cal")-1

Hint — BFS with a deque, and a hops dict that records each listener's distance the first time you see them — it's your visited set too.

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = this piece of the app is built. ✅

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upDiscovery order

Write bfs_order(graph, start) returning listeners in the order breadth-first search discovers them, beginning with start. Explore each listener's neighbours in the order they're listed, and mark a listener seen when you queue them. Every name in the graph is a key. Example: {"ana": ["ben", "cal"], "ben": ["dia"], "cal": [], "dia": []} from "ana"["ana", "ben", "cal", "dia"].

bfsvisited-setdeque
DrillYour extended circle

Tunebox's 'extended circle' shows everyone within k hops. Write within_hops(graph, start, k) returning the listeners reachable from start in 1 to k hops, sorted alphabetically — never start itself. Every name is a key. Example: {"ana": ["ben"], "ben": ["cal"], "cal": ["dia"], "dia": []}, "ana", 2["ben", "cal"].

bfsqueuesorted
BuildShow the chain

Degrees are a number — now show the chain. Write shortest_route(graph, start, goal) returning the listeners on a shortest route from start to goal, both ends included, or [] if there is none. Explore neighbours in the order they're listed and keep the first parent recorded for each listener, so ties always resolve the same way. Every name is a key. Example: {"ana": ["ben", "cal"], "ben": ["dia"], "cal": ["dia"], "dia": []}, "ana", "dia"["ana", "ben", "dia"].

bfsvisited-setdict
BossFind the stage

Tunebox Live guides festival-goers to the stage. The venue is a list of equal-length strings: S is you, E is the stage, # is a barrier and . is open ground. Write venue_steps(grid) returning the fewest steps from S to E, moving up, down, left or right, or -1 if the stage can't be reached. Example: ["S.#", "..E"]3.

bfsqueuevisited-set
CapstoneNearest artist, one search

Tunebox shows every listener how close they are to any verified artist. graph is an undirected friendship graph (each friendship listed from both sides; every name is a key), and artists is a list of names in it. Write hops_to_artist(graph, artists) returning a dict of every listener's hops to their nearest artist — 0 for artists themselves, -1 if no artist can be reached. Aim for one search over the graph, not one per artist.

bfsqueuedict
degrees_between.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
degrees_between(graph, start, goal) → intFewest follow hops from start to goal; 0 for the same listener, -1 if unreachable.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.
Breadth-First Search & Degrees of Separation — Pebells