Skip to content
CCC Python Course

BFS variants

Module
M5.9
Lesson
1 of 1
Reading time
5 min

In this lesson

  • Implement multi-source BFS starting from several nodes at once.
  • Use 0-1 BFS with deque to handle graphs with edge weights 0 and 1.
  • Detect and process forced zero-cost moves (conveyors) with cycle detection.
  • Apply reverse-graph BFS to find distances backward from a target.

Standard BFS explores a single starting node, one distance layer at a time. Four variations adapt that same idea to more specific problem structures. You can start from several nodes at once, handle edge weights of 0 and 1 without a heap, follow forced zero-cost moves safely, or search backward from a target instead of forward from a source.

Multi-source BFS

When several nodes are equally good starting points, initialize the queue with all of them at distance 0. BFS then explores outward from all sources in parallel. This is useful when you need the closest source to every other node, or the shortest distance from any source.

examples/multisource_bfs.py
from collections import deque

def multisource_bfs(adj, sources):    """BFS from multiple sources at once."""    n = len(adj)    dist = [-1] * n    queue = deque()
    for src in sources:        dist[src] = 0        queue.append(src)
    while queue:        u = queue.popleft()        for v in adj[u]:            if dist[v] == -1:                dist[v] = dist[u] + 1                queue.append(v)
    return dist

adj = [    [1, 2],    [0, 3],    [0, 4],    [1, 5],    [2, 5],    [3, 4]]
distances = multisource_bfs(adj, [0, 3])print("Distances from sources [0, 3]:", distances)

Output

Distances from sources [0, 3]: [0, 1, 1, 0, 2, 1]
Start BFS from all sources at once

Multi-source BFS assigns each node its distance to the nearest source. The algorithm is identical to single-source BFS except you push multiple nodes into the initial queue.

0-1 BFS with deque

When edges have weight 0 or 1 only, you do not need a full priority queue (heap). Instead, use a deque (double-ended queue).

For each edge:

  • If the edge weight is 0, push the neighbor to the front (appendleft).
  • If the edge weight is 1, push the neighbor to the back (append).

This ensures nodes are processed in non-decreasing order of distance without sorting.

examples/zero_one_bfs.py
from collections import deque

def zero_one_bfs(adj, start):    """0-1 BFS using deque with appendleft for weight-0 edges."""    n = len(adj)    dist = [float('inf')] * n    dist[start] = 0    queue = deque([start])
    while queue:        u = queue.popleft()        for v, w in adj[u]:            if dist[u] + w < dist[v]:                dist[v] = dist[u] + w                if w == 0:                    queue.appendleft(v)                else:                    queue.append(v)
    return dist

adj = [    [(1, 0), (2, 1)],    [(0, 0), (3, 1)],    [(0, 1), (4, 0)],    [(1, 1), (5, 1)],    [(2, 0), (5, 1)],    [(3, 1), (4, 1)]]
distances = zero_one_bfs(adj, 0)print("0-1 BFS distances from node 0:", distances)

Output

0-1 BFS distances from node 0: [0, 0, 1, 1, 1, 2]
0-1 BFS using appendleft for 0-edges and append for 1-edges

0-1 BFS is much faster than a heap-based Dijkstra when edge weights are restricted to 0 and 1. It runs in O(V+E)O(V + E) time.

Why does this work? Standard BFS processes nodes in layers by distance. In a weighted graph, Dijkstra uses a heap to process nodes in order of distance, and each node's extraction costs O(log⁡E)O(\log E). With only weights 0 and 1, you know that all distance-dd nodes are reachable before distance-d+1d+1 nodes. A weight-0 edge keeps you at the same distance (append to front), and a weight-1 edge moves you to the next distance (append to back). The deque handles this automatically without sorting.

Zero-cost moves and conveyors

Some problems have forced moves (conveyors, teleporters) that consume no time or resources. To handle these, follow all zero-cost moves exhaustively before adding the node to the normal BFS queue. Detect cycles by tracking which nodes are currently being processed.

examples/conveyor_bfs.py
from collections import deque

def conveyor_bfs(adj, conveyors, start):    """BFS handling forced zero-cost moves (conveyors)."""    n = len(adj)    dist = [-1] * n    visited = [False] * n
    def follow_conveyors(u, in_progress):        """Follow zero-cost moves, detect cycles."""        if in_progress[u]:            return u        in_progress[u] = True        if u in conveyors:            u = follow_conveyors(conveyors[u], in_progress)        in_progress[u] = False        return u
    dist[start] = 0    queue = deque([start])
    while queue:        u = queue.popleft()        u = follow_conveyors(u, [False] * n)        if visited[u]:            continue        visited[u] = True
        for v in adj[u]:            if not visited[v]:                dist[v] = dist[u] + 1                queue.append(v)
    return dist

adj = {    0: [1, 2],    1: [3],    2: [4],    3: [5],    4: [5],    5: []}conveyors = {1: 2}
distances = conveyor_bfs(adj, conveyors, 0)print("Distances with conveyors:", distances)

Output

Distances with conveyors: [0, 1, 1, -1, 2, 3]
Handle forced zero-cost moves and detect cycles

The key is to recognize when a zero-cost move creates a cycle and stop following that chain. Otherwise, you risk infinite loops. Marking nodes as "in progress" prevents revisiting the same cycle.

For example, imagine a conveyor from node 1 to node 2, and another from node 2 back to node 1. If you naively follow zero-cost moves, you loop infinitely: 1 → 2 → 1 → 2 → ... Instead, mark 1 as in-progress, follow to 2, then check if 2 is in-progress. Since it is not yet, move to it and mark it in-progress. When you check the edge from 2 back to 1, you see 1 is already in-progress and stop. Unmark both nodes and continue with normal BFS. This prevents infinite loops while still following valid zero-cost moves.

Reverse-graph BFS

Sometimes it is easier to search from the target backward. Build the reverse graph (flip all edges) and run BFS from the target. Distances in the reverse graph are the distances from each node to the target in the original graph.

examples/reverse_bfs.py
from collections import deque

def reverse_bfs(adj, target):    """BFS on reverse graph to compute distances to target."""    n = len(adj)    rev_adj = [[] for _ in range(n)]
    for u in range(n):        for v in adj[u]:            rev_adj[v].append(u)
    dist = [-1] * n    dist[target] = 0    queue = deque([target])
    while queue:        u = queue.popleft()        for v in rev_adj[u]:            if dist[v] == -1:                dist[v] = dist[u] + 1                queue.append(v)
    return dist

adj = [    [1, 2],    [3],    [4],    [5],    [5],    []]
distances = reverse_bfs(adj, 5)print("Distance from each node to target 5:", distances)

Output

Distance from each node to target 5: [3, 2, 2, 1, 1, 0]
Run BFS on the reverse graph to compute distances to the target

This is useful when the target is given but the starting nodes are many or implicit. For example, find the shortest time to reach an exit from every room in a building.

Reverse-graph BFS suits time-based problems too. Given daily tasks and a final deadline state, reversing the graph lets you compute how much time is left once you reach each state, working backward from the deadline. Building the reverse graph takes O(V+E)O(V + E) time (iterate edges and add reversed copies), and then BFS is O(V+E)O(V + E) as usual.

Why these variants matter

Multi-source BFS fits problems that need the closest source to every node. It also fits problems that hand you several starting points by design. 0-1 BFS skips the heap Dijkstra's algorithm needs, so it runs faster when edge weights are restricted to 0 and 1. Zero-cost moves force you to follow chains and handle the resulting structure carefully. Reverse-graph BFS works well when the target is known but the sources are implicit or numerous.

Contest problems often mix these variants. A problem might start with zero-cost moves, follow them completely, then continue over ordinary weight-1 edges toward a target. Searching backward from that target with the reverse graph is often cleaner than tracking every path forward. Senior 3 and 4 problems combine these techniques regularly.

Common mistakes

  • Forgetting to initialize all sources: In multi-source BFS, every source must start at distance 0. If you initialize only one, you get single-source BFS.
  • Mixing up appendleft and append: In 0-1 BFS, weight-0 edges go to the front (appendleft), and weight-1 edges go to the back (append). Swapping them breaks the order.
  • Infinite loops with conveyors: If you follow zero-cost moves without cycle detection, you can loop forever. Always mark nodes as visited or in-progress.
  • Forgetting to reverse all edges: When building the reverse graph, flip every edge. An undirected edge becomes a pair of reversed edges in both directions.
  • Confusing distance in the original vs. reverse graph: Distance from A to B in the original graph equals distance from B to A in the reverse graph. If you query the reverse graph, remember to query with swapped endpoints.

Recap

Multi-source BFS starts from several nodes at distance 0. 0-1 BFS avoids a heap by using deque with appendleft for weight-0 edges. Zero-cost moves require cycle detection to avoid infinite loops. Reverse-graph BFS reaches a target by searching backward. These variants extend BFS to handle richer graph structures.

Practice

Try these on the judges. Each link opens the problem on WMOJ or DMOJ.

  1. 2018 S3
    RoboThieves (opens on DMOJ in a new tab) DMOJ

    Search from multiple starting positions while handling conveyors and cameras.

    Why DMOJ: Combines multi-source BFS with zero-cost-move detection.

  2. 2021 S4
    Daily Commute (opens on WMOJ in a new tab) WMOJ

    Find optimal daily routes using per-day BFS with reverse-graph distances.