0-1 BFS
Find cheapest paths when every edge costs either 0 or 1, using a double-ended queue instead of a heap.
O(n + e)Space O(n + e)3 practice problemsStep-through pictureThe idea
Plain BFS assumes every edge costs 1. Dijkstra's algorithm handles any non-negative cost but needs a priority queue. There is a common middle case: every edge costs either 0 (free) or 1. Examples are "walking is free, breaking a wall costs 1" or "following a link the right way is free, going against it costs 1".
0-1 BFS handles this with a deque, a list you can add to at both ends. When you find a cheaper way to a node using a free edge, you put that node at the front, because it is just as close as the current node. When you use a cost-1 edge, you put it at the back, like normal BFS.
This keeps the deque sorted by distance at all times, so nodes come out cheapest first, just as in Dijkstra's algorithm, but without a heap.
An everyday example
Moving walkways in an airport
In an airport, some corridors have moving walkways that carry you for free, and others you must walk, which costs you one unit of effort. When a free walkway opens up from where you stand, you take it right away, because it doesn't cost anything. Corridors that cost effort go on your to-do list for later.
| In the example | In the code |
|---|---|
| A free moving walkway | An edge with weight === 0 |
| A corridor you must walk | An edge with weight === 1 |
| Taking the free walkway right away | deque.unshift(next) |
| Adding a walk to the end of the to-do list | deque.push(next) |
| The least effort to reach a gate | dist[next] |
From the slow way to 0-1 bfs
- When edges cost 0 or 1, plain BFS gives wrong answers, because it counts a free edge as a full step. The safe choice is Dijkstra's algorithm with a heap, which costs
O(e log n). That works, but it is more machinery than this problem needs. - Watch what Dijkstra's heap holds when every cost is 0 or 1. The node taken out has the smallest distance,
d. Its edges only create distancesdord + 1. So the heap only ever holds two different distance values at once:dandd + 1. A full heap is only being used to sort two values. - A deque, a list you can add to at both ends, can keep that two-value order by itself. A free edge gives a node the same distance
das the current node, so it goes to the front with the otherdnodes. A cost-1 edge givesd + 1, so it goes to the back. - One more change from plain BFS: a node's first distance is not always its final one, because a free path to it may turn up later. So instead of a visited mark, compare with
distand update whenever a cheaper cost appears, even if that puts a second copy of the node in the deque. That loop is 0-1 BFS.
How it works
- Build
graphwith[to, weight]pairs, where each weight is 0 or 1. - Set every
disttoInfinity, thendist[start] = 0, and putstartin thedeque. - Take
nodefrom the front of the deque. - For each edge
[next, weight], skip it ifdist[node] + weightis not smaller thandist[next]. - Otherwise update
dist[next]. Ifweightis 0, addnextto the front; if it is 1, add it to the back. - When the deque is empty,
distholds the cheapest cost to every node.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function zeroOneBfs(n, edges, start) {const graph = Array.from({ length: n }, () => []);for (const [from, to, weight] of edges) graph[from].push([to, weight]);const dist = new Array(n).fill(Infinity);dist[start] = 0;const deque = [start];while (deque.length > 0) {const node = deque.shift();for (const [next, weight] of graph[node]) {if (dist[node] + weight >= dist[next]) continue;dist[next] = dist[node] + weight;if (weight === 0) deque.unshift(next);else deque.push(next);}}return dist;}
How fast it is
| Time | O(n + e) |
|---|---|
| Extra memory | O(n + e) |
With a real deque each push and pop takes constant time, and with costs of only 0 and 1 each node is improved only a few times. The adjacency list holds every edge.
- Best case
O(n + e)Building
graphreads every edge, even whenstarthas no outgoing edges and the loop runs only once. - Average case
O(n + e)Each node enters the deque at most twice, and each entry reads that node's edges once.
- Worst case
O(n + e)Every node is improved once, so it enters twice and its edges are read twice. That is with a real deque; the template's
shiftandunshiftmove the whole array and can be much slower on big inputs.
Common mistakes
- Using plain BFS and counting every edge as a step, which ignores the free edges.
- Adding free-edge nodes to the back. The deque is no longer sorted and some distances come out too big.
- Marking nodes as done the first time they are seen, as in plain BFS. A later free edge can still make them cheaper, so compare with
distinstead. - Using it with costs like 2 or 5. It only works for 0 and 1; use Dijkstra's algorithm otherwise.
When to use it
Look for these clues in a problem:
- Every move costs either 0 or 1, never anything else.
- The question asks for the fewest walls to break, cells to change, or edges to reverse.
- Some moves are free and others count, as in "following an arrow is free, changing it costs 1".
- Plain BFS gives wrong answers because not every edge counts as a step.
Check yourself
Answer each one in your head first, then open it.
Why can't 0-1 BFS mark a node as done the first time it gets a cost, like plain BFS does?
A cheaper free route can show up later. In the trace, 4 first got cost 2 and was later improved to 1 through 2 → 3 → 4.
At most how many times can one node enter the deque?
Twice. Its first cost is d or d + 1, and later costs can't go below d, so it can improve at most once.
In the trace, what did the old copy of 4 change?
Nothing. It re-checked 4 → 5, found 1 + 1 was not less than dist[5] = 2, and was done.
Why does the deque never hold more than two different labels?
Entries are only added with the current smallest label d or with d + 1, and the smallest label never goes down. So everything in it is d or d + 1.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 3 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
Give each deque entry a label: the distance its node had when the entry was added. The rule is that labels read from front to back never go down, and they hold at most two values, d and d + 1. At the start the deque holds only the start, labeled 0.
Taking the front entry removes a smallest label, d. A free edge adds a label d at the front, which is still the smallest. A cost-1 edge adds d + 1 at the back, which is at most the largest. So the rule holds, and entries come out in order of label, just as Dijkstra's heap hands out its smallest item. Dijkstra's argument then applies: when an entry comes out with a label equal to dist[node], that cost is final, because every later entry has a label at least as large and edges never cost less than 0.
It ends because costs only go down. When a node first gets a cost, it is d or d + 1, and later entries all have labels of at least d, so a node can improve at most once, from d + 1 to d. Each node therefore enters the deque at most twice. An old copy, like the second 4 in the trace, comes out after the newer copy already tried every edge with the lower cost, so it never pushes anything.
Going deeper
3 more ideas
Why the deque stays sorted
At any moment the deque holds nodes with at most two distances: some with distance d and some with d + 1, with the d ones in front. A free edge from a d node gives another d node, which goes to the front. A cost-1 edge gives a d + 1 node, which goes to the back. Neither breaks the order, so the front is always a cheapest node, the same promise a heap gives Dijkstra.
Cost compared with Dijkstra
Each improvement adds a node to the deque in O(1), and with costs of only 0 and 1 a node can only be improved a few times. The whole run is O(n + e) with a real deque, compared with O((n + e) log n) for Dijkstra with a heap.
About shift and unshift
JavaScript arrays have shift and unshift, but they move every element, so they are slow on big inputs. For large graphs, build a deque from a fixed-size array with head and tail indexes that wrap around, or keep two arrays: a "current distance" list and a "next distance" list.
Work it by hand
Every step in a table, with why
Cheapest costs from node 0 in a directed graph where the route through 2 and 3 uses free edges.
n = 6, edges = [[0, 1, 1], [0, 2, 1], [1, 4, 1], [2, 3, 0], [3, 4, 0], [4, 5, 1], [3, 5, 1]], start = 0
| Step | node | Edges tried | dist | deque after | What happens | Why |
|---|---|---|---|---|---|---|
| 1 | - | - | [0, ∞, ∞, ∞, ∞, ∞] | [0] | Start | Only the start has a cost. ∞ stands for Infinity, no path found yet. |
| 2 | 0 | 0→1 (1), 0→2 (1) | [0, 1, 1, ∞, ∞, ∞] | [1, 2] | Both go to the back | Both edges cost 1, so 1 and 2 get cost 1 and join the back. |
| 3 | 1 | 1→4 (1) | [0, 1, 1, ∞, 2, ∞] | [2, 4] | dist[4] = 2 | The first route to 4 costs 2. It isn't final: a cheaper route will show up. |
| 4 | 2 | 2→3 (0) | [0, 1, 1, 1, 2, ∞] | [3, 4] | Free edge: front | 3 costs the same as 2, so it goes to the front, ahead of 4. |
| 5 | 3 | 3→4 (0), 3→5 (1) | [0, 1, 1, 1, 1, 2] | [4, 4, 5] | dist[4] drops to 1 | 1 + 0 = 1 beats 2, so dist[4] improves and a second copy of 4 goes to the front. 5 gets cost 2 at the back. |
| 6 | 4 | 4→5 (1) | same | [4, 5] | No change | 1 + 1 = 2 is not less than dist[5] = 2, so the edge is skipped. |
| 7 | 4 | 4→5 (1) | same | [5] | The old copy | This is the copy pushed when dist[4] was 2. It reads the same edge again and changes nothing. |
| 8 | 5 | none | [0, 1, 1, 1, 1, 2] | [] | Stop | 5 has no outgoing edges, and the deque is empty. |
It returns [0, 1, 1, 1, 1, 2]. Plain BFS would count edges instead and say 4 is 2 steps and 5 is 3 steps away, but the free edges make them cost 1 and 2.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| 0-1 BFS | O(n + e) | O(n + e) | Every edge costs exactly 0 or 1. |
| Breadth-first search | O(n + e) | O(n) | Every edge costs the same, so only the number of edges matters. |
| Dijkstra's algorithm | O(e log n) | O(n + e) | Costs are any non-negative numbers, such as travel times. |
| Bellman-Ford | O(n · e) | O(n) | Some costs are negative, or you must detect a negative loop. |
Extra memory: O(n + e): the adjacency list holds every edge, dist holds n numbers, and the deque holds at most 2n entries.
How the steps grow
- 0-1 BFS, Breadth-first search
O(n + e) - Dijkstra's algorithm
O(e log n) - Bellman-Ford
O(n · e)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| 0-1 BFS | 20 | 2,000 | 2 million |
| Breadth-first search | 20 | 2,000 | 2 million |
| Dijkstra's algorithm | 33 | 9,966 | 20 million |
| Bellman-Ford | 100 | 1 million | 1 trillion |
Inputs that need care
5 tricky inputs
- A node that can't be reached
- It is never pushed and keeps
Infinity. Convert it at the end if the problem wants-1. - Every edge costs 1
- Every push goes to the back, so it behaves exactly like plain BFS, and each node enters once.
- Every edge costs 0
- Every push goes to the front, and every reachable node ends with cost 0. The order looks like a depth-first walk.
- Two edges between the same nodes, one costing 0 and one costing 1
- Both are tried. The free one gives the lower cost, and the
>=check makes the other one change nothing. - An edge that costs 2 slips into the input
- It would go to the back with label
d + 2, which breaks the two-value rule, and nodes can come out before their cost is final. Use Dijkstra's algorithm for costs other than 0 and 1.
Variations and follow-ups
Twists interviewers add
Fewest walls to break on a grid
Moving into an open cell costs 0 and moving into a wall costs 1. Run 0-1 BFS over cells with the four directions, and dist of the target cell is the fewest walls broken.
Fewest edges to reverse
For each directed edge a → b, add a → b with cost 0 and b → a with cost 1. The cheapest cost from the start to the target is the fewest edges you must flip to make a path.
Skipping old copies
Push [node, cost] pairs instead of plain nodes. When a pair comes out with a cost bigger than dist[node], skip it, the same trick Dijkstra's algorithm uses.
Small whole-number costs
If costs are whole numbers from 0 to k, keep k + 1 buckets in a circle instead of a deque. This is called Dial's algorithm, and it avoids a heap as long as k is small.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "fewest walls to break", "fewest arrows to change", or "fewest edges to reverse": some moves are free and the others each cost one.
- Explain the deque in one sentence: "free edges go to the front because they keep the same cost; cost-1 edges go to the back, so the deque stays sorted like Dijkstra's heap".
- Test by hand on a graph where a free route improves a node that already has a cost, as with node
4in the trace. That is the case where code with a plain visited mark fails. - Expect "what if costs go up to 5?". Answer: the two-value rule breaks, so use Dijkstra's algorithm, or Dial's buckets for small whole numbers.
Where frontend code uses it
The same idea in real UI code
- Grid map tools where walking on open ground is free and crossing an obstacle costs one action, such as "fewest walls to remove".
- Navigation over a link graph where following a link forward is free but using the browser's back button counts as one extra action.
- Puzzle games in the browser that count only some kinds of moves, such as "rotations used", while other moves are free.