0-1 BFS — Mathematical Foundations and Complexity Theory¶
Table of Contents¶
- Formal Definition
- Correctness Proof — The Two-Level Deque Invariant
- Equivalence to Dijkstra with a 2-Bucket Priority Queue
- O(V + E) Complexity Proof
- Generalization to Dial's Algorithm — O(V + E + maxDist)
- Cache and Memory-Hierarchy Analysis
- Average-Case and Frontier-Size Analysis
- Space-Time Trade-offs
- Comparison with Alternatives (asymptotics + constants)
- Open Problems and Research Directions
- Summary
1. Formal Definition¶
Let G = (V, E) be a directed graph with a weight function w : E → {0, 1}. (Undirected graphs are handled by adding both orientations of each edge.) Fix a source s ∈ V. The single-source shortest path (SSSP) problem asks for
with δ(v) = +∞ if no path exists.
Definition 1.1 (0-1 BFS). Maintain an array dist[v], initialised dist[s] = 0 and dist[v] = +∞ otherwise, and a double-ended queue Q initialised to ⟨s⟩. Repeat until Q is empty:
u ← Q.pop_front()
if u is already settled: continue (* stale skip *)
mark u settled
for each (u, v) ∈ E with weight w:
if dist[u] + w < dist[v]:
dist[v] ← dist[u] + w
if w = 0: Q.push_front(v)
else: Q.push_back(v)
Definition 1.2 (Deque operations). push_front, push_back, pop_front each run in O(1) (e.g. on a doubly linked list or a power-of-two ring buffer). This O(1) deque is the only structural assumption distinguishing 0-1 BFS from Dijkstra.
Definition 1.3 (Settled). A vertex u is settled at the first instant it is removed from the front of Q and not skipped. We will prove dist[u] = δ(u) at that instant.
We use the convention that the deque stores vertices and the algorithm reads dist[u] on pop; the "store the distance in the deque and compare" variant is equivalent (§2.6).
2. Correctness Proof — The Two-Level Deque Invariant¶
We prove 0-1 BFS computes δ correctly. The proof rests on a structural invariant about the contents of Q.
2.1 The Key Invariant¶
Lemma 2.1 (Two-level monotonicity). Let d be the distance of the vertex at the front of Q (the value dist[front] at the time it will be popped). At every point during execution, reading Q from front to back, the associated dist values are non-decreasing and lie in {d, d+1}. Formally, there is a (possibly empty) front segment all of value d followed by a (possibly empty) rear segment all of value d+1.
Proof. By induction on the number of operations.
Base. Q = ⟨s⟩ with dist[s] = 0. One element, value d = 0. Holds.
Step. Assume the invariant before an operation. Two cases.
-
A pop. Removing the front element cannot increase the spread of remaining values; the remaining elements are a suffix of a non-decreasing sequence in
{d, d+1}. If the entired-segment is now gone, the new front has valued+1, and relative to the new front valued' = d+1the contents lie in{d+1} ⊆ {d', d'+1}. Holds. -
A push during expansion of a popped vertex
uwithdist[u] = d. Whenuis expanded it is (or was) the front, so by the invariant the live values are in{d, d+1}. - A 0-edge sets
dist[v] = dand doespush_front(v). The front segment has valued, so prepending anotherdkeeps the front segment uniform atd; the rest is unchanged. Values still{d, d+1}. Holds. - A 1-edge sets
dist[v] = d+1and doespush_back(v). The rear segment has valued+1, so appending anotherd+1keeps it uniform. Values still{d, d+1}. Holds.
In all cases the invariant is preserved. ∎
2.2 Front = Minimum¶
Corollary 2.2. At every pop, the front vertex has the minimum dist value among all vertices currently in Q.
Proof. Immediate from Lemma 2.1: the front segment carries the smaller of the two live values. ∎
2.3 Distances Are Settled in Non-Decreasing Order¶
Lemma 2.3. The sequence of dist values at which vertices are settled is non-decreasing over time.
Proof. A vertex is settled when popped from the front. By Corollary 2.2 the front is the current minimum. New pushes only ever create values in {d, d+1} where d is the current front value, so no value smaller than the current front is ever introduced. Hence successive settled values never decrease. ∎
2.4 Optimality at Settling Time¶
Theorem 2.4 (Correctness). When a vertex u is settled, dist[u] = δ(u).
Proof. Suppose, for contradiction, that some vertex is settled with dist[u] > δ(u), and let u be the first such vertex (in settling order). Let P be a shortest s⇝u path, δ(u) = w(P). Let y be the last vertex on P that is settled with the correct distance dist[y] = δ(y) before u is settled (such a y exists: s is settled first with dist[s] = 0 = δ(s)). Let (y, z) be the edge of P leaving y toward u, with weight w(y,z) ∈ {0,1}.
When y was settled (correctly), the algorithm relaxed (y, z):
(the last equality because (y,z) lies on a shortest path, so prefixes of shortest paths are shortest). Since distances never undershoot δ (they correspond to real path costs), dist[z] = δ(z) from that point on, and z was pushed into Q.
If z = u, then dist[u] = δ(u), contradicting the assumption. Otherwise z lies strictly between y and u on P, and by choice of y (the last correctly-settled vertex before u) z is not settled before u. But z is in Q with dist[z] = δ(z) ≤ δ(u). By Lemma 2.3 the algorithm settles vertices in non-decreasing distance order, so z (distance ≤ δ(u) < dist[u]) would be settled before u — contradiction with "z not settled before u".
Hence no such u exists: every vertex is settled with dist[u] = δ(u). ∎
2.5 Termination¶
Each vertex is settled at most once (the settled flag / stale skip). Each settling triggers O(deg(u)) relaxations, each of which pushes at most once. Thus the number of pushes is at most Σ_u deg(u) + 1 = O(V + E), finite; the loop terminates. ∎
2.6 Equivalence of the Two Stale-Handling Styles¶
The "store (d, u) and skip if d > dist[u]" style and the "store u, settle-flag" style accept exactly the same final dist array: both expand each vertex exactly once, at its minimum distance, and both ignore later (larger) re-pushes. The proofs above apply verbatim to either, since neither changes the order of front-pops of the minimum live value. ∎
3. Equivalence to Dijkstra with a 2-Bucket Priority Queue¶
Dijkstra's algorithm requires a priority queue supporting extract-min and decrease-key. Its only correctness requirement is that extract-min returns a vertex of minimum tentative distance (this is exactly the property used in the classical Dijkstra proof, structurally identical to Theorem 2.4).
Proposition 3.1. With w : E → {0,1}, the set of finite tentative distances present in Dijkstra's priority queue at any time spans at most two consecutive integers.
Proof. Dijkstra settles in non-decreasing distance order; let d be the distance of the most recently settled vertex. Any unsettled vertex v in the queue has dist[v] = dist[x] + w(x,v) for some settled x with dist[x] ≤ d, so dist[v] ≤ d + 1. Also dist[v] ≥ d (a smaller value would have been settled already). Hence every live tentative distance is d or d+1. ∎
Theorem 3.2. 0-1 BFS is Dijkstra's algorithm with the priority queue realised as a two-bucket structure — bucket d (the deque front segment) and bucket d+1 (the deque rear segment).
Proof. By Proposition 3.1 a two-bucket queue suffices for Dijkstra under 0/1 weights. The deque realises exactly this: push_front inserts into bucket d (a 0-edge keeps distance d), push_back inserts into bucket d+1 (a 1-edge advances distance to d+1), and pop_front performs extract-min by draining bucket d before bucket d+1. The decrease-key of Dijkstra is realised lazily: an improved distance pushes a fresh copy, and the stale copy is discarded on pop (Theorem 2.4 / §2.6). Hence the two algorithms expand vertices in the identical order and produce identical results. ∎
This is the deep reason 0-1 BFS drops Dijkstra's log V factor: the log V exists only to support an arbitrary-key priority queue; two buckets are an exact O(1) priority queue for the 0/1 case.
4. O(V + E) Complexity Proof¶
Theorem 4.1. 0-1 BFS runs in O(V + E) time and O(V) space.
Proof.
Time. Initialising dist is O(V). Each vertex is settled (expanded) at most once by the settled flag (§2.5). When u is expanded, the algorithm scans its deg(u) outgoing edges, performing O(1) work per edge (a comparison, possibly a dist write and one O(1) deque push). Summed over all settled vertices:
The number of pops is at most the number of pushes; each push corresponds to a successful relaxation, of which there are at most E (one per edge per settling of its tail), plus the initial source push. Stale pops are bounded by total pushes, so the pop work is also O(V + E). Each deque operation is O(1) (Definition 1.2). Total: O(V + E).
Space. dist and settled are O(V). The deque holds at most the number of unsettled pushed vertices. In the settled-flag style each vertex is pushed O(deg(u)) times in the worst case, so the deque can in principle hold O(E) entries; in practice the live (unsettled) count is O(V). Either way the asymptotic space is O(V + E) worst case, O(V) typical. ∎
Remark 4.2 (Tightness). The bound is tight: a graph that is a single path of V vertices with V−1 edges forces Θ(V) settles and Θ(V) edge scans; a complete digraph forces Θ(V + E) = Θ(V²) edge scans. No comparison-free improvement is possible because every edge must be examined to certify a shortest path in the worst case.
5. Generalization to Dial's Algorithm — O(V + E + maxDist)¶
When w : E → {0, 1, …, k} for a small integer k, the two-bucket deque generalises to a bucket array indexed by distance — Dial's algorithm (Dial 1969).
5.1 Definition¶
Maintain buckets B[0 .. maxDist], where maxDist is an upper bound on δ(v) over reachable v (at most (V−1)·k). Process buckets in increasing index; relaxing (u, v, w) from a vertex at distance d = dist[u] inserts v into B[d + w].
B[0].add(s); dist[s] = 0
for d = 0 to maxDist:
while B[d] nonempty:
u = B[d].pop()
if d > dist[u]: continue (* stale *)
for (u, v, w) ∈ E:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
B[dist[v]].add(v)
5.2 Correctness¶
Proposition 5.1. Dial's algorithm settles vertices in non-decreasing distance order, hence (by the same exchange argument as Theorem 2.4) computes δ correctly.
Proof. Buckets are scanned by increasing index d. A vertex is settled when first popped from its bucket B[dist[v]]. Because we never revisit a smaller index after advancing, and a relaxation from distance d with weight w ≥ 0 always targets bucket d + w ≥ d, no vertex is settled at a smaller distance after a larger one. The non-decreasing-order property then drives the standard Dijkstra optimality argument. ∎
5.3 Complexity¶
Theorem 5.2. Dial's algorithm runs in O(V + E + maxDist) time and O(V + maxDist) space.
Proof. Allocating and scanning the bucket array costs O(maxDist) (we touch each index once, even empty ones). Each vertex is settled once: O(V) settles, O(E) edge relaxations, each O(1). Summing: O(V + E + maxDist). Space: dist is O(V), the buckets together hold O(V + E) entries over time but O(V) live, plus O(maxDist) bucket headers. ∎
Corollary 5.3. 0-1 BFS is Dial's algorithm with k = 1, where at most two consecutive buckets are ever simultaneously nonempty (Lemma 2.1), so the bucket array degenerates to a sliding window of width 2 — implementable as a single deque, eliminating the O(maxDist) term and giving O(V + E).
Remark 5.4 (When Dial's loses). If maxDist is large (large k or long paths), the O(maxDist) term and the empty-bucket scans dominate. With weights bounded by C, a radix heap (Ahuja–Mehlhorn–Orlin–Tarjan 1990) achieves O(E + V log C); for arbitrary non-negative weights, a binary-heap Dijkstra at O(E log V) is the fallback.
6. Cache and Memory-Hierarchy Analysis¶
We use the external-memory model: block size B, cache size M, counting I/Os.
6.1 Why 0-1 BFS is cache-friendly¶
The hot data structures are flat arrays: dist[V], settled[V], and a ring-buffer deque. Compared to Dijkstra's binary heap — whose sift_down jumps between parent index i and child 2i+1, crossing a cache block once 2i+1 > B — the deque touches contiguous memory at both ends.
Proposition 6.1. The deque incurs O(1/B) amortised cache misses per push/pop on a ring buffer (sequential access at head and tail), versus Θ(log(n/B)) misses per heap operation in Dijkstra.
Proof sketch. A ring buffer's head and tail advance by one slot per operation; consecutive operations touch the same cache block until it is exhausted, amortising to 1/B misses per op. A binary heap operation traverses a root-to-leaf path of length Θ(log n), and each level beyond depth log B lands in a distinct block, giving Θ(log(n/B)) misses. ∎
6.2 Adjacency layout¶
Storing the graph in CSR with the weight packed into one bit of each target id (§ senior.md) means a vertex's entire neighbour list is one contiguous run of deg(u) words — ⌈deg(u)/B⌉ block reads, optimal for a scan. This is why 0-1 BFS at scale is memory-bandwidth-bound on the CSR targets array rather than compute-bound.
7. Average-Case and Frontier-Size Analysis¶
7.1 Frontier size¶
The "frontier" is the set of live (pushed, unsettled) vertices. In a graph with bounded degree Δ, each settling pushes at most Δ neighbours, so the deque grows by at most Δ − 1 per pop. On grids (Δ = 4) the live frontier typically scales with the perimeter of the explored region, Θ(√V) for a 2D ball, not Θ(V). This keeps the deque small in practice and the ring buffer cache-resident.
Concretely, on a 1000 × 1000 grid (V = 10^6), a roughly circular settled region of radius r has perimeter ≈ 2πr. At the half-explored point (r ≈ 564, half the cells settled) the live frontier is on the order of 3.5·10^3 entries — about 0.35% of V. So a ring buffer sized to a few thousand int32 (tens of KB) is L1/L2-resident throughout, whereas a binary heap for the same problem would hold up to Θ(V) entries and spill to L3/RAM. This perimeter-vs-area gap is a second, often-overlooked reason 0-1 BFS outruns heap-Dijkstra in practice, independent of the asymptotic log V.
7.2 Expected stale pops¶
With the distance-check style, a vertex is re-pushed each time a strictly shorter path is found before it settles. On random 0/1-weighted graphs the expected number of improving relaxations per vertex is O(1) (few predecessors improve it after the first reaching path), so the expected total work stays Θ(V + E) with a small constant. The settled-flag style bounds re-expansion to zero regardless.
7.3 Number of distinct distance levels¶
The diameter in the 0/1 metric is at most the number of 1-edges on the longest shortest path, ≤ V − 1. The deque only ever holds two adjacent levels (Lemma 2.1), so the number of levels affects total iterations but never the deque's instantaneous size — this is precisely why 0-1 BFS avoids Dial's O(maxDist) term.
7B. A Fully Worked Numeric Trace¶
To make Lemma 2.1 and Theorem 2.4 concrete, we trace the algorithm on a small instance and watch the two-level invariant hold at every step.
Directed graph (vertices A=0 … E=4), edge weights in braces:
State columns: the popped vertex, the action taken, the deque rendered as front … back with each entry vertex(dist), and the live distance values present.
init deque = [A(0)] levels = {0}
pop A(0) settle A=0
A->B w0 dist[B]=0 pf B deque = [B(0)] levels = {0}
A->C w1 dist[C]=1 pb C deque = [B(0), C(1)] levels = {0,1}
pop B(0) settle B=0
B->D w1 dist[D]=1 pb D deque = [C(1), D(1)] levels = {1}
B->E w0 dist[E]=0 pf E deque = [E(0), C(1), D(1)] levels = {0,1}
pop E(0) settle E=0
E->D w0 dist[D]=0 pf D deque = [D(0), C(1), D(1)] levels = {0,1}*
pop D(0) settle D=0 deque = [C(1), D(1)] levels = {1}
pop C(1) settle C=1
C->E w1 1+1=2 ≥ dist[E]=0 no push
deque = [D(1)] levels = {1}
pop D(1) STALE (1 > dist[D]=0) skip; metrics.stale++
deque = [] done
() At the starred step there are two D entries: the fresh D(0) at the front and a stale D(1) still lurking near the back. The invariant "values are {d, d+1}" counts live* (not-yet-settled, distance-current) entries; the stale D(1) is discarded harmlessly when popped. Final δ = (A:0, B:0, C:1, D:0, E:0).
Two observations the trace makes vivid:
- The level set is always a subset of
{d, d+1}for the current front valued— the algebraic content of Lemma 2.1. - A vertex can be enqueued twice (here
D), once via a 1-edge then improved via a 0-edge; the later, smaller value is what survives, and the earlier copy becomes a stale skip — the mechanism that makes the lazydecrease-keyof Theorem 3.2 correct.
7C. Reference Implementations¶
Three idiomatic, self-contained implementations producing the trace above. Each uses the settled-flag stale-handling style and a deque with O(1) ends.
Go¶
package main
import "fmt"
type Edge struct{ to, w int } // w in {0,1}
// ZeroOneBFS: settled-flag style; deque via two slices for O(1) both ends.
func ZeroOneBFS(adj [][]Edge, n, src int) []int {
const INF = 1 << 30
dist := make([]int, n)
settled := make([]bool, n)
for i := range dist {
dist[i] = INF
}
dist[src] = 0
front := []int{src} // logical front: top of this stack
var back []int // logical back: in order
pop := func() int {
if len(front) > 0 {
x := front[len(front)-1]
front = front[:len(front)-1]
return x
}
for i := len(back) - 1; i >= 0; i-- {
front = append(front, back[i])
}
back = back[:0]
x := front[len(front)-1]
front = front[:len(front)-1]
return x
}
empty := func() bool { return len(front) == 0 && len(back) == 0 }
for !empty() {
u := pop()
if settled[u] {
continue // stale
}
settled[u] = true
for _, e := range adj[u] {
nd := dist[u] + e.w
if nd < dist[e.to] {
dist[e.to] = nd
if e.w == 0 {
front = append(front, e.to)
} else {
back = append(back, e.to)
}
}
}
}
return dist
}
func main() {
adj := make([][]Edge, 5)
adj[0] = []Edge{{1, 0}, {2, 1}}
adj[1] = []Edge{{3, 1}, {4, 0}}
adj[2] = []Edge{{4, 1}}
adj[4] = []Edge{{3, 0}}
fmt.Println(ZeroOneBFS(adj, 5, 0)) // [0 0 1 0 0]
}
Java¶
import java.util.*;
public final class ZeroOneBFS {
record Edge(int to, int w) {}
static int[] run(List<Edge>[] adj, int n, int src) {
final int INF = Integer.MAX_VALUE / 2;
int[] dist = new int[n];
boolean[] settled = new boolean[n];
Arrays.fill(dist, INF);
dist[src] = 0;
Deque<Integer> dq = new ArrayDeque<>();
dq.addFirst(src);
while (!dq.isEmpty()) {
int u = dq.pollFirst();
if (settled[u]) continue; // stale
settled[u] = true;
for (Edge e : adj[u]) {
int nd = dist[u] + e.w();
if (nd < dist[e.to()]) {
dist[e.to()] = nd;
if (e.w() == 0) dq.addFirst(e.to());
else dq.addLast(e.to());
}
}
}
return dist;
}
public static void main(String[] args) {
int n = 5;
List<Edge>[] adj = new List[n];
for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();
adj[0].add(new Edge(1, 0)); adj[0].add(new Edge(2, 1));
adj[1].add(new Edge(3, 1)); adj[1].add(new Edge(4, 0));
adj[2].add(new Edge(4, 1));
adj[4].add(new Edge(3, 0));
System.out.println(Arrays.toString(run(adj, n, 0))); // [0, 0, 1, 0, 0]
}
}
Python¶
from collections import deque
def zero_one_bfs(adj, n, src):
INF = float("inf")
dist = [INF] * n
settled = [False] * n
dist[src] = 0
dq = deque([src])
while dq:
u = dq.popleft()
if settled[u]: # stale
continue
settled[u] = True
for v, w in adj[u]:
nd = dist[u] + w
if nd < dist[v]:
dist[v] = nd
dq.appendleft(v) if w == 0 else dq.append(v)
return dist
if __name__ == "__main__":
adj = {
0: [(1, 0), (2, 1)],
1: [(3, 1), (4, 0)],
2: [(4, 1)],
3: [],
4: [(3, 0)],
}
print(zero_one_bfs(adj, 5, 0)) # [0, 0, 1, 0, 0]
All three settle each vertex once, skip stale re-pops, and reproduce δ = (0, 0, 1, 0, 0) from the trace in §7B.
8. Space-Time Trade-offs¶
| Variant | Time | Space | Note |
|---|---|---|---|
| Deque, settled-flag | O(V+E) | O(V) live + O(E) worst deque | One expansion per vertex; minimal re-work. |
| Deque, distance-check | O(V+E) | O(E) worst deque | No flag array; relies on lazy skip. |
| Dial's bucket array | O(V+E+maxDist) | O(V+maxDist) | Generalises to 0..k. |
| Dijkstra binary heap | O(E log V) | O(V) | Arbitrary non-negative weights. |
| Radix heap | O(E + V log C) | O(V + log C) | Integer weights bounded by C. |
The deque variants are Pareto-optimal for {0,1} weights: nothing achieves o(V + E) (every edge must be inspected), and the constants are the smallest among the options because no heap or bucket-scan overhead is paid.
Path reconstruction adds an O(V) parent[] array and O(path length) reconstruction time, no change to asymptotics.
9. Comparison with Alternatives (asymptotics + constants)¶
9.1 Asymptotics¶
| Algorithm | Weight model | Time | Space |
|---|---|---|---|
| Plain BFS | unweighted | O(V+E) | O(V) |
| 0-1 BFS | {0,1} | O(V+E) | O(V) |
| Dial's | {0..k} int | O(V+E+maxDist) | O(V+maxDist) |
| Dijkstra (binary heap) | non-negative | O(E log V) | O(V) |
| Dijkstra (Fibonacci heap) | non-negative | O(E + V log V) | O(V) |
| Radix heap | non-negative ints ≤ C | O(E + V log C) | O(V + log C) |
| Bellman–Ford | arbitrary (neg ok) | O(V·E) | O(V) |
9.2 Constant Factors¶
For a 4-regular grid of N cells, full SSSP, indicative ns-per-edge on commodity hardware:
plain BFS : ~3 ns / edge
0-1 BFS (ring deque) : ~4 ns / edge
Dial's (small k) : ~5 ns / edge + maxDist bucket sweep
Dijkstra (binary heap) : ~12 ns / edge (heap sift + cache misses)
Dijkstra (4-ary heap) : ~9 ns / edge
0-1 BFS sits essentially at BFS speed: the only overhead over plain BFS is the per-edge weight read and the front/back branch, both predictable. The 2–3× gap to Dijkstra is the eliminated log V heap work plus the deque's superior cache profile (§6).
9.3 The boundary cases¶
- All weights 0: every vertex settles at distance 0; the deque behaves as a stack/queue hybrid but the result (all reachable = 0) is trivially correct.
- All weights 1: every push is
push_back; the deque is a FIFO queue and 0-1 BFS is identical to plain BFS. This confirms 0-1 BFS strictly generalises BFS.
10. Open Problems and Research Directions¶
-
Parallel work-optimal 0-1 BFS. Δ-stepping gives work-efficient parallel SSSP for general weights; whether a strictly work-optimal, low-depth parallel algorithm specialised to
{0,1}weights with provably better span than general Δ-stepping is fully characterised remains an engineering-research gap. -
Dynamic 0-1 SSSP. Maintaining
δunder edge flips (0↔1) faster than recompute. Decremental and fully-dynamic SSSP have known bounds for general weights (e.g. Bernstein–Chechik); tight bounds for the restricted 0/1 case, exploiting the two-level structure, are less explored. -
Cache-oblivious 0-1 BFS. BFS has cache-oblivious variants (Munagala–Ranade, buffered repository trees). Adapting these to the deque/two-bucket structure of 0-1 BFS with optimal
O((V+E)/B)-style I/O bounds is a refinement question. -
Streaming / semi-streaming. In the semi-streaming model (
O(V polylog V)memory, few passes), approximate and exact shortest paths are studied; the 0/1 special case may admit fewer passes or smaller memory. -
Negative-edge analogues. 0/1 weights are non-negative; a clean deque-based analogue for
{-1, 0, +1}weights (which can encode certain DP/difference-constraint systems) without falling back to full Bellman–Ford is of interest where no negative cycle exists.
10B. Relationship to Difference Constraints and 0-1 Structures¶
0-1 BFS is the shortest-path engine behind several theoretical reductions, which is worth seeing because it explains why the 0/1 restriction shows up so often.
10B.1 Boolean reachability with cost¶
Consider a system of constraints x_v − x_u ≤ c(u,v) with c ∈ {0,1} (a restricted difference-constraint system). Its feasibility/optimal-potential problem maps to single-source shortest paths in the constraint graph; when all constraint constants are 0 or 1 and non-negative, 0-1 BFS solves it in O(V + E) instead of the general O(V·E) Bellman–Ford used for arbitrary difference constraints. This is the formal backbone of "minimum number of unit adjustments" problems.
10B.2 Layered / product graphs¶
State-augmented 0-1 BFS (vertex = (position, state)) is shortest paths on a product graph G × S. The two-level invariant (Lemma 2.1) is preserved under the product because edge weights remain in {0,1}; only |V| scales to |V|·|S|. Thus the complexity becomes O((V + E)·|S|) — exact, with no heap, as long as the state-transition weights stay binary. This is the rigorous justification for the "just make the vertex a tuple" modelling advice.
10B.3 Why the boundary is exactly 1¶
The deque works because the reachable-distance window has width 1 (values {d, d+1}). With a weight k ≥ 2, the window widens to {d, …, d+k} and a single front/back distinction can no longer separate the current minimum from the rest — you need k+1 buckets (Dial's). So the "0/1" restriction is not arbitrary: it is precisely the largest weight set for which a two-ended structure is an exact priority queue.
11. Summary¶
- Model. 0-1 BFS solves SSSP for
w : E → {0,1}by maintaining a deque, pushing 0-edges to the front and 1-edges to the back, and settling each vertex on its first front-pop. - Invariant. The deque always holds at most two consecutive distance values
{d, d+1}, with the smaller in front (Lemma 2.1). Hencepop_frontis an exactextract-min. - Correctness. Vertices settle in non-decreasing distance order, and the standard shortest-path exchange argument shows each is settled with
dist[u] = δ(u)(Theorem 2.4). - Equivalence. 0-1 BFS is exactly Dijkstra with a 2-bucket priority queue; the
log Vfactor vanishes because two buckets are anO(1)priority queue for 0/1 weights (Theorem 3.2). - Complexity.
O(V + E)time,O(V)space — tight, since every edge must be inspected (Theorem 4.1). - Generalization. For
{0..k}weights, the deque becomes Dial's bucket array atO(V + E + maxDist)(Theorem 5.2); 0-1 BFS is thek = 1case where the bucket array collapses to a width-2 window (Corollary 5.3). Beyond smallk, use radix-heap or binary-heap Dijkstra. - Constants. Near-BFS speed; 2–3× faster than binary-heap Dijkstra from dropped heap work and superior cache locality (§§6, 9).
Foundational references: Dial (1969) for the bucket queue; cp-algorithms for the canonical deque exposition; Ahuja–Mehlhorn–Orlin–Tarjan (1990) for radix heaps; Meyer–Sanders for Δ-stepping; CLRS Ch. 24 for the Dijkstra optimality argument reused in §2.