0-1 BFS is the trick that finds shortest paths on a graph whose edges cost only 0 or 1 — in linear time, no priority queue needed. It is a beautiful upgrade to plain BFS, and a favorite for graphs where some moves are "free."
Problem. You have a directed graph where every edge weight is either 0 or 1. Given a source
vertex, find the shortest distance (sum of weights) from the source to every other vertex.
Example: edges S→A (1), S→B (0), A→C (0), B→A (1), B→T (1), C→T (0). The shortest path
from S to T is S → B → T with total cost 0 + 1 = 1.
The slow way first
The general tool for weighted shortest paths is Dijkstra, which uses a priority queue (a heap). That works, but it costs O(E log V) because every push and pop touches the heap.
The question to ask: do I really need a full priority queue? With arbitrary weights, yes. But when every weight is only 0 or 1, the set of "live" distances at any moment spans at most two values — some distance d and d + 1. That tiny range means we can replace the heap with something much cheaper.
The idea: a deque instead of a heap
Run a BFS, but use a double-ended queue (deque). When you relax an edge out of vertex u:
- weight 0 → the neighbor has the same distance as
u, so push it to the front. - weight 1 → the neighbor is one farther, so push it to the back.
This keeps the deque sorted by distance at all times, just like a heap would — but with O(1) pushes and pops. The first time a vertex is popped, its distance is final.
The key insight: because the deque never holds more than two distinct distance values, "front" always means the smallest distance — so a deque gives the same ordering guarantee as a heap, for free.
Walk through it
Step through the animation. We start at S with distance 0. Popping S, the edge S→B is weight 0 so B jumps to the front at distance 0; S→A is weight 1 so A goes to the back at distance 1. Watch the deque stay ordered: when we later relax A→C (weight 0), C slips ahead of T even though T was queued earlier. The first pop of each vertex locks in its final distance.
Pseudocode
dist[source] = 0, all others = infinity
deque dq = [source]
while dq is not empty:
u = pop from FRONT
for each edge (u -> v) with weight w:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
if w == 0: push v to the FRONT
else: push v to the BACK
return distThe Python solution
from collections import deque
def zero_one_bfs(graph, source, n):
dist = [float("inf")] * n
dist[source] = 0
dq = deque([source])
while dq:
u = dq.popleft()
d = dist[u]
for v, w in graph[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
if w == 0:
dq.appendleft(v)
else:
dq.append(v)
return distdiststarts as all infinity except the source, which is0.dqis a deque seeded with the source. We always pop from the front withpopleft.- For each neighbor
vwith edge weightw, the candidate distance isnd = d + w. - We only act when
nd < dist[v]— the standard relaxation check. - Lines 14-18 are the heart of it: a 0-weight edge does
appendleft(front), a 1-weight edge doesappend(back). That single choice keeps the deque sorted by distance.
Complexity
| Case | Time | Notes |
|---|---|---|
| Dijkstra (heap) | O(E log V) (moderate) | every edge touches the priority queue |
| 0-1 BFS (this solution) | O(V + E) (moderate) | O(1) deque pushes and pops |
O(V) (moderate)Each vertex can be pushed a few times, but every edge is relaxed a constant number of times, so the total work is linear in the size of the graph. That is strictly faster than Dijkstra — the 0/1 weight restriction buys us the log factor.
When this pattern shows up
Whenever a shortest-path problem has only two edge costs — often "free" moves versus "costly" moves — think 0-1 BFS instead of Dijkstra. Classic disguises: a grid where you can walk freely (cost 0) but pay to break a wall (cost 1), or "minimum sign flips / minimum changes to reach the goal."
The same vertex can be added to the deque more than once at different distances. That is fine — guard
every relaxation with the nd < dist[v] check, and trust that the first pop of a vertex carries its
final, smallest distance.
Practice
After popping S, we relax S→B (weight 0) and S→A (weight 1). What does the deque look like, front to back?
1. Why can we use a deque instead of a priority queue here?
2. When we relax an edge of weight 0, where does the neighbor go?
3. What is the time complexity of 0-1 BFS?
4. How do we know a vertex's distance is final?