0-1 BFS
Key Points
- •0-1 BFS is a shortest path algorithm specialized for graphs whose edge weights are only 0 or 1.
- •It uses a deque: pus when the edge weight is 0, and pus when the edge weight is 1.
- •Because each vertex is processed at most once per successful relaxation, the algorithm runs in O(V + E) time.
- •0-1 BFS is ideal for grid problems where moving onto certain cells is free while others cost 1 (e.g., walls to break).
- •It is a practical, faster alternative to Dijkstra’s algorithm for weights, avoiding a priority queue.
- •Distances are nondecreasing when popped from the front of the deque, ensuring correctness like Dijkstra.
- •It generalizes standard BFS (which is the special case where all edges have weight 0 or 1 but identical).
- •Do not use 0-1 BFS if weights exceed 1 or are negative; use Dijkstra (nonnegative) or Bellman-Ford/SPFA (can handle negatives).
Prerequisites
- →Graphs and adjacency lists — You must know how to represent graphs to iterate over neighbors efficiently and achieve O(V + E) complexity.
- →BFS (Breadth-First Search) — 0-1 BFS generalizes BFS by exploring in nondecreasing cost layers instead of equal-cost layers.
- →Dijkstra’s algorithm basics — Understanding why a priority queue is needed for general weights clarifies why a deque suffices for 0/1 weights.
- →Big-O notation — To analyze and compare the O(V + E) performance guarantee.
- →Shortest path relaxation — Core step common to all shortest path algorithms; necessary for correctness reasoning.
Detailed Explanation
Tap terms for definitions01Overview
Hook: Imagine navigating a city where some roads are fast lanes (free to traverse) and others have a small toll of exactly one unit. You want to reach your destination with the least total toll. Concept: 0-1 BFS is a shortest path algorithm tailored precisely for this setting—graphs where each edge weight is either 0 or 1. Instead of a priority queue (as in Dijkstra), it cleverly uses a double-ended queue (deque) to keep the next best nodes at the front. Example: In a grid maze, stepping onto empty cells might be free (0) while stepping through a door costs 1. 0-1 BFS will find the path with the fewest doors to open.
More formally, 0-1 BFS maintains a distance array and relaxes edges like Dijkstra. When it finds a zero-cost edge to a neighbor, it puts that neighbor at the front of the deque (so it’s processed immediately); when it finds a cost-one edge, it appends the neighbor at the back (to be processed later). This mirrors the behavior of an implicit two-level priority structure without the log-factor overhead.
Because each edge is considered at most once for a successful relaxation and deque operations are O(1), the whole algorithm runs in O(V + E) time and O(V) space. This makes 0-1 BFS a go-to tool in competitive programming (CF 1400–1800 range) for problems like minimal obstacle removals, binary-weighted networks, and layered transitions with free vs. costly moves.
02Intuition & Analogies
Hook: Picture a line at an amusement park with two special rules. If you get a VIP stamp (weight 0), you move to the very front of the line. If you don’t (weight 1), you wait at the back like everyone else. Concept: The deque in 0-1 BFS is that line. Whenever you can move along a free edge, you jump to the front; if the move costs 1, you join the back. This way, people with fewer total waiting costs always get served first—just like exploring paths with smaller distances first.
Analogy: Think of playing a board game where moving onto certain squares is free (like sliding on ice) and others consume exactly one energy point (like stepping over a small fence). If you always explore the free slides immediately (front of deque) and postpone the fence steps slightly (back of deque), you naturally prioritize cheaper total paths without needing a full-blown calculator (priority queue).
Example: In a grid puzzle, suppose moving into a clear cell is free and entering a rough cell costs one. Start at the origin with distance 0. From each position, any free step is explored right away (pushed to front), so all positions reachable at the same cost are expanded before any position with a higher cost is considered. Only when no free expansions remain do we take a step that increases the cost by 1. This layered expansion mimics BFS levels, except levels are “cost layers” rather than “edge-count layers.” That’s why 0-1 BFS generalizes BFS: it preserves the spirit of expanding by increasing cost layers using only a deque.
03Formal Definition
04When to Use
- Binary-weight graphs: Any shortest path problem where every edge weight is either 0 or 1. Examples include toggling states with a free or paid operation, or moving through different types of transitions with a small fixed penalty.
- Grid problems: Minimal walls/obstacles to remove, where moving into an empty cell is 0 and into a wall is 1. This captures common CF tasks like “break minimum walls to reach target.”
- Layered states: Problems modeling states as nodes where some transitions are free (state reuse, same layer) and others cost 1 (move to next layer), such as bitmask DP transitions encoded as a graph.
- Dijkstra alternative: Use 0-1 BFS instead of Dijkstra to remove the O(\log |V|) priority queue overhead when weights are strictly 0/1. This yields O(|V| + |E|), typically faster in practice.
- Multi-source starts: When multiple sources have distance 0 (e.g., many starting portals), initialize the deque with all sources; 0-1 BFS naturally handles this.
Avoid 0-1 BFS if weights exceed 1, are non-integers, or negative. Use Dijkstra for general nonnegative weights and Bellman-Ford/SPFA variants if negative weights exist (with caution about performance).
⚠️Common Mistakes
- Using 0-1 BFS for weights beyond {0, 1}: The deque trick relies on two buckets (front/back). If weights include 2 or more, the method breaks. Switch to Dial’s algorithm (bucketed Dijkstra) for small integer weights, or standard Dijkstra for general nonnegative weights.
- Forgetting proper relaxation: Use “if d[v] > d[u] + w(u, v)” not “>=”. Using “>=” can create unnecessary pushes and even infinite loops with equal distances.
- Not initializing distances to +∞ and sources to 0: Incorrect initial values lead to wrong layering and missed updates.
- Marking visited too early: Do not mark vertices as permanently visited when first seen. In 0-1 BFS, a later 0-edge can improve a vertex’s distance. Use the distance check to control reprocessing; a node is effectively finalized once it’s popped with its minimal distance (which the algorithm ensures).
- Ignoring graph direction or multiple edges: Always relax all edges from u, respecting direction. Multiple parallel edges with different weights are fine; relax each.
- Forgetting to handle disconnected vertices: Unreachable nodes should remain at +∞ (or a sentinel like -1 in output). Don’t print garbage values.
- Overflowing distances: Use a large sentinel like 1e9 for int distances or use long long if paths may have up to |E| ≈ 2e5 causing sums to fit comfortably.
Key Formulas
Relaxation Rule
Explanation: When exploring edge (u, v), we can improve the tentative distance to v by going through u. This is the core step that drives all shortest-path algorithms, including 0-1 BFS.
Time Complexity
Explanation: 0-1 BFS processes each vertex and edge a constant number of times with O(1) deque operations, leading to linear time in the size of the graph.
Grid Size Bounds
Explanation: For an n-by-m grid with 4-neighborhood adjacency, the number of vertices is nm and each cell has at most 4 edges, yielding at most 4nm edges.
Feasible Potentials
Explanation: Distances satisfy the triangle inequality along edges and start from zero at the source. Any shortest-path solution must respect these inequalities.
Binary Weights Constraint
Explanation: 0-1 BFS requires every edge weight to be exactly 0 or 1. Violating this assumption breaks the deque scheduling logic.
Edge Accounting
Explanation: Each edge can cause at most one successful relaxation that inserts a node into the deque, bounding the total number of insertions by the number of edges.
Distance Range
Explanation: With weights 0 or 1, any shortest path has integral cost between 0 and the number of edges, which bounds array sizes and supports bucket-like data structures.
Priority Queue vs Deque
Explanation: Replacing a binary heap (Dijkstra) with a deque under weights removes the log factor, yielding linear time in practice and theory.
Complexity Analysis
Code Examples
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Solves single-source shortest paths on a graph with edge weights 0 or 1 5 // Input format example: 6 // n m s 7 // u v w (m lines, 0-indexed vertices, weight w in {0,1}) 8 // Prints distances from s to every vertex (INF if unreachable) 9 10 int main() { 11 ios::sync_with_stdio(false); 12 cin.tie(nullptr); 13 14 int n, m, s; 15 if (!(cin >> n >> m >> s)) return 0; 16 vector<vector<pair<int,int>>> g(n); 17 for (int i = 0; i < m; ++i) { 18 int u, v, w; cin >> u >> v >> w; 19 // Directed edge u -> v with weight w in {0,1} 20 g[u].push_back({v, w}); 21 // If the graph is undirected, also add g[v].push_back({u, w}); 22 } 23 24 const int INF = 1e9; 25 vector<int> dist(n, INF); 26 deque<int> dq; 27 28 dist[s] = 0; 29 dq.push_front(s); 30 31 while (!dq.empty()) { 32 int u = dq.front(); dq.pop_front(); 33 for (auto [v, w] : g[u]) { 34 // Relaxation: try to improve dist[v] 35 if (dist[v] > dist[u] + w) { 36 dist[v] = dist[u] + w; 37 if (w == 0) dq.push_front(v); else dq.push_back(v); 38 } 39 } 40 } 41 42 for (int i = 0; i < n; ++i) { 43 if (dist[i] == INF) cout << "INF\n"; else cout << dist[i] << "\n"; 44 } 45 return 0; 46 } 47
This program builds a directed graph with binary weights and runs 0-1 BFS from source s. When a 0-weight relaxation occurs, the neighbor is pushed to the front (processed sooner); for a 1-weight relaxation, it’s pushed to the back. Distances are finalized in nondecreasing order, yielding correct shortest paths in O(V + E).
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Given an n x m grid of characters: 5 // '.' means free cell (entering costs 0) 6 // '#' means wall (entering costs 1) 7 // Move in 4 directions. Start at (0,0), target at (n-1,m-1). 8 // Find the minimum number of walls to break to reach the target. 9 10 int main() { 11 ios::sync_with_stdio(false); 12 cin.tie(nullptr); 13 14 int n, m; 15 if (!(cin >> n >> m)) return 0; 16 vector<string> grid(n); 17 for (int i = 0; i < n; ++i) cin >> grid[i]; 18 19 const int INF = 1e9; 20 vector<vector<int>> dist(n, vector<int>(m, INF)); 21 22 auto inside = [&](int r, int c){ return r >= 0 && r < n && c >= 0 && c < m; }; 23 24 deque<pair<int,int>> dq; 25 dist[0][0] = (grid[0][0] == '#'); // cost to enter start cell (often 0 if ensured '.') 26 dq.push_front({0,0}); 27 28 int dr[4] = {-1, 1, 0, 0}; 29 int dc[4] = {0, 0, -1, 1}; 30 31 while (!dq.empty()) { 32 auto [r, c] = dq.front(); dq.pop_front(); 33 for (int k = 0; k < 4; ++k) { 34 int nr = r + dr[k], nc = c + dc[k]; 35 if (!inside(nr, nc)) continue; 36 int w = (grid[nr][nc] == '#') ? 1 : 0; // cost to enter next cell 37 if (dist[nr][nc] > dist[r][c] + w) { 38 dist[nr][nc] = dist[r][c] + w; 39 if (w == 0) dq.push_front({nr, nc}); else dq.push_back({nr, nc}); 40 } 41 } 42 } 43 44 int ans = dist[n-1][m-1]; 45 if (ans >= INF) cout << -1 << "\n"; else cout << ans << "\n"; 46 return 0; 47 } 48
Cells are nodes; edges connect 4-neighbors. Entering a wall cell has weight 1, entering a free cell has weight 0. Using 0-1 BFS over the implicit grid graph yields the minimal number of walls to break from start to goal in O(nm).
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Multi-source 0-1 BFS: distances from any source in S to all nodes, with path reconstruction to target t. 5 // Input example: 6 // n m k 7 // s1 s2 ... sk (k sources) 8 // u v w (m lines, directed, w in {0,1}) 9 // t (target) 10 11 int main() { 12 ios::sync_with_stdio(false); 13 cin.tie(nullptr); 14 15 int n, m, k; 16 if (!(cin >> n >> m >> k)) return 0; 17 vector<int> sources(k); 18 for (int i = 0; i < k; ++i) cin >> sources[i]; 19 vector<vector<pair<int,int>>> g(n); 20 for (int i = 0; i < m; ++i) { 21 int u, v, w; cin >> u >> v >> w; 22 g[u].push_back({v, w}); 23 } 24 int t; cin >> t; 25 26 const int INF = 1e9; 27 vector<int> dist(n, INF), par(n, -1), parW(n, -1); 28 deque<int> dq; 29 30 // Initialize all sources with distance 0 31 for (int s : sources) { 32 if (dist[s] > 0) { 33 dist[s] = 0; 34 dq.push_front(s); 35 } 36 } 37 38 while (!dq.empty()) { 39 int u = dq.front(); dq.pop_front(); 40 for (auto [v, w] : g[u]) { 41 if (dist[v] > dist[u] + w) { 42 dist[v] = dist[u] + w; 43 par[v] = u; // store predecessor for path reconstruction 44 parW[v] = w; // store edge weight used 45 if (w == 0) dq.push_front(v); else dq.push_back(v); 46 } 47 } 48 } 49 50 if (dist[t] >= INF) { 51 cout << "No path\n"; 52 return 0; 53 } 54 55 // Reconstruct one shortest path to t 56 vector<int> path; 57 for (int v = t; v != -1; v = par[v]) path.push_back(v); 58 reverse(path.begin(), path.end()); 59 60 cout << "Distance: " << dist[t] << "\n"; 61 cout << "Path (" << path.size() << " nodes): "; 62 for (int i = 0; i < (int)path.size(); ++i) cout << path[i] << (i+1==(int)path.size()? '\n' : ' '); 63 64 // Optional: print edge weights along the path 65 cout << "Edge weights along path: "; 66 for (int i = 1; i < (int)path.size(); ++i) cout << parW[path[i]] << (i+1==(int)path.size()? '\n' : ' '); 67 68 return 0; 69 } 70
This variant initializes multiple sources at distance 0 and uses the same deque logic. It also stores parent pointers to reconstruct one shortest path to a given target t. Parent and edge-weight arrays are updated upon successful relaxation so the reconstructed path aligns with the minimal distance.