You are given a transit network represented by an array of bus routes. Each bus route is a list of stops that the bus visits. Buses follow their routes in a circular pattern, meaning you can board at any stop on the route and get off at any other stop on the same route.
Given a starting stop and a destination stop, determine the minimum number of buses you must take to travel from the source to the target. If it's impossible to reach the target, return -1.
Example 1:
Input: routes = [[1, 2, 7], [3, 6, 7]], source = 1, target = 6 Output: 2 Explanation: Take the first bus (route [1, 2, 7]) from stop 1 to stop 7. Then transfer to the second bus (route [3, 6, 7]) and ride to stop 6. Total buses taken: 2
Example 2:
Input: routes = [[7, 12], [4, 5, 15], [6], [15, 19], [9, 12, 13]], source = 15, target = 12 Output: 3 Explanation: One possible path requires 3 bus transfers to get from stop 15 to stop 12.
Example 3:
Input: routes = [[1, 2, 3], [4, 5, 6]], source = 1, target = 6 Output: -1 Explanation: There's no way to travel from stop 1 to stop 6 since the routes don't connect.
Hint 1: Graph Modeling Think of this problem as a graph traversal. What should the nodes represent? Consider treating each bus route as a node rather than each stop. Two route nodes are connected if they share at least one common stop.
Hint 2: Search Strategy Once you've modeled routes as nodes in a graph, you need to find the shortest path from any route containing the source to any route containing the target. What algorithm is best suited for finding the shortest path in an unweighted graph?
Hint 3: Optimization To efficiently find which routes share common stops, build a mapping from each stop to all routes that visit it. This preprocessing step will help you quickly identify which routes you can transfer to from your current route.
Full Solution `` Solution Explanation:
The key insight is to treat bus routes as nodes in a graph rather than individual stops. Two routes are connected if they share at least one common stop (transfer point).
Algorithm:
Preprocessing: Build a mapping from each stop to all routes that visit it. This allows us to quickly find transfer opportunities.
BFS Search: We perform breadth-first search starting from all routes that contain the source stop. BFS guarantees we find the shortest path (minimum transfers).
State Tracking:
- Track visited routes to avoid cycles
- Track visited stops to avoid redundant processing
- Each BFS level represents one additional bus taken
Early Exit: As soon as we encounter the target stop on any route we're exploring, we return the current bus count.
Time Complexity: O(N * S) where N is the number of routes and S is the total number of stops across all routes. In the worst case, we visit each route once and process all its stops.
Space Complexity: O(N * S) for the stop-to-routes mapping and visited sets.
This approach is efficient because it focuses on route-level transfers rather than exploring every possible stop-to-stop path, which would be much slower for large networks.
from collections import deque, defaultdict
def numBusesToDestination(routes: list[list[int]], source: int, target: int) -> int:
# Edge case: already at target
if source == target:
return 0
# Build a mapping: stop -> list of route indices that visit this stop
stop_to_routes = defaultdict(set)
for route_idx, route in enumerate(routes):
for stop in route:
stop_to_routes[stop].add(route_idx)
# Check if source or target are reachable at all
if source not in stop_to_routes or target not in stop_to_routes:
return -1
# BFS on routes (not stops!)
# Each element in queue: (route_index, number_of_buses_taken)
queue = deque()
visited_routes = set()
# Initialize: add all routes that contain the source stop
for route_idx in stop_to_routes[source]:
queue.append((route_idx, 1))
visited_routes.add(route_idx)
# Track which stops we've already processed to avoid redundant work
visited_stops = {source}
while queue:
current_route_idx, bus_count = queue.popleft()
# Check all stops on the current route
for stop in routes[current_route_idx]:
# If we reached the target, return the bus count
if stop == target:
return bus_count
# Skip stops we've already processed
if stop in visited_stops:
continue
visited_stops.add(stop)
# Add all routes connected to this stop (that we haven't visited)
for next_route_idx in stop_to_routes[stop]:
if next_route_idx not in visited_routes:
visited_routes.add(next_route_idx)
queue.append((next_route_idx, bus_count + 1))
# No path found
return -1