Prerequisites
Before attempting this problem, you should be comfortable with:
- Breadth-First Search (BFS) - Level-order traversal for finding shortest paths in unweighted graphs
- Hash Maps - Building mappings between stops and routes for efficient lookups
- Graph Modeling - Representing real-world problems (bus routes) as graph structures
- Sets for Visited Tracking - Using sets to avoid revisiting nodes and prevent infinite loops
1. Breadth First Search (Stops as Nodes)
Intuition
This is a shortest path problem where we want to find the minimum number of buses needed to travel from a source stop to a target stop. We can model this as a graph problem where stops are nodes, and we use BFS to find the shortest path in terms of bus transfers.
The key insight is that when we board a bus, we can reach all stops on that route. So from any stop, we can transition to all other stops on any bus that serves that stop. We count bus transfers (not individual stops) to measure distance.
Algorithm
- If
sourceequalstarget, return0(no bus needed). - Build a mapping from each stop to the list of bus routes that serve it.
- Initialize BFS from the source stop, tracking visited stops and visited buses.
- For each level of BFS (representing one bus ride):
- For each stop at the current level, check all buses that serve this stop.
- For each unvisited bus, add all its stops to the next BFS level.
- Mark buses and stops as visited to avoid revisiting.
- If we reach the target stop, return the number of bus rides taken.
- If BFS completes without finding the target, return
-1.
class Solution:
def numBusesToDestination(self, routes: List[List[int]], source: int, target: int) -> int:
if source == target:
return 0
n = len(routes)
stops = defaultdict(list)
for bus in range(n):
for stop in routes[bus]:
stops[stop].append(bus)
seen_bus = set()
seen_stop = set([source])
res = 0
q = deque([source])
while q:
for _ in range(len(q)):
stop = q.popleft()
if stop == target:
return res
for bus in stops[stop]:
if bus in seen_bus:
continue
seen_bus.add(bus)
for nxtStop in routes[bus]:
if nxtStop in seen_stop:
continue
seen_stop.add(nxtStop)
q.append(nxtStop)
res += 1
return -1Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of routes and is the maximum number of stops per bus route.
2. Breadth First Search (Routes as Nodes)
Intuition
Instead of treating stops as nodes, we can treat bus routes as nodes. Two routes are connected if they share at least one common stop (you can transfer between them). This transforms the problem into finding the shortest path between any route containing the source stop and any route containing the target stop.
This approach can be more efficient when there are many stops but fewer routes, as we traverse through routes rather than individual stops.
Algorithm
- If
sourceequalstarget, return0. - Build a mapping from each stop to the list of routes serving it.
- Build an adjacency list between routes: two routes are neighbors if they share a stop.
- Start BFS from all routes that contain the source stop.
- For each level of BFS:
- Check if the current route contains the target stop; if so, return the current distance.
- Add all neighboring routes (connected via shared stops) to the queue.
- If BFS completes without finding a route containing the target, return
-1.
class Solution:
def numBusesToDestination(self, routes: List[List[int]], source: int, target: int) -> int:
if source == target:
return 0
n = len(routes)
adjList = [[] for _ in range(n)]
stopToRoutes = defaultdict(list)
for bus, route in enumerate(routes):
for stop in route:
stopToRoutes[stop].append(bus)
if target not in stopToRoutes or source not in stopToRoutes:
return -1
hasEdge = [[False] * n for _ in range(n)]
for buses in stopToRoutes.values():
for i in range(len(buses)):
for j in range(i + 1, len(buses)):
if hasEdge[buses[i]][buses[j]]:
continue
hasEdge[buses[i]][buses[j]] = True
hasEdge[buses[j]][buses[i]] = True
adjList[buses[i]].append(buses[j])
adjList[buses[j]].append(buses[i])
q = deque([node for node in stopToRoutes[source]])
res = 1
while q:
for _ in range(len(q)):
node = q.popleft()
if node in stopToRoutes[target]:
return res
while adjList[node]:
nxtBus = adjList[node].pop()
if adjList[nxtBus]:
q.append(nxtBus)
res += 1
return -1Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of routes and is the maximum number of stops per bus route.
Common Pitfalls
Forgetting the Source Equals Target Edge Case
If the source and target are the same stop, no bus is needed. Forgetting this check causes unnecessary computation or incorrect results when the BFS never finds the target in its traversal.
# Wrong: missing early return
def numBusesToDestination(self, routes, source, target):
# starts BFS without checking source == targetCounting Stops Instead of Bus Transfers
The problem asks for minimum number of buses, not minimum stops visited. A common mistake is incrementing the counter for each stop rather than each bus transfer.
# Wrong: incrementing for each stop
for nxtStop in routes[bus]:
q.append(nxtStop)
res += 1 # Should be outside the inner loopNot Tracking Both Visited Buses and Visited Stops
Only tracking visited stops leads to revisiting the same bus multiple times, causing TLE. Only tracking visited buses may revisit stops unnecessarily. Both sets are needed for efficiency.
# Wrong: only tracking stops
seen_stop = set()
# Missing: seen_bus = set()Incorrect BFS Level Counting
Incrementing the bus count at the wrong position (before checking if target is reached, or inside the wrong loop) leads to off-by-one errors in the result.
Building Inefficient Route Adjacency Graph
When using routes as nodes, failing to deduplicate edges between routes that share multiple stops creates redundant work and can cause memory issues with large inputs.