Skip to main content

oxicuda_graphalg/shortest_path/
zero_one_bfs.rs

1//! 0-1 BFS — single-source shortest paths on graphs whose edge weights are
2//! restricted to `{0, 1}`.
3//!
4//! When every edge has weight 0 or 1, Dijkstra's `O(E log V)` heap can be
5//! replaced by a double-ended queue giving `O(V + E)`: relaxing a **0-weight**
6//! edge pushes the neighbour to the *front* of the deque, while a **1-weight**
7//! edge pushes to the *back*. The deque therefore always holds vertices of at
8//! most two consecutive distance levels, so it is processed in non-decreasing
9//! distance order exactly as a priority queue would be — but in linear time.
10//!
11//! This is the standard tool for grid/state-graph problems where some
12//! transitions are "free" and others "cost one".
13//!
14//! ## References
15//! - Bard, E. & others; folklore "0-1 BFS" (see CP-Algorithms,
16//!   "0-1 BFS"). Equivalent to Dial's algorithm with two buckets.
17
18use std::collections::VecDeque;
19
20use crate::error::{GraphalgError, GraphalgResult};
21use crate::repr::weighted_graph::WeightedGraph;
22
23/// Result of a 0-1 BFS run from a single source.
24#[derive(Debug, Clone)]
25pub struct ZeroOneBfsOutput {
26    /// `dist[v]` = shortest distance from source (`u64::MAX` if unreachable).
27    pub dist: Vec<u64>,
28    /// `parent[v]` = predecessor of `v` (`usize::MAX` if none; source is its own).
29    pub parent: Vec<usize>,
30}
31
32/// Compute single-source shortest paths on a graph with `{0, 1}` edge weights.
33///
34/// Edge weights are read from the [`WeightedGraph`]; each must equal `0.0` or
35/// `1.0` (within a small tolerance).
36///
37/// # Errors
38/// - [`GraphalgError::SourceOutOfRange`] if `source >= g.n`.
39/// - [`GraphalgError::InvalidEdgeWeight`] if any edge weight is not 0 or 1.
40pub fn zero_one_bfs(g: &WeightedGraph, source: usize) -> GraphalgResult<ZeroOneBfsOutput> {
41    if source >= g.n {
42        return Err(GraphalgError::SourceOutOfRange {
43            node: source,
44            n: g.n,
45        });
46    }
47    // Validate the {0,1} restriction.
48    for (u, adj) in g.edges.iter().enumerate() {
49        for &(v, w) in adj {
50            let is_zero = w.abs() < 1e-9;
51            let is_one = (w - 1.0).abs() < 1e-9;
52            if !(is_zero || is_one) {
53                return Err(GraphalgError::InvalidEdgeWeight(format!(
54                    "edge ({u},{v}) weight {w} is not 0 or 1"
55                )));
56            }
57        }
58    }
59
60    let n = g.n;
61    let mut dist = vec![u64::MAX; n];
62    let mut parent = vec![usize::MAX; n];
63    dist[source] = 0;
64    parent[source] = source;
65
66    let mut dq: VecDeque<usize> = VecDeque::new();
67    dq.push_front(source);
68
69    while let Some(u) = dq.pop_front() {
70        let du = dist[u];
71        for &(v, w) in g.neighbors(u)? {
72            // `w` is guaranteed 0 or 1 by the validation above.
73            let weight = if w < 0.5 { 0u64 } else { 1u64 };
74            let nd = du.saturating_add(weight);
75            if nd < dist[v] {
76                dist[v] = nd;
77                parent[v] = u;
78                if weight == 0 {
79                    dq.push_front(v);
80                } else {
81                    dq.push_back(v);
82                }
83            }
84        }
85    }
86
87    Ok(ZeroOneBfsOutput { dist, parent })
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn all_ones_is_plain_bfs() {
96        // Path 0 -1 -2 -3 with unit edges → distances 0,1,2,3.
97        let mut g = WeightedGraph::new(4);
98        g.add_edge(0, 1, 1.0).expect("ok");
99        g.add_edge(1, 2, 1.0).expect("ok");
100        g.add_edge(2, 3, 1.0).expect("ok");
101        let out = zero_one_bfs(&g, 0).expect("ok");
102        assert_eq!(out.dist, vec![0, 1, 2, 3]);
103    }
104
105    #[test]
106    fn zero_edge_is_free() {
107        // 0 -(1)-> 1 -(0)-> 2; distance to 2 equals distance to 1.
108        let mut g = WeightedGraph::new(3);
109        g.add_edge(0, 1, 1.0).expect("ok");
110        g.add_edge(1, 2, 0.0).expect("ok");
111        let out = zero_one_bfs(&g, 0).expect("ok");
112        assert_eq!(out.dist[1], 1);
113        assert_eq!(out.dist[2], 1, "0-weight edge should be free");
114    }
115
116    #[test]
117    fn prefers_cheaper_zero_path() {
118        // Two ways to reach 3: direct 0->3 cost 1, or 0->1->2->3 with weights
119        // 0,0,0 cost 0. The 0-cost route must win.
120        let mut g = WeightedGraph::new(4);
121        g.add_edge(0, 3, 1.0).expect("ok");
122        g.add_edge(0, 1, 0.0).expect("ok");
123        g.add_edge(1, 2, 0.0).expect("ok");
124        g.add_edge(2, 3, 0.0).expect("ok");
125        let out = zero_one_bfs(&g, 0).expect("ok");
126        assert_eq!(out.dist[3], 0, "should take the all-zero path");
127    }
128
129    #[test]
130    fn source_distance_zero() {
131        let mut g = WeightedGraph::new(2);
132        g.add_edge(0, 1, 1.0).expect("ok");
133        let out = zero_one_bfs(&g, 0).expect("ok");
134        assert_eq!(out.dist[0], 0);
135        assert_eq!(out.parent[0], 0);
136    }
137
138    #[test]
139    fn unreachable_is_max() {
140        let mut g = WeightedGraph::new(3);
141        g.add_edge(0, 1, 1.0).expect("ok");
142        let out = zero_one_bfs(&g, 0).expect("ok");
143        assert_eq!(out.dist[2], u64::MAX);
144        assert_eq!(out.parent[2], usize::MAX);
145    }
146
147    #[test]
148    fn rejects_non_binary_weight() {
149        let mut g = WeightedGraph::new(2);
150        g.add_edge(0, 1, 2.0).expect("ok");
151        assert!(matches!(
152            zero_one_bfs(&g, 0).unwrap_err(),
153            GraphalgError::InvalidEdgeWeight(_)
154        ));
155    }
156
157    #[test]
158    fn source_out_of_range() {
159        let g = WeightedGraph::new(3);
160        assert!(matches!(
161            zero_one_bfs(&g, 9).unwrap_err(),
162            GraphalgError::SourceOutOfRange { .. }
163        ));
164    }
165
166    #[test]
167    fn matches_unit_bfs_on_grid() {
168        // Undirected unit-weight ring 0-1-2-3-4-0; from 0 distances are
169        // 0,1,2,2,1.
170        let mut g = WeightedGraph::new(5);
171        g.add_undirected_edge(0, 1, 1.0).expect("ok");
172        g.add_undirected_edge(1, 2, 1.0).expect("ok");
173        g.add_undirected_edge(2, 3, 1.0).expect("ok");
174        g.add_undirected_edge(3, 4, 1.0).expect("ok");
175        g.add_undirected_edge(4, 0, 1.0).expect("ok");
176        let out = zero_one_bfs(&g, 0).expect("ok");
177        assert_eq!(out.dist, vec![0, 1, 2, 2, 1]);
178    }
179
180    #[test]
181    fn mixed_weights_correct() {
182        // 0 -(0)-> 1 -(1)-> 2 -(0)-> 3 -(1)-> 4.  dist = 0,0,1,1,2.
183        let mut g = WeightedGraph::new(5);
184        g.add_edge(0, 1, 0.0).expect("ok");
185        g.add_edge(1, 2, 1.0).expect("ok");
186        g.add_edge(2, 3, 0.0).expect("ok");
187        g.add_edge(3, 4, 1.0).expect("ok");
188        let out = zero_one_bfs(&g, 0).expect("ok");
189        assert_eq!(out.dist, vec![0, 0, 1, 1, 2]);
190    }
191
192    #[test]
193    fn parent_chain_reconstructs_path() {
194        let mut g = WeightedGraph::new(4);
195        g.add_edge(0, 1, 1.0).expect("ok");
196        g.add_edge(1, 2, 1.0).expect("ok");
197        g.add_edge(2, 3, 1.0).expect("ok");
198        let out = zero_one_bfs(&g, 0).expect("ok");
199        // Walk parents back from 3 to the source.
200        let mut path = vec![3usize];
201        let mut cur = 3usize;
202        while cur != 0 {
203            cur = out.parent[cur];
204            path.push(cur);
205        }
206        path.reverse();
207        assert_eq!(path, vec![0, 1, 2, 3]);
208    }
209
210    #[test]
211    fn deterministic() {
212        let mut g = WeightedGraph::new(4);
213        g.add_edge(0, 1, 0.0).expect("ok");
214        g.add_edge(0, 2, 1.0).expect("ok");
215        g.add_edge(1, 3, 1.0).expect("ok");
216        g.add_edge(2, 3, 0.0).expect("ok");
217        let a = zero_one_bfs(&g, 0).expect("ok");
218        let b = zero_one_bfs(&g, 0).expect("ok");
219        assert_eq!(a.dist, b.dist);
220    }
221
222    #[test]
223    fn self_loop_ignored() {
224        // A 0-weight self loop must not break termination or distances.
225        let mut g = WeightedGraph::new(2);
226        g.add_edge(0, 0, 0.0).expect("ok");
227        g.add_edge(0, 1, 1.0).expect("ok");
228        let out = zero_one_bfs(&g, 0).expect("ok");
229        assert_eq!(out.dist[0], 0);
230        assert_eq!(out.dist[1], 1);
231    }
232}