Bus Routes

Hard BFS

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

Input: routes = [[1,2,7],[3,6,7]], source = 1, target = 6
Output: 2

Starting 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

1class 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

1

Build Stop-to-Bus Mapping to Enable Fast Bus Lookup

6stop_to_buses = defaultdict(list)
7for 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.

2

Initialize BFS State with Source Stop and Tracking Structures

11queue = deque([source])
12visited_stops = {source}
13used_buses = set()
14buses_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.

3

Perform BFS to Explore Stops Layer by Layer by Taking Buses

16while 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.

4

Return -1 if Target Stop is Unreachable

36return -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.

Line 29 Critical
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.

Line 30 Critical
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.

Line 9 Critical
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.

Line 17 Critical
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.

Line 23 Critical
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.

Line 26 Critical
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.

Line 33 Critical
visited_stops.add(next_stop)

Mark the next stop as visited.

Recording visited stops prevents revisiting and infinite loops during BFS traversal.

Line 6 Core
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.

Line 12 Core
visited_stops = {source}

Initialize a set to track visited stops.

Tracking visited stops prevents revisiting and infinite loops during BFS.

Line 13 Core
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.

Line 20 Core
stop = queue.popleft()

Dequeue the next stop to process.

FIFO queue ensures BFS explores stops in order of increasing bus transfers.

Line 22 Core
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.

Line 28 Core
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.

Line 32 Core
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.

Line 34 Core
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.

Line 3 Core
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.

Line 11 Core
queue = deque([source])

Initialize BFS queue with the source stop.

Starting BFS from the source models the initial position in the traversal graph.

Line 16 Core
while queue:

Continue BFS while there are stops to explore.

The loop processes stops layer by layer, corresponding to increasing bus transfers.

Line 19 Core
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.

Line 24 Core
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.

Line 36 Core
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.

Line 7 Standard
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.

Line 8 Standard
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.

Line 4 Standard
return 0

Return 0 when source equals target.

This early return prevents unnecessary computation when no travel is needed.

Line 14 Standard
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

or drill a free problem