Skip to main content

facett_graphview/analysis/
pathfinding.rs

1//! **Pathfinding** — BFS / DFS orders, shortest paths (unweighted BFS + weighted
2//! Dijkstra), and all simple paths between two nodes. Directed (over the `out` view);
3//! pass an [`Adjacency`] built from undirected-doubled edges for undirected search.
4
5use std::cmp::Ordering;
6use std::collections::{BinaryHeap, VecDeque};
7
8use super::Adjacency;
9
10/// **BFS** order from `s` over the directed `out` view (nodes in the order first
11/// visited). Includes `s` first.
12#[must_use]
13pub fn bfs(g: &Adjacency, s: usize) -> Vec<usize> {
14    if s >= g.n {
15        return Vec::new();
16    }
17    let mut seen = vec![false; g.n];
18    let mut order = Vec::new();
19    let mut q = VecDeque::new();
20    seen[s] = true;
21    q.push_back(s);
22    while let Some(v) = q.pop_front() {
23        order.push(v);
24        for &(w, _) in &g.out[v] {
25            if !seen[w] {
26                seen[w] = true;
27                q.push_back(w);
28            }
29        }
30    }
31    order
32}
33
34/// **DFS** pre-order from `s` over the directed `out` view (lowest-index neighbour
35/// first, so it's deterministic). Includes `s` first.
36#[must_use]
37pub fn dfs(g: &Adjacency, s: usize) -> Vec<usize> {
38    if s >= g.n {
39        return Vec::new();
40    }
41    let mut seen = vec![false; g.n];
42    let mut order = Vec::new();
43    let mut stack = vec![s];
44    seen[s] = true;
45    // To emit lowest-index-first we push neighbours in reverse onto the stack.
46    while let Some(v) = stack.pop() {
47        order.push(v);
48        let mut nbrs: Vec<usize> = g.out[v].iter().map(|&(w, _)| w).collect();
49        nbrs.sort_unstable();
50        for &w in nbrs.iter().rev() {
51            if !seen[w] {
52                seen[w] = true;
53                stack.push(w);
54            }
55        }
56    }
57    order
58}
59
60/// **Unweighted shortest path** (BFS) from `s` to `t` over the directed `out` view.
61/// Returns the node sequence `[s, …, t]`, or `None` if `t` is unreachable.
62#[must_use]
63pub fn shortest_path(g: &Adjacency, s: usize, t: usize) -> Option<Vec<usize>> {
64    if s >= g.n || t >= g.n {
65        return None;
66    }
67    if s == t {
68        return Some(vec![s]);
69    }
70    let mut prev = vec![usize::MAX; g.n];
71    let mut seen = vec![false; g.n];
72    let mut q = VecDeque::new();
73    seen[s] = true;
74    q.push_back(s);
75    while let Some(v) = q.pop_front() {
76        for &(w, _) in &g.out[v] {
77            if !seen[w] {
78                seen[w] = true;
79                prev[w] = v;
80                if w == t {
81                    return Some(reconstruct(&prev, s, t));
82                }
83                q.push_back(w);
84            }
85        }
86    }
87    None
88}
89
90/// **Weighted shortest path** (Dijkstra) from `s` to `t` over the directed `out`
91/// view. Edge weights must be non-negative. Returns `(path, cost)` or `None`.
92#[must_use]
93pub fn dijkstra(g: &Adjacency, s: usize, t: usize) -> Option<(Vec<usize>, f32)> {
94    if s >= g.n || t >= g.n {
95        return None;
96    }
97    let mut dist = vec![f32::INFINITY; g.n];
98    let mut prev = vec![usize::MAX; g.n];
99    dist[s] = 0.0;
100    let mut heap = BinaryHeap::new();
101    heap.push(HeapItem { cost: 0.0, node: s });
102    while let Some(HeapItem { cost, node }) = heap.pop() {
103        if node == t {
104            return Some((reconstruct(&prev, s, t), cost));
105        }
106        if cost > dist[node] {
107            continue;
108        }
109        for &(w, weight) in &g.out[node] {
110            let nc = cost + weight.max(0.0);
111            if nc < dist[w] {
112                dist[w] = nc;
113                prev[w] = node;
114                heap.push(HeapItem { cost: nc, node: w });
115            }
116        }
117    }
118    None
119}
120
121/// **All simple paths** from `s` to `t` (no repeated node), each up to `max_len`
122/// nodes long (a guard against exponential blow-up on dense graphs). Directed.
123#[must_use]
124pub fn all_simple_paths(g: &Adjacency, s: usize, t: usize, max_len: usize) -> Vec<Vec<usize>> {
125    let mut out = Vec::new();
126    if s >= g.n || t >= g.n || max_len == 0 {
127        return out;
128    }
129    let mut on_path = vec![false; g.n];
130    let mut path = vec![s];
131    on_path[s] = true;
132    dfs_paths(g, s, t, max_len, &mut on_path, &mut path, &mut out);
133    out
134}
135
136fn dfs_paths(
137    g: &Adjacency,
138    v: usize,
139    t: usize,
140    max_len: usize,
141    on_path: &mut [bool],
142    path: &mut Vec<usize>,
143    out: &mut Vec<Vec<usize>>,
144) {
145    if v == t {
146        out.push(path.clone());
147        return;
148    }
149    if path.len() >= max_len {
150        return;
151    }
152    let mut nbrs: Vec<usize> = g.out[v].iter().map(|&(w, _)| w).collect();
153    nbrs.sort_unstable();
154    for w in nbrs {
155        if !on_path[w] {
156            on_path[w] = true;
157            path.push(w);
158            dfs_paths(g, w, t, max_len, on_path, path, out);
159            path.pop();
160            on_path[w] = false;
161        }
162    }
163}
164
165/// Walk `prev` back from `t` to `s` into a forward path.
166fn reconstruct(prev: &[usize], s: usize, t: usize) -> Vec<usize> {
167    let mut path = vec![t];
168    let mut cur = t;
169    while cur != s {
170        cur = prev[cur];
171        path.push(cur);
172    }
173    path.reverse();
174    path
175}
176
177/// A min-heap item (Dijkstra) — ordered by cost ascending.
178struct HeapItem {
179    cost: f32,
180    node: usize,
181}
182impl PartialEq for HeapItem {
183    fn eq(&self, o: &Self) -> bool {
184        self.cost == o.cost && self.node == o.node
185    }
186}
187impl Eq for HeapItem {}
188impl PartialOrd for HeapItem {
189    fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
190        Some(self.cmp(o))
191    }
192}
193impl Ord for HeapItem {
194    fn cmp(&self, o: &Self) -> Ordering {
195        // Reverse so BinaryHeap (a max-heap) pops the smallest cost first.
196        o.cost.partial_cmp(&self.cost).unwrap_or(Ordering::Equal).then(o.node.cmp(&self.node))
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    fn diamond() -> Adjacency {
205        // 0→1, 0→2, 1→3, 2→3 — two length-2 routes from 0 to 3.
206        Adjacency::from_edges(4, &[(0, 1), (0, 2), (1, 3), (2, 3)])
207    }
208
209    #[test]
210    fn bfs_and_dfs_visit_every_reachable_node() {
211        let g = diamond();
212        let b = bfs(&g, 0);
213        assert_eq!(b[0], 0);
214        assert_eq!(b.len(), 4);
215        let d = dfs(&g, 0);
216        assert_eq!(d[0], 0);
217        assert_eq!(d.len(), 4);
218        // DFS is lowest-index-first: 0,1,3,2.
219        assert_eq!(d, vec![0, 1, 3, 2]);
220    }
221
222    #[test]
223    fn shortest_path_finds_a_length_two_route() {
224        let g = diamond();
225        let p = shortest_path(&g, 0, 3).unwrap();
226        assert_eq!(p.first(), Some(&0));
227        assert_eq!(p.last(), Some(&3));
228        assert_eq!(p.len(), 3, "two hops: 0 → x → 3");
229        assert!(shortest_path(&g, 3, 0).is_none(), "directed: no route back");
230        assert_eq!(shortest_path(&g, 2, 2), Some(vec![2]), "trivial self path");
231    }
232
233    #[test]
234    fn dijkstra_prefers_the_cheaper_route() {
235        // 0→1 (w=1) →3 (w=1) = 2; 0→2 (w=5) →3 (w=1) = 6. Cheapest is via 1.
236        let g = Adjacency::from_weighted(4, &[(0, 1, 1.0), (1, 3, 1.0), (0, 2, 5.0), (2, 3, 1.0)]);
237        let (path, cost) = dijkstra(&g, 0, 3).unwrap();
238        assert_eq!(path, vec![0, 1, 3]);
239        assert!((cost - 2.0).abs() < 1e-6, "cheapest cost is 2 (got {cost})");
240    }
241
242    #[test]
243    fn all_simple_paths_enumerates_both_routes() {
244        let g = diamond();
245        let paths = all_simple_paths(&g, 0, 3, 8);
246        assert_eq!(paths.len(), 2, "the diamond has two simple paths");
247        assert!(paths.contains(&vec![0, 1, 3]));
248        assert!(paths.contains(&vec![0, 2, 3]));
249        // The length guard prunes.
250        assert!(all_simple_paths(&g, 0, 3, 2).is_empty(), "max_len=2 admits no 3-node path");
251    }
252}