Bus Routes
Problem
Given a list of bus routes where each route is a list of stops, and two stops source and target, return the minimum number of buses one must take to travel from source to target. Return -1 if it is not possible.
- 1 ≤ routes.length ≤ 500
- 1 ≤ routes[i].length ≤ 10⁵
- 0 ≤ routes[i][j] < 10⁶
- 0 ≤ source, target < 10⁶
Example
routes = [[1,2,7],[3,6,7]], source = 1, target = 62Starting at stop 1, take the first bus (route 0) which visits stops 1, 2, and 7. At stop 7, transfer to the second bus (route 1) which visits stops 3, 6, and 7. This requires taking 2 buses in total. The BFS explores stops reachable by taking buses from the source, expanding layer by layer until the target stop is found.
Approach
Straightforward Solution
A brute-force approach would attempt to explore all possible sequences of stops and buses, which is computationally infeasible due to the large input size and potential cycles.
Core Observation
The problem can be viewed as a graph traversal where nodes are bus stops and edges represent the ability to travel between stops via a bus route. The goal is to find the shortest path in terms of number of buses taken, not stops visited.
Path to Optimal
The key insight is to use Breadth-First Search (BFS) starting from the source stop, exploring all stops reachable by taking one bus, then all stops reachable by taking two buses, and so forth. To efficiently find which buses can be taken from a stop, build a mapping from stops to the buses that visit them. Track visited stops and used buses to avoid redundant exploration and infinite loops.
Optimal Approach
Construct a mapping from each stop to the list of buses that visit it. Use a BFS queue initialized with the source stop. At each BFS layer, increment the bus count and explore all buses reachable from the current stops that have not been used yet. For each such bus, enqueue all its stops that have not been visited. If the target stop is found during this process, return the current bus count. If BFS completes without finding the target, return -1.
Time
O(N + E)
Building the stop-to-bus mapping takes O(N) where N is the total number of stops across all routes. BFS explores each stop and bus at most once, resulting in O(N + E) time, where E is the total number of edges (bus-stop connections).
Space
O(N + E)
The stop-to-bus mapping and BFS queue require O(N + E) space, where N is the number of stops and E is the number of bus-stop edges. The visited sets for stops and buses also scale with input size.
Pattern Spotlight
BFS (Shortest Path on Unweighted Graph)
When the problem asks for the minimum number of steps or transitions in a network with uniform edge cost, model the problem as a graph and use BFS to explore layer by layer, ensuring the first time you reach the target is the shortest path.
Solution
| 1 | class Solution: |
| 2 | def numBusesToDestination(self, routes: List[List[int]], source: int, target: int) -> int: |
| 3 | if source == target: |
| 4 | return 0 |
| 5 | |
| 6 | stop_to_buses = defaultdict(list) |
| 7 | for bus, stops in enumerate(routes): |
| 8 | for stop in stops: |
| 9 | stop_to_buses[stop].append(bus) |
| 10 | |
| 11 | queue = deque([source]) |
| 12 | visited_stops = {source} |
| 13 | used_buses = set() |
| 14 | buses_taken = 0 |
| 15 | |
| 16 | while queue: |
| 17 | buses_taken += 1 |
| 18 | |
| 19 | for _ in range(len(queue)): |
| 20 | stop = queue.popleft() |
| 21 | |
| 22 | for bus in stop_to_buses[stop]: |
| 23 | if bus in used_buses: |
| 24 | continue |
| 25 | |
| 26 | used_buses.add(bus) |
| 27 | |
| 28 | for next_stop in routes[bus]: |
| 29 | if next_stop == target: |
| 30 | return buses_taken |
| 31 | |
| 32 | if next_stop not in visited_stops: |
| 33 | visited_stops.add(next_stop) |
| 34 | queue.append(next_stop) |
| 35 | |
| 36 | return -1 |
Step-by-Step Solution
Build Stop-to-Bus Mapping to Enable Fast Bus Lookup
| 6 | stop_to_buses = defaultdict(list) |
| 7 | for bus, stops in enumerate(routes): |
| 8 | for stop in stops: |
| 9 | stop_to_buses[stop].append(bus) |
Objective
To create a mapping from each stop to the list of buses that visit it, enabling quick lookup of buses available at any stop.
Key Insight
Directly searching which buses can be taken from a stop is expensive without indexing. By precomputing a dictionary mapping stops to buses, the algorithm can instantly find all buses accessible from any stop during BFS traversal, reducing redundant searches and improving efficiency.
Interview Quick-Check
Core Logic
The stop-to-bus mapping transforms the problem into a graph where edges connect stops to buses, enabling BFS to traverse stops via buses efficiently.
Common Pitfalls & Bugs
Failing to build this mapping leads to repeated scanning of all routes for each stop, causing timeouts.
Initialize BFS State with Source Stop and Tracking Structures
| 11 | queue = deque([source]) |
| 12 | visited_stops = {source} |
| 13 | used_buses = set() |
| 14 | buses_taken = 0 |
Objective
To set up the BFS queue starting from the source stop and initialize visited sets for stops and buses to avoid revisiting.
Key Insight
Starting BFS from the source stop models the initial position. Tracking visited stops prevents cycles and redundant exploration, while tracking used buses ensures each bus route is only taken once, preventing infinite loops and repeated work.
Interview Quick-Check
Core Logic
Visited sets for stops and buses are essential to prune the search space and guarantee BFS correctness and efficiency.
State & Boundaries
The BFS queue holds stops to explore at the current bus transfer level.
Perform BFS to Explore Stops Layer by Layer by Taking Buses
| 16 | while queue: |
| 17 | buses_taken += 1 |
| 19 | for _ in range(len(queue)): |
| 20 | stop = queue.popleft() |
| 22 | for bus in stop_to_buses[stop]: |
| 23 | if bus in used_buses: |
| 24 | continue |
| 26 | used_buses.add(bus) |
| 28 | for next_stop in routes[bus]: |
| 29 | if next_stop == target: |
| 30 | return buses_taken |
| 32 | if next_stop not in visited_stops: |
| 33 | visited_stops.add(next_stop) |
| 34 | queue.append(next_stop) |
Objective
To iteratively explore stops reachable by taking an increasing number of buses, returning the bus count when the target stop is found.
Key Insight
BFS expands the frontier of reachable stops by taking all buses available at the current stops. By incrementing the bus count at each BFS layer, the algorithm naturally counts the minimum number of buses needed. Marking buses as used prevents revisiting the same bus route multiple times, and marking stops as visited avoids redundant enqueues.
Interview Quick-Check
Core Logic
Incrementing the bus count at each BFS layer corresponds to taking one more bus, ensuring the first time the target is found is the minimal bus count.
State & Boundaries
The inner loop processes all stops at the current BFS level, ensuring layer-by-layer expansion.
Common Pitfalls & Bugs
Forgetting to mark buses as used or stops as visited can cause infinite loops or redundant processing.
Complexity
Each bus and stop is processed at most once, ensuring O(N + E) time complexity.
Return -1 if Target Stop is Unreachable
| 36 | return -1 |
Objective
To return -1 when BFS completes without finding the target stop, indicating no possible route.
Key Insight
If BFS exhausts all reachable stops without encountering the target, it proves the target is unreachable from the source given the bus routes. Returning -1 signals this impossibility clearly.
Interview Quick-Check
State & Boundaries
This return is only reached if the BFS queue empties without finding the target.
Line Analysis
This solution has 7 Critical lines interviewers watch for.
if next_stop == target:
Check if the next stop is the target stop.
Finding the target stop means the shortest path in terms of buses taken has been found, allowing immediate return.
return buses_taken
Return the current bus count as the minimum buses needed.
Returning immediately upon finding the target stop ensures the BFS shortest path property is preserved and the minimal bus count is returned.
stop_to_buses[stop].append(bus)
Append the current bus to the list of buses for this stop.
This line populates the stop-to-bus mapping, enabling quick access to buses from any stop during BFS.
buses_taken += 1
Increment the bus count at the start of each BFS layer.
Each BFS layer corresponds to taking one additional bus, so incrementing here tracks the number of buses taken.
if bus in used_buses:
Skip buses that have already been used.
Avoiding reuse of buses prevents cycles and redundant exploration, ensuring BFS efficiency and correctness.
used_buses.add(bus)
Mark the current bus as used.
Recording the bus as used ensures it won't be considered again in future BFS layers, pruning the search space.
visited_stops.add(next_stop)
Mark the next stop as visited.
Recording visited stops prevents revisiting and infinite loops during BFS traversal.
stop_to_buses = defaultdict(list)
Initialize a dictionary mapping stops to buses that visit them.
This data structure enables O(1) average-time lookup of buses available at any stop, which is critical for efficient BFS traversal.
visited_stops = {source}
Initialize a set to track visited stops.
Tracking visited stops prevents revisiting and infinite loops during BFS.
used_buses = set()
Initialize a set to track buses already taken.
Marking buses as used ensures each bus route is only considered once, avoiding redundant exploration.
stop = queue.popleft()
Dequeue the next stop to process.
FIFO queue ensures BFS explores stops in order of increasing bus transfers.
for bus in stop_to_buses[stop]:
Iterate over all buses that visit the current stop.
Exploring all buses available at the current stop expands the BFS frontier to new stops reachable by taking these buses.
for next_stop in routes[bus]:
Iterate over all stops served by the current bus.
Exploring all stops reachable by the current bus expands the BFS frontier to new stops that can be reached with one more bus transfer.
if next_stop not in visited_stops:
Check if the next stop has not been visited yet.
Only enqueue unvisited stops to prevent cycles and redundant BFS exploration.
queue.append(next_stop)
Enqueue the next stop for BFS exploration.
Adding the stop to the queue schedules it for exploration in the next BFS layers, expanding the search frontier.
if source == target:
Check if source and target stops are the same.
If source equals target, no buses need to be taken, so returning 0 immediately handles this trivial case efficiently.
queue = deque([source])
Initialize BFS queue with the source stop.
Starting BFS from the source models the initial position in the traversal graph.
while queue:
Continue BFS while there are stops to explore.
The loop processes stops layer by layer, corresponding to increasing bus transfers.
for _ in range(len(queue)):
Iterate over all stops at the current BFS level.
Processing all stops at the current bus transfer level ensures BFS explores layer by layer, guaranteeing shortest path in terms of buses taken.
continue
Continue to next bus if current bus is already used.
This control flow prevents processing the same bus multiple times, which could cause infinite loops or redundant work.
return -1
Return -1 if the target stop is unreachable after BFS completes.
If BFS exhausts all reachable stops without finding the target, it proves no route exists, so returning -1 correctly signals impossibility.
for bus, stops in enumerate(routes):
Enumerate each bus route and its stops.
Enumerating routes with indices allows associating stops with their bus IDs, which is necessary for tracking used buses.
for stop in stops:
Iterate through each stop in the current bus route.
Visiting each stop allows building the reverse mapping from stop to buses.
return 0
Return 0 when source equals target.
This early return prevents unnecessary computation when no travel is needed.
buses_taken = 0
Initialize the count of buses taken to zero.
This variable tracks the number of bus transfers made during BFS traversal.
Test Your Understanding
Why does BFS guarantee the minimum number of buses taken to reach the target stop?
Because BFS explores all stops reachable with k buses before exploring those reachable with k+1 buses, the first time the target stop is reached corresponds to the shortest path in terms of bus transfers.
Related Problems
BFS pattern
Don't just read it. Drill it.
Reconstruct Bus Routes from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.
Unlock the Bus Routes drill