DSA DSA In Depth
0%
lesson bfs01-bfsshortest-paths

Breadth-First Search: Multi-Source BFS, 0-1 BFS (Deque) & Bidirectional Search

7 min read Module 5.1: Graph Representations, Traversals, Connectivity & Topological Order Updated Oct 23, 2026
📄 Cheat Sheet
Breadth First Search (BFS) Algorithm Visualized
William Fiset Breadth First Search (BFS) Algorithm Visualized Watch on YouTube ↗

Demonstrates wave-front level expansion and shortest-path tree construction on unweighted graphs.

0:00 / --:--

The Two-Valued Queue Monotonicity Invariant

Why does a simple FIFO queue suffice to compute exact shortest paths in unweighted graphs, and why does a double-ended queue (deque) suffice when edge weights are $w(u, v) \in \{0, 1\}$? The answer lies in the Two-Valued Queue Invariant.

Formal Invariant — Queue Monotonicity & Two-Level Bound

At any point during standard BFS or 0-1 BFS execution, if the deque contains vertices $\langle v_1, v_2, \dots, v_k \rangle$ from front to back, their tentative distances satisfy:

$$d[v_1] \le d[v_2] \le \dots \le d[v_k] \quad \text{and} \quad d[v_k] - d[v_1] \le 1$$

In 0-1 BFS, popping $u = v_1$ with minimum distance $d[u]$ and relaxing an edge $(u, x)$ produces either $d[x] = d[u]$ (via a weight-0 edge, pushed to the front before $v_2$) or $d[x] = d[u] + 1$ (via a weight-1 edge, pushed to the back after $v_k$, since $d[v_k] \le d[u] + 1$). Thus monotonicity is preserved inductively in $O(1)$ per operation.

1. Multi-Source BFS via Virtual Super-Source

Given a set of source vertices $S = \{s_1, s_2, \dots, s_k\}$ (such as all exit cells in a grid or all infected nodes in a network), finding $\min_{s \in S} \text{dist}(s, v)$ for every vertex $v \in V$ does not require $k$ separate BFS runs. Conceptually, introduce a virtual super-source $s^*$ with directed zero-weight edges $(s^*, s_i)$ for all $s_i \in S$. In code, simply initialize $d[s_i] = 0$ and push all $s_i \in S$ into the queue at step 0 before starting the loop. Every vertex $v$ is visited at most once in $O(V + E)$ total time.

2. 0-1 BFS with a Double-Ended Queue (Dial's $C=1$ Specialization)

Many grid and state-space problems have free transitions ($w = 0$, e.g., moving along a conveyor belt's direction or staying in the same corridor) and unit-cost modifications ($w = 1$, e.g., rotating a tile or breaking a wall). Standard BFS fails because a path with more edges can have lower total weight. Dijkstra's algorithm runs in $O((V + E) \log V)$, whereas 0-1 BFS achieves $O(V + E)$:

  • When relaxing $(u, v)$ with weight $w \in \{0, 1\}$, check if $d[u] + w < d[v]$.
  • If improved, update $d[v] = d[u] + w$. If $w == 0$, call dq.push_front(v); if $w == 1$, call dq.push_back(v).
  • Because a vertex $v$ might first be discovered via a weight-1 edge ($d[u] + 1$) and later relaxed via a weight-0 edge ($d[u'] + 0 = d[u]$), $v$ can be pushed into the deque at most twice. Popping a stale duplicate is skipped in $O(1)$ or guarded by a visited bitset upon pop.
0-1 BFS Deque State: At Most Two Distinct Distance Values (d and d+1) v₁ (pop) dist = d v₂ dist = d v₃ dist = d+1 v₄ dist = d+1 w = 0 edge push_front(d) w = 1 edge push_back(d+1) Invariant preserved: all dist=d items precede all dist=d+1 items
flowchart TD Pop["Pop u = dq.front()"] --> Check{"u already settled?"} Check -- "Yes" --> Pop Check -- "No" --> Mark["Mark settled[u] = true"] Mark --> Relax["For each edge (u, v, w) where d[u] + w < d[v]"] Relax --> Weight{"w == 0?"} Weight -- "Yes (w = 0)" --> Front["d[v] = d[u]; dq.push_front(v)"] Weight -- "No (w = 1)" --> Back["d[v] = d[u] + 1; dq.push_back(v)"]
0-1 BFS Deque, DFS Edge Classification & Tarjan tin/low Bridges & SCCs
O(V + E) Single-Pass Traversal

Step through 0-1 BFS deque relaxation (push_front for w=0, push_back for w=1), Tarjan’s undirected bridge/articulation criterion (low[v] > tin[u] / low[v] ≥ tin[u]), and Tarjan’s single-pass SCC condensation.

Step 1 / 9
Custom Input:
Timeline

Invariant & State

DFS reaches sink v5 (tin=6, low=6). Since low[v5] == tin[v5], pop v5 as SCC #1 = {v5} in reverse topological order.
back cross-SCC back v0 1/1 v1 2/1 v2 3/1 v3 4/4 v4 5/4 v5 6/6 ROOT
Active Tarjan Stack
[v0, v1, v2, v3, v4]
Extracted SCCs
SCC #1: {v5}
Root Invariant
low[u] == tin[u]
[Step 1] DFS reaches sink v5 (tin=6, low=6). Since low[v5] == tin[v5], pop v5 as SCC #1 = {v5} in reverse topological order.

3. Bidirectional BFS & Frontier Balancing

When searching for a shortest path between a single pair $(s, t)$ in a graph with branching factor $b$ and shortest-path distance $D$, standard BFS explores $\Theta(b^D)$ states. Bidirectional BFS maintains two frontiers $Q_f$ (expanding forward from $s$) and $Q_b$ (expanding backward along reversed edges from $t$). At each step, expand the frontier with fewer vertices ($|Q_f| \le |Q_b|$) by one full level. When the frontiers intersect at some vertex $m$, the shortest path has length $d_f[m] + d_b[m]$, and only $\Theta(2 \cdot b^{D/2})$ vertices are visited—turning $10^8$ states into $2 \times 10^4$.

Hardware Reality — std::deque Chunked Allocation vs. Ring Buffer

In C++ libstdc++ and libc++, std::deque is implemented as a dynamic array of pointers to fixed-size 512-byte blocks. While asymptotic $O(1)$, it introduces double indirection on index operations. For performance-critical 0-1 BFS where $|V| \le 10^6$, pre-allocating a flat array of size $2|V| + 5$ with initial head = V, tail = V provides a contiguous cache-resident deque with branchless pointer arithmetic.

Complexity Matrix

Variant Edge Weights Time Complexity Space Complexity Data Structure
Single / Multi-Source BFS Unweighted ($w = 1$) O(V + E) O(V) FIFO Queue
0-1 BFS Binary ($w \in \{0, 1\}$) O(V + E) O(V) Double-Ended Queue (Deque)
Dial's Bucket BFS ($0..C$) Integer ($w \in [0, C]$) O(V·C + E) O(V + C) Circular Array of $C+1$ Lists
Bidirectional BFS Unweighted ($w = 1$) O(b^(D/2)) O(b^(D/2)) Two FIFO Queues + Visited Maps

Production Implementation: 0-1 BFS & Multi-Source BFS

#include <vector>
#include <deque>
#include <limits>

struct Edge01 {
    int to;
    int weight; // Must be 0 or 1
};

std::vector<int> zeroOneBFS(int n, const std::vector<std::vector<Edge01>>& adj,
                            const std::vector<int>& sources) {
    constexpr int INF = std::numeric_limits<int>::max() / 2;
    std::vector<int> dist(n, INF);
    std::vector<uint8_t> settled(n, 0);
    std::deque<int> dq;

    for (int s : sources) {
        dist[s] = 0;
        dq.push_front(s);
    }

    while (!dq.empty()) {
        int u = dq.front();
        dq.pop_front();
        if (settled[u]) continue;
        settled[u] = 1;

        for (const auto& e : adj[u]) {
            if (dist[u] + e.weight < dist[e.to]) {
                dist[e.to] = dist[u] + e.weight;
                if (e.weight == 0) {
                    dq.push_front(e.to);
                } else {
                    dq.push_back(e.to);
                }
            }
        }
    }
    return dist;
}
Failure Mode — Early Stopping Trap in Bidirectional BFS and 0-1 BFS

In standard BFS, you can mark a vertex visited at the exact moment it is pushed into the queue. In 0-1 BFS, marking a vertex final upon push is a bug: a vertex pushed to the back via a weight-1 edge at distance $d+1$ may subsequently be reached via a weight-0 edge at distance $d$. Always relax distances upon push and mark settled upon pop. Similarly, in Bidirectional BFS, the first vertex $v$ seen by both sides is not guaranteed to be the global minimum unless you expand a full level and take $\min_v (d_f[v] + d_b[v])$ across the entire layer.

Check yourself Question 1 of 4
Concept Check Deque Insertion Rule in 0-1 BFS recall
Section: 0-1 BFS Mechanics

In 0-1 BFS, when relaxing an edge $(u, v)$ of weight $w \in \{0, 1\}$ that strictly improves `dist[v]`, where must vertex $v$ be inserted into the deque `dq`?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why does marking `visited[v] = true` at the moment $v$ is *pushed* into the deque (as is standard in unweighted BFS) produce wrong shortest paths in 0-1 BFS?

Key Takeaways

Standard BFS and 0-1 BFS rely on the Two-Valued Queue Invariant: all elements in the deque have distances within {d, d+1} in non-decreasing order.
Multi-Source BFS computes the distance from every vertex to its nearest source in O(V + E) time by seeding the queue with all sources at distance 0.
In 0-1 BFS, weight-0 edges push to the front of the deque and weight-1 edges push to the back; vertices must be marked settled upon pop, not push.
Bidirectional BFS reduces state-space exploration from O(b^D) to O(b^(D/2)) by expanding the smaller frontier one complete level at a time.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue