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

  1. If source equals target, return 0 (no bus needed).
  2. Build a mapping from each stop to the list of bus routes that serve it.
  3. Initialize BFS from the source stop, tracking visited stops and visited buses.
  4. 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.
  5. If we reach the target stop, return the number of bus rides taken.
  6. 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 -1

Time & Space Complexity

  • Time complexity: O(nm)O(n * m)
  • Space complexity: O(nm)O(n * m)

Where nn is the number of routes and mm 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

  1. If source equals target, return 0.
  2. Build a mapping from each stop to the list of routes serving it.
  3. Build an adjacency list between routes: two routes are neighbors if they share a stop.
  4. Start BFS from all routes that contain the source stop.
  5. 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.
  6. 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 -1

Time & Space Complexity

  • Time complexity: O(n2+nm)O(n ^ 2 + n * m)
  • Space complexity: O(n2+nm)O(n ^ 2 + n * m)

Where nn is the number of routes and mm 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 == target

Counting 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 loop

Not 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.