Skip to main content

samyama_graph_algorithms/
pathfinding.rs

1//! Pathfinding algorithms
2//!
3//! Implements REQ-ALGO-002 (BFS) and REQ-ALGO-003 (Dijkstra)
4
5use super::common::{GraphView, NodeId};
6use std::collections::{HashMap, VecDeque, BinaryHeap};
7use std::cmp::Ordering;
8
9/// Result of a pathfinding algorithm
10#[derive(Debug, Clone)]
11pub struct PathResult {
12    pub source: NodeId,
13    pub target: NodeId,
14    pub path: Vec<NodeId>,
15    pub cost: f64,
16}
17
18/// Breadth-First Search (Unweighted Shortest Path)
19pub fn bfs(
20    view: &GraphView,
21    source: NodeId,
22    target: NodeId,
23) -> Option<PathResult> {
24    let source_idx = *view.node_to_index.get(&source)?;
25    let target_idx = *view.node_to_index.get(&target)?;
26
27    let mut queue = VecDeque::new();
28    let mut visited = HashMap::new(); // index -> parent_index
29    
30    queue.push_back(source_idx);
31    visited.insert(source_idx, None);
32
33    while let Some(current_idx) = queue.pop_front() {
34        if current_idx == target_idx {
35            // Reconstruct path
36            let mut path = Vec::new();
37            let mut curr = Some(target_idx);
38            while let Some(idx) = curr {
39                path.push(view.index_to_node[idx]);
40                if let Some(parent) = visited.get(&idx) {
41                    curr = *parent;
42                } else {
43                    curr = None;
44                }
45            }
46            path.reverse();
47            return Some(PathResult {
48                source,
49                target,
50                cost: (path.len() - 1) as f64,
51                path,
52            });
53        }
54
55        for &next_idx in view.successors(current_idx) {
56            if !visited.contains_key(&next_idx) {
57                visited.insert(next_idx, Some(current_idx));
58                queue.push_back(next_idx);
59            }
60        }
61    }
62
63    None
64}
65
66/// State for Dijkstra priority queue
67#[derive(Copy, Clone, PartialEq)]
68struct State {
69    cost: f64,
70    node_idx: usize,
71}
72
73impl Eq for State {}
74
75impl Ord for State {
76    fn cmp(&self, other: &Self) -> Ordering {
77        // Compare costs reversed for min-heap
78        other.cost.partial_cmp(&self.cost).unwrap_or(Ordering::Equal)
79    }
80}
81
82impl PartialOrd for State {
83    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
84        Some(self.cmp(other))
85    }
86}
87
88/// Dijkstra's Algorithm (Weighted Shortest Path)
89///
90/// Uses edge weights from GraphView if available, otherwise assumes 1.0.
91pub fn dijkstra(
92    view: &GraphView,
93    source: NodeId,
94    target: NodeId,
95) -> Option<PathResult> {
96    let source_idx = *view.node_to_index.get(&source)?;
97    let target_idx = *view.node_to_index.get(&target)?;
98
99    let mut dist = HashMap::new();
100    let mut parent = HashMap::new();
101    let mut heap = BinaryHeap::new();
102
103    dist.insert(source_idx, 0.0);
104    heap.push(State { cost: 0.0, node_idx: source_idx });
105
106    while let Some(State { cost, node_idx }) = heap.pop() {
107        if node_idx == target_idx {
108            // Reconstruct path
109            let mut path = Vec::new();
110            let mut curr = Some(target_idx);
111            while let Some(idx) = curr {
112                path.push(view.index_to_node[idx]);
113                curr = parent.get(&idx).cloned().flatten();
114            }
115            path.reverse();
116            return Some(PathResult {
117                source,
118                target,
119                path,
120                cost,
121            });
122        }
123
124        if cost > *dist.get(&node_idx).unwrap_or(&f64::INFINITY) {
125            continue;
126        }
127
128        let edges = view.successors(node_idx);
129        let weights = view.weights(node_idx);
130
131        for (i, &next_idx) in edges.iter().enumerate() {
132            let weight = if let Some(w) = weights {
133                w[i]
134            } else {
135                1.0
136            };
137
138            if weight < 0.0 { continue; }
139
140            let next_cost = cost + weight;
141
142            if next_cost < *dist.get(&next_idx).unwrap_or(&f64::INFINITY) {
143                dist.insert(next_idx, next_cost);
144                parent.insert(next_idx, Some(node_idx));
145                heap.push(State { cost: next_cost, node_idx: next_idx });
146            }
147        }
148    }
149
150    None
151}
152
153/// BFS that returns ALL shortest paths between source and target
154pub fn bfs_all_shortest_paths(
155    view: &GraphView,
156    source: NodeId,
157    target: NodeId,
158) -> Vec<PathResult> {
159    let source_idx = match view.node_to_index.get(&source) {
160        Some(&idx) => idx,
161        None => return vec![],
162    };
163    let target_idx = match view.node_to_index.get(&target) {
164        Some(&idx) => idx,
165        None => return vec![],
166    };
167
168    if source_idx == target_idx {
169        return vec![PathResult {
170            source,
171            target,
172            path: vec![source],
173            cost: 0.0,
174        }];
175    }
176
177    // BFS tracking all parents for each discovered node
178    let mut parents: HashMap<usize, Vec<usize>> = HashMap::new();
179    let mut distance: HashMap<usize, usize> = HashMap::new();
180    let mut queue = VecDeque::new();
181
182    queue.push_back(source_idx);
183    distance.insert(source_idx, 0);
184
185    let mut target_distance: Option<usize> = None;
186
187    while let Some(current) = queue.pop_front() {
188        let current_dist = distance[&current];
189
190        // Stop if we've gone past the target distance
191        if let Some(td) = target_distance {
192            if current_dist >= td {
193                continue;
194            }
195        }
196
197        for &next_idx in view.successors(current) {
198            let next_dist = current_dist + 1;
199
200            if let Some(&existing_dist) = distance.get(&next_idx) {
201                if next_dist == existing_dist {
202                    // Same distance — add as additional parent
203                    parents.entry(next_idx).or_default().push(current);
204                }
205                // Longer path — skip
206            } else {
207                // First time reaching this node
208                distance.insert(next_idx, next_dist);
209                parents.insert(next_idx, vec![current]);
210                queue.push_back(next_idx);
211
212                if next_idx == target_idx {
213                    target_distance = Some(next_dist);
214                }
215            }
216        }
217    }
218
219    // If target not reached, return empty
220    if !distance.contains_key(&target_idx) {
221        return vec![];
222    }
223
224    // Reconstruct all paths from target back to source
225    let mut all_paths = Vec::new();
226    let mut stack: Vec<(usize, Vec<usize>)> = vec![(target_idx, vec![target_idx])];
227
228    while let Some((node, partial_path)) = stack.pop() {
229        if node == source_idx {
230            let mut path: Vec<NodeId> = partial_path.iter()
231                .rev()
232                .map(|&idx| view.index_to_node[idx])
233                .collect();
234            all_paths.push(PathResult {
235                source,
236                target,
237                cost: (path.len() - 1) as f64,
238                path,
239            });
240            continue;
241        }
242
243        if let Some(parent_list) = parents.get(&node) {
244            for &parent in parent_list {
245                let mut new_path = partial_path.clone();
246                new_path.push(parent);
247                stack.push((parent, new_path));
248            }
249        }
250    }
251
252    all_paths
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use std::collections::HashMap;
259    use crate::common::GraphView;
260
261    #[test]
262    fn test_bfs() {
263        // 1->2->3
264        let index_to_node = vec![1, 2, 3];
265        let mut node_to_index = HashMap::new();
266        node_to_index.insert(1, 0);
267        node_to_index.insert(2, 1);
268        node_to_index.insert(3, 2);
269
270        let mut outgoing = vec![vec![]; 3];
271        outgoing[0].push(1);
272        outgoing[1].push(2);
273
274        let view = GraphView::from_adjacency_list(
275            3,
276            index_to_node,
277            node_to_index,
278            outgoing,
279            vec![vec![]; 3],
280            None,
281        );
282
283        let result = bfs(&view, 1, 3).unwrap();
284        assert_eq!(result.path, vec![1, 2, 3]);
285        assert_eq!(result.cost, 2.0);
286    }
287
288    #[test]
289    fn test_dijkstra() {
290        // 1->2 (10.0), 2->3 (5.0), 1->3 (50.0)
291        let index_to_node = vec![1, 2, 3];
292        let mut node_to_index = HashMap::new();
293        node_to_index.insert(1, 0);
294        node_to_index.insert(2, 1);
295        node_to_index.insert(3, 2);
296
297        let mut outgoing = vec![vec![]; 3];
298        let mut weights = vec![vec![]; 3];
299
300        outgoing[0].push(1); weights[0].push(10.0);
301        outgoing[0].push(2); weights[0].push(50.0); // Direct 1->3
302        outgoing[1].push(2); weights[1].push(5.0);
303
304        let view = GraphView::from_adjacency_list(
305            3,
306            index_to_node,
307            node_to_index,
308            outgoing,
309            vec![vec![]; 3],
310            Some(weights),
311        );
312
313        let result = dijkstra(&view, 1, 3).unwrap();
314        assert_eq!(result.path, vec![1, 2, 3]);
315        assert_eq!(result.cost, 15.0);
316    }
317
318    #[test]
319    fn test_bfs_all_shortest_paths() {
320        // Diamond: 1->2, 1->3, 2->4, 3->4
321        let index_to_node = vec![1, 2, 3, 4];
322        let mut node_to_index = HashMap::new();
323        node_to_index.insert(1, 0);
324        node_to_index.insert(2, 1);
325        node_to_index.insert(3, 2);
326        node_to_index.insert(4, 3);
327
328        let mut outgoing = vec![vec![]; 4];
329        outgoing[0] = vec![1, 2]; // 1->2, 1->3
330        outgoing[1] = vec![3];    // 2->4
331        outgoing[2] = vec![3];    // 3->4
332
333        let view = GraphView::from_adjacency_list(
334            4,
335            index_to_node,
336            node_to_index,
337            outgoing,
338            vec![vec![]; 4],
339            None,
340        );
341
342        let results = bfs_all_shortest_paths(&view, 1, 4);
343        assert_eq!(results.len(), 2, "Should find 2 shortest paths in diamond graph");
344        for r in &results {
345            assert_eq!(r.cost, 2.0);
346            assert_eq!(r.path.len(), 3);
347            assert_eq!(r.path[0], 1);
348            assert_eq!(r.path[2], 4);
349        }
350    }
351
352    #[test]
353    fn test_bfs_all_shortest_paths_no_path() {
354        let index_to_node = vec![1, 2];
355        let mut node_to_index = HashMap::new();
356        node_to_index.insert(1, 0);
357        node_to_index.insert(2, 1);
358
359        let view = GraphView::from_adjacency_list(
360            2,
361            index_to_node,
362            node_to_index,
363            vec![vec![], vec![]],
364            vec![vec![], vec![]],
365            None,
366        );
367
368        let results = bfs_all_shortest_paths(&view, 1, 2);
369        assert!(results.is_empty());
370    }
371}