Bus Routes is a graph problem in disguise. The trick is realizing that the routes — not the stops — are the nodes you want to search over. Once you see that, it becomes a plain shortest-path BFS.
Problem. You are given routes, where routes[i] is the list of stops the i-th bus serves on a
loop. Starting at the bus stop source, return the fewest number of buses you must take to reach
target. Return -1 if it is impossible. You may switch buses only at a shared stop.
Example: routes = [[1,2,7], [3,6,7], [6,8], [8,9]], source = 1, target = 9 → answer 4
(board R0, transfer to R1 at stop 7, to R2 at stop 6, to R3 at stop 8 — which reaches stop 9).
The slow way first
You might BFS over stops, treating each stop as a node. That works, but a single bus connects every pair of stops on its route, so you re-walk the same route's stops over and over and the edge count explodes. Worse, counting "buses taken" gets awkward when your nodes are stops, not buses.
The question to ask: what am I actually choosing each step? You are choosing which bus to board next. So make the buses themselves the nodes.
The idea: routes are nodes, shared stops are edges
Build a graph where each route is a node. Two routes are connected if they share a stop — that shared stop is exactly where you can transfer between them. Then BFS from the routes serving source: the BFS depth at which you first reach a route that covers target is the fewest buses.
The key insight: BFS explores ring by ring, so the first time we reach a route covering the target, we have used the fewest buses possible.
Walk through it
Step through the animation. Each circle is a route. We start at R0 (it serves the source stop 1) with the bus count at 1. We pop R0, see it does not cover stop 9, and expand through shared stop 7 to R1 — that is bus 2. We keep going: R1 to R2 via stop 6 (bus 3), R2 to R3 via stop 8 (bus 4). When we pop R3 it contains stop 9, so we return 4.
Pseudocode
build map stop_to_routes: each stop -> list of routes serving it
queue <- every route serving source, each tagged with bus count 1
mark those routes as seen
while queue not empty:
(route, buses) <- pop front
if target is one of this route's stops:
return buses # first time = fewest buses
for each stop on this route:
for each neighbor route serving that stop:
if neighbor not seen:
mark seen, push (neighbor, buses + 1)
return -1 # target unreachableThe Python solution
def num_buses(routes, source, target):
stop_to_routes = defaultdict(list)
for r, stops in enumerate(routes):
for s in stops:
stop_to_routes[s].append(r)
queue = deque()
seen = set()
for r in stop_to_routes[source]:
queue.append((r, 1)); seen.add(r)
while queue:
route, buses = queue.popleft()
if target in routes[route]:
return buses
for s in routes[route]:
for nxt in stop_to_routes[s]:
if nxt not in seen:
seen.add(nxt); queue.append((nxt, buses + 1))
return -1stop_to_routesmaps a stop → the routes that serve it, so we can find transfers in O(1).- We seed the queue with every route that serves
source, each tagged with a bus count of1. seenholds the routes already queued, so we never board the same bus twice.- When we pop a route, we first check
target in routes[route]— if found, the bus count is the answer because BFS reaches it at minimal depth. - To expand, we look at each stop on the current route and queue every unseen neighbor route with
buses + 1.
Complexity
| Case | Time | Notes |
|---|---|---|
| Build stop map | O(S) (moderate) | S = total stops across all routes |
| BFS over routes | O(S) (moderate) | each route and shared stop visited once |
O(S) (moderate)Let N be the number of routes and S the total count of stops across them. The map build is O(S), and the BFS touches each route and its stops once, so the whole thing is O(S) time and space. The win comes from searching over the small set of routes instead of re-expanding stops.
When this pattern shows up
When a problem asks for the fewest steps / shortest path on an unweighted graph, reach for BFS. The real skill here is choosing the right nodes: the obvious entity (stops) is not always the best one (routes). Re-modeling so the thing you choose each step becomes a node is a recurring interview move.
Mark a route as seen the moment you enqueue it, not when you pop it. If you wait until pop, the same route can be pushed many times through different shared stops, blowing up the queue and the runtime.
Practice
In the example, after we pop R1 and find it does not cover stop 9, which route do we reach next and at what bus count?
1. Why do we make routes the nodes instead of stops?
2. What does the stop_to_routes map let us do quickly?
3. Why is the first time BFS reaches a target-covering route guaranteed to be optimal?
4. When should a route be marked as seen?