815. Bus Routes
Problem Description
You have multiple bus routes, where each bus travels in a circular loop through a sequence of stops forever. For example, if a bus route is [1, 5, 7]
, the bus travels: 1 β 5 β 7 β 1 β 5 β 7 β 1 β ...
continuously.
Given:
- An array
routes
whereroutes[i]
represents the stops that thei-th
bus visits in its circular route - A
source
bus stop where you start (you're initially not on any bus) - A
target
bus stop where you want to go
You can only travel between bus stops by taking buses. Your goal is to find the minimum number of buses you need to take to travel from source
to target
.
Return the least number of buses required, or -1
if it's impossible to reach the target from the source.
Key Points:
- Each bus follows its route in a circular pattern forever
- You start at a bus stop (not on a bus)
- You can transfer between buses at common stops
- The answer is the count of different buses used, not the number of stops
Example:
If you have routes [[1, 2, 7], [3, 6, 7]]
and want to go from stop 1
to stop 6
:
- Take bus 0 from stop
1
to stop7
- Transfer to bus 1 at stop
7
and ride to stop6
- Total buses taken: 2
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: This problem can be modeled as a graph where bus stops are nodes and bus routes create connections between stops. We need to find the minimum number of bus transfers (edges in our graph representation) to get from source to target.
Is it a tree?
- No: The graph is not a tree because multiple buses can connect the same stops, creating cycles and multiple paths between nodes.
Is the problem related to directed acyclic graphs?
- No: The graph has cycles since buses travel in circular routes and multiple buses can share common stops.
Is the problem related to shortest paths?
- Yes: We need to find the minimum number of buses (shortest path in terms of bus transfers) from source to target.
Is the graph weighted?
- No: Each bus transfer has the same "cost" of 1. We're counting the number of buses taken, not varying weights between connections.
BFS
- Yes: Since we have an unweighted graph shortest path problem, BFS is the optimal choice.
Conclusion: The flowchart leads us to use BFS (Breadth-First Search) for this problem. BFS is perfect here because:
- We're looking for the shortest path in an unweighted graph
- BFS explores nodes level by level, guaranteeing the first time we reach the target will be via the minimum number of buses
- The problem asks for the minimum number of transfers, which BFS naturally provides by tracking the depth/level of exploration
Intuition
The key insight is to think of this problem not as traveling through individual stops, but as traveling through buses. Each time we board a new bus, that counts as one unit toward our answer.
Initially, we might think to model this as a graph where stops are nodes and direct connections between consecutive stops on the same route are edges. However, this would make finding the minimum number of buses complex, as we'd need to track which bus we're currently on.
Instead, we can restructure our thinking: from any stop, we can reach all other stops on the same bus route with just one bus ride. This means if we're at stop A
and want to reach stop B
on the same bus route, it takes exactly 1 bus, regardless of how many stops are between them.
This leads to a more efficient graph representation where:
- We first map each stop to all the buses that visit it
- When we're at a stop, we can explore all buses passing through that stop
- For each unvisited bus, we can reach all stops on that bus route with one additional bus ride
The BFS approach naturally fits because:
- We start from the source stop with 0 buses taken
- We explore all buses reachable from our current stop
- For each bus, we add all its stops to our queue with
bus_count + 1
- The first time we reach the target stop, we've found the minimum number of buses
To avoid revisiting, we track:
vis_bus
: Buses we've already boarded (no need to board the same bus twice)vis_stop
: Stops we've already visited (prevents infinite loops)
This way, BFS guarantees that when we first reach the target, we've taken the minimum number of buses possible.
Learn more about Breadth-First Search patterns.
Solution Approach
Following our BFS intuition, let's walk through the implementation step by step:
1. Handle Base Cases
if source == target: return 0
If we're already at the target, no buses are needed.
2. Build Stop-to-Bus Mapping
g = defaultdict(list)
for i, route in enumerate(routes):
for stop in route:
g[stop].append(i)
We create a hash table g
where g[stop]
contains all bus indices that visit that stop. This allows us to quickly find which buses we can board from any given stop.
3. Validate Source and Target
if source not in g or target not in g: return -1
If either the source or target stop isn't serviced by any bus, it's impossible to make the journey.
4. Initialize BFS
q = [(source, 0)]
vis_bus = set()
vis_stop = {source}
- Queue
q
stores tuples of(stop, bus_count)
representing our current position and buses taken vis_bus
tracks which buses we've already boarded to avoid redundant explorationvis_stop
tracks visited stops to prevent revisiting the same stop
5. BFS Traversal
for stop, bus_count in q: if stop == target: return bus_count
We process the queue level by level. If we reach the target, we immediately return the bus count since BFS guarantees this is the minimum.
6. Explore Available Buses
for bus in g[stop]: if bus not in vis_bus: vis_bus.add(bus) for next_stop in routes[bus]: if next_stop not in vis_stop: vis_stop.add(next_stop) q.append((next_stop, bus_count + 1))
For each unvisited bus at the current stop:
- Mark the bus as visited
- Add all unvisited stops on this bus route to the queue with
bus_count + 1
- This represents taking one more bus to reach those stops
7. No Path Found
return -1
If we exhaust the queue without reaching the target, no path exists.
Time Complexity: O(N * M)
where N
is the number of buses and M
is the average number of stops per bus. We visit each bus at most once and process all its stops.
Space Complexity: O(N * M)
for storing the graph mapping and visited sets.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example:
- Routes:
[[1, 2, 3], [3, 4, 5], [5, 6]]
- Source:
1
- Target:
6
Step 1: Build Stop-to-Bus Mapping We create a mapping of which buses visit each stop:
Stop 1: [Bus 0] Stop 2: [Bus 0] Stop 3: [Bus 0, Bus 1] Stop 4: [Bus 1] Stop 5: [Bus 1, Bus 2] Stop 6: [Bus 2]
Step 2: Initialize BFS
- Queue:
[(1, 0)]
- starting at stop 1 with 0 buses taken - Visited buses:
{}
- Visited stops:
{1}
Step 3: First Iteration (Process stop 1)
- Current: stop 1, bus_count = 0
- Not the target (6), so continue
- Buses available at stop 1: [Bus 0]
- Board Bus 0 (mark as visited)
- Add all stops from Bus 0 to queue:
- Stop 2 (not visited) β add
(2, 1)
to queue - Stop 3 (not visited) β add
(3, 1)
to queue - Stop 1 (already visited) β skip
- Stop 2 (not visited) β add
Queue now: [(2, 1), (3, 1)]
Visited buses: {0}
Visited stops: {1, 2, 3}
Step 4: Second Iteration (Process stop 2)
- Current: stop 2, bus_count = 1
- Not the target, continue
- Buses at stop 2: [Bus 0]
- Bus 0 already visited β skip
Step 5: Third Iteration (Process stop 3)
- Current: stop 3, bus_count = 1
- Not the target, continue
- Buses at stop 3: [Bus 0, Bus 1]
- Bus 0 already visited β skip
- Board Bus 1 (mark as visited)
- Add stops from Bus 1:
- Stop 4 (not visited) β add
(4, 2)
to queue - Stop 5 (not visited) β add
(5, 2)
to queue - Stop 3 (already visited) β skip
- Stop 4 (not visited) β add
Queue now: [(4, 2), (5, 2)]
Visited buses: {0, 1}
Visited stops: {1, 2, 3, 4, 5}
Step 6: Fourth Iteration (Process stop 4)
- Current: stop 4, bus_count = 2
- Not the target, continue
- Buses at stop 4: [Bus 1]
- Bus 1 already visited β skip
Step 7: Fifth Iteration (Process stop 5)
- Current: stop 5, bus_count = 2
- Not the target, continue
- Buses at stop 5: [Bus 1, Bus 2]
- Bus 1 already visited β skip
- Board Bus 2 (mark as visited)
- Add stops from Bus 2:
- Stop 6 (not visited) β add
(6, 3)
to queue - Stop 5 (already visited) β skip
- Stop 6 (not visited) β add
Queue now: [(6, 3)]
Visited buses: {0, 1, 2}
Visited stops: {1, 2, 3, 4, 5, 6}
Step 8: Sixth Iteration (Process stop 6)
- Current: stop 6, bus_count = 3
- This is our target!
- Return 3
Answer: 3 buses needed
- Bus 0: Travel from stop 1 to stop 3
- Bus 1: Travel from stop 3 to stop 5
- Bus 2: Travel from stop 5 to stop 6
The BFS approach ensures we find the minimum number of buses by exploring all possibilities level by level, where each level represents taking one additional bus.
Solution Implementation
1from collections import defaultdict, deque
2from typing import List
3
4class Solution:
5 def numBusesToDestination(
6 self, routes: List[List[int]], source: int, target: int
7 ) -> int:
8 # If already at destination, no buses needed
9 if source == target:
10 return 0
11
12 # Build adjacency list: stop -> list of bus indices that visit this stop
13 stop_to_buses = defaultdict(list)
14 for bus_index, route in enumerate(routes):
15 for stop in route:
16 stop_to_buses[stop].append(bus_index)
17
18 # Check if source and target are reachable by any bus
19 if source not in stop_to_buses or target not in stop_to_buses:
20 return -1
21
22 # BFS queue: stores (current_stop, number_of_buses_taken)
23 queue = deque([(source, 0)])
24
25 # Track visited buses to avoid revisiting the same bus route
26 visited_buses = set()
27
28 # Track visited stops to avoid revisiting the same stop
29 visited_stops = {source}
30
31 # BFS traversal
32 while queue:
33 current_stop, buses_taken = queue.popleft()
34
35 # Check if we've reached the target
36 if current_stop == target:
37 return buses_taken
38
39 # Explore all buses that visit the current stop
40 for bus_index in stop_to_buses[current_stop]:
41 # Skip if we've already taken this bus
42 if bus_index in visited_buses:
43 continue
44
45 # Mark this bus as visited
46 visited_buses.add(bus_index)
47
48 # Add all unvisited stops on this bus route to the queue
49 for next_stop in routes[bus_index]:
50 if next_stop not in visited_stops:
51 visited_stops.add(next_stop)
52 queue.append((next_stop, buses_taken + 1))
53
54 # Target is unreachable
55 return -1
56
1class Solution {
2 public int numBusesToDestination(int[][] routes, int source, int target) {
3 // If source and target are the same, no bus needed
4 if (source == target) {
5 return 0;
6 }
7
8 // Build graph: stop -> list of bus indices that pass through this stop
9 Map<Integer, List<Integer>> stopToBuses = new HashMap<>();
10 for (int busIndex = 0; busIndex < routes.length; busIndex++) {
11 for (int stop : routes[busIndex]) {
12 stopToBuses.computeIfAbsent(stop, k -> new ArrayList<>()).add(busIndex);
13 }
14 }
15
16 // Check if source and target stops exist in any bus route
17 if (!stopToBuses.containsKey(source) || !stopToBuses.containsKey(target)) {
18 return -1;
19 }
20
21 // BFS initialization
22 // Queue stores [current stop, number of buses taken to reach here]
23 Deque<int[]> queue = new ArrayDeque<>();
24 Set<Integer> visitedBuses = new HashSet<>();
25 Set<Integer> visitedStops = new HashSet<>();
26
27 // Start BFS from source stop with 0 buses taken
28 queue.offer(new int[] {source, 0});
29 visitedStops.add(source);
30
31 // BFS traversal
32 while (!queue.isEmpty()) {
33 int[] current = queue.poll();
34 int currentStop = current[0];
35 int busCount = current[1];
36
37 // Check if we reached the target
38 if (currentStop == target) {
39 return busCount;
40 }
41
42 // Explore all buses that pass through current stop
43 for (int busIndex : stopToBuses.get(currentStop)) {
44 // Skip if this bus has already been used
45 if (visitedBuses.add(busIndex)) {
46 // Explore all stops reachable by this bus
47 for (int nextStop : routes[busIndex]) {
48 // Add unvisited stops to queue
49 if (visitedStops.add(nextStop)) {
50 queue.offer(new int[] {nextStop, busCount + 1});
51 }
52 }
53 }
54 }
55 }
56
57 // Target is unreachable
58 return -1;
59 }
60}
61
1class Solution {
2public:
3 int numBusesToDestination(vector<vector<int>>& routes, int source, int target) {
4 // If source and target are the same, no bus needed
5 if (source == target) {
6 return 0;
7 }
8
9 // Build adjacency list: stop -> list of buses that visit this stop
10 unordered_map<int, vector<int>> stopToBuses;
11 for (int busIndex = 0; busIndex < routes.size(); busIndex++) {
12 for (int stop : routes[busIndex]) {
13 stopToBuses[stop].push_back(busIndex);
14 }
15 }
16
17 // Check if source and target stops are reachable by any bus
18 if (!stopToBuses.contains(source) || !stopToBuses.contains(target)) {
19 return -1;
20 }
21
22 // BFS setup: queue stores (current stop, number of buses taken)
23 queue<pair<int, int>> bfsQueue;
24 unordered_set<int> visitedBuses; // Track visited buses to avoid revisiting
25 unordered_set<int> visitedStops; // Track visited stops to avoid cycles
26
27 // Start BFS from source stop with 0 buses taken
28 bfsQueue.push({source, 0});
29 visitedStops.insert(source);
30
31 // BFS traversal
32 while (!bfsQueue.empty()) {
33 auto [currentStop, busCount] = bfsQueue.front();
34 bfsQueue.pop();
35
36 // Check if we reached the target
37 if (currentStop == target) {
38 return busCount;
39 }
40
41 // Explore all buses that pass through current stop
42 for (int busIndex : stopToBuses[currentStop]) {
43 // Skip if we've already taken this bus
44 if (!visitedBuses.contains(busIndex)) {
45 // Mark this bus as visited
46 visitedBuses.insert(busIndex);
47
48 // Explore all stops on this bus route
49 for (int nextStop : routes[busIndex]) {
50 // Only visit unvisited stops
51 if (!visitedStops.contains(nextStop)) {
52 visitedStops.insert(nextStop);
53 bfsQueue.push({nextStop, busCount + 1});
54 }
55 }
56 }
57 }
58 }
59
60 // Target is unreachable
61 return -1;
62 }
63};
64
1/**
2 * Finds the minimum number of buses needed to travel from source to target stop
3 * @param routes - Array where routes[i] contains all stops for bus route i
4 * @param source - Starting bus stop
5 * @param target - Destination bus stop
6 * @returns Minimum number of buses needed, or -1 if impossible
7 */
8function numBusesToDestination(routes: number[][], source: number, target: number): number {
9 // If already at destination, no buses needed
10 if (source === target) {
11 return 0;
12 }
13
14 // Build graph: stop -> list of bus routes that visit this stop
15 const stopToBusRoutes: Map<number, number[]> = new Map();
16 for (let busIndex = 0; busIndex < routes.length; busIndex++) {
17 for (const stop of routes[busIndex]) {
18 if (!stopToBusRoutes.has(stop)) {
19 stopToBusRoutes.set(stop, []);
20 }
21 stopToBusRoutes.get(stop)!.push(busIndex);
22 }
23 }
24
25 // Check if source and target stops are reachable by any bus
26 if (!stopToBusRoutes.has(source) || !stopToBusRoutes.has(target)) {
27 return -1;
28 }
29
30 // BFS queue: [current stop, number of buses taken so far]
31 const queue: [number, number][] = [[source, 0]];
32
33 // Track visited bus routes to avoid revisiting
34 const visitedBusRoutes: Set<number> = new Set();
35
36 // Track visited stops to avoid revisiting
37 const visitedStops: Set<number> = new Set([source]);
38
39 // Process BFS queue
40 for (const [currentStop, busCount] of queue) {
41 // Check if we've reached the target
42 if (currentStop === target) {
43 return busCount;
44 }
45
46 // Explore all bus routes that visit current stop
47 for (const busRoute of stopToBusRoutes.get(currentStop)!) {
48 // Skip if we've already taken this bus route
49 if (!visitedBusRoutes.has(busRoute)) {
50 visitedBusRoutes.add(busRoute);
51
52 // Add all unvisited stops from this bus route to queue
53 for (const nextStop of routes[busRoute]) {
54 if (!visitedStops.has(nextStop)) {
55 visitedStops.add(nextStop);
56 queue.push([nextStop, busCount + 1]);
57 }
58 }
59 }
60 }
61 }
62
63 // Target is unreachable
64 return -1;
65}
66
Time and Space Complexity
Time Complexity: O(L)
, where L
is the total number of stops across all bus routes.
The algorithm builds a graph mapping each stop to its bus routes, which takes O(L)
time since we iterate through every stop in every route once. In the BFS traversal, each bus route is visited at most once (tracked by vis_bus
), and each stop is visited at most once (tracked by vis_stop
). When a bus is visited, we iterate through all its stops. Since each stop belongs to at least one bus route and we process each stop-bus pair once, the total work done is proportional to the sum of all stops across all routes, which is O(L)
.
Space Complexity: O(L)
, where L
is the total number of stops across all bus routes.
The graph g
stores mappings from stops to bus indices. In the worst case, if every stop appears in every route, the graph would store O(L)
entries. The vis_bus
set can contain at most O(n)
buses (where n
is the number of routes), and vis_stop
can contain at most O(L)
unique stops. The queue q
can also hold at most O(L)
stops in the worst case. Since the number of routes n
is bounded by L
(as each route must have at least one stop), the overall space complexity is O(L)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Treating Stops as Graph Nodes Instead of Buses
A common mistake is building a graph where stops are nodes and edges connect stops on the same bus route. This leads to counting stops instead of buses.
Wrong Approach:
# Building stop-to-stop graph (INCORRECT)
graph = defaultdict(set)
for route in routes:
for i in range(len(route)):
for j in range(len(route)):
if i != j:
graph[route[i]].add(route[j])
Why it's wrong: This counts the number of stop transitions, not the number of buses taken. You could visit many stops on the same bus but it should only count as 1 bus.
Correct Approach: Treat buses as the primary unit of traversal. When you board a bus, you can reach all stops on that bus with just that one bus.
2. Not Tracking Visited Buses
Without tracking visited buses, the algorithm might repeatedly explore the same bus route from different stops, causing inefficiency or infinite loops.
Wrong Implementation:
# Only tracking visited stops (INCOMPLETE) visited_stops = {source} for stop, bus_count in queue: for bus in stop_to_buses[stop]: # Missing: if bus in visited_buses: continue for next_stop in routes[bus]: if next_stop not in visited_stops: visited_stops.add(next_stop) queue.append((next_stop, bus_count + 1))
Problem: If bus 0 visits stops [1, 2, 3], you might explore bus 0 from stop 1, then again from stop 2, leading to redundant work.
Solution: Always maintain a visited_buses
set and check it before exploring any bus route.
3. Incrementing Bus Count at Wrong Time
A subtle error is incrementing the bus count when you reach a stop rather than when you board a new bus.
Wrong:
if current_stop == target: return buses_taken + 1 # WRONG: adds extra bus
Correct:
# Increment only when adding stops from a NEW bus to the queue queue.append((next_stop, buses_taken + 1))
4. Using BFS Level-by-Level Instead of Simple Queue Processing
Some implementations try to process BFS level-by-level to track bus counts, which adds unnecessary complexity.
Overcomplicated:
level = 0
while queue:
size = len(queue)
for _ in range(size):
stop = queue.popleft()
if stop == target:
return level
# ... explore neighbors
level += 1
Better: Store the bus count directly with each stop in the queue as a tuple (stop, bus_count)
. This is cleaner and less error-prone.
5. Forgetting Circular Nature Doesn't Matter
Some solutions try to handle the circular nature of routes specially, but it's unnecessary for this problem.
Unnecessary Complexity:
# Trying to handle circular routes specially for route in routes: # Adding reverse connections or duplicating routes extended_route = route + route # UNNECESSARY
Why it's unnecessary: Since we can board a bus at any stop and travel to any other stop on that route, the circular nature is implicitly handled. We just need to know which stops each bus visits.
How does quick sort divide the problem into subproblems?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!