facett_graphview/analysis/
pathfinding.rs1use std::cmp::Ordering;
6use std::collections::{BinaryHeap, VecDeque};
7
8use super::Adjacency;
9
10#[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#[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 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#[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#[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#[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
165fn 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
177struct 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 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 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 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 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 assert!(all_simple_paths(&g, 0, 3, 2).is_empty(), "max_len=2 admits no 3-node path");
251 }
252}