Skip to main content

trueno_graph/algorithms/
shortest_path.rs

1//! Shortest path algorithms: Dijkstra's algorithm
2//!
3//! Provides shortest path computation for weighted graphs:
4//! - `dijkstra`: Single-source shortest paths with non-negative weights
5//! - `dijkstra_path`: Shortest path between two specific nodes
6//!
7//! # Example
8//!
9//! ```
10//! use trueno_graph::{CsrGraph, NodeId, dijkstra};
11//!
12//! // Build a weighted graph
13//! let edges = vec![
14//!     (NodeId(0), NodeId(1), 1.0),
15//!     (NodeId(1), NodeId(2), 2.0),
16//!     (NodeId(0), NodeId(2), 5.0),
17//! ];
18//! let graph = CsrGraph::from_edge_list(&edges).unwrap();
19//!
20//! // Find shortest paths from node 0
21//! let distances = dijkstra(&graph, NodeId(0));
22//! assert_eq!(distances.get(&NodeId(0)), Some(&0.0));
23//! assert_eq!(distances.get(&NodeId(1)), Some(&1.0));
24//! assert_eq!(distances.get(&NodeId(2)), Some(&3.0)); // 0→1→2 = 3.0, not 0→2 = 5.0
25//! ```
26
27use crate::storage::CsrGraph;
28use crate::NodeId;
29use std::cmp::Ordering;
30use std::collections::{BinaryHeap, HashMap};
31
32/// State for Dijkstra's priority queue
33#[derive(Clone, Copy)]
34struct State {
35    cost: f32,
36    node: usize,
37}
38
39impl PartialEq for State {
40    fn eq(&self, other: &Self) -> bool {
41        self.cost == other.cost && self.node == other.node
42    }
43}
44
45impl Eq for State {}
46
47impl Ord for State {
48    fn cmp(&self, other: &Self) -> Ordering {
49        // Reverse ordering for min-heap (BinaryHeap is max-heap by default)
50        other
51            .cost
52            .partial_cmp(&self.cost)
53            .unwrap_or(Ordering::Equal)
54            .then_with(|| self.node.cmp(&other.node))
55    }
56}
57
58impl PartialOrd for State {
59    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
60        Some(self.cmp(other))
61    }
62}
63
64/// Compute single-source shortest paths using Dijkstra's algorithm
65///
66/// Finds the shortest path from the source node to all reachable nodes.
67/// Edge weights are used as distances (must be non-negative).
68///
69/// # Arguments
70///
71/// * `graph` - The CSR graph with edge weights as distances
72/// * `source` - The starting node
73///
74/// # Returns
75///
76/// A `HashMap` mapping each reachable `NodeId` to its shortest distance from source.
77/// Unreachable nodes are not included in the map.
78///
79/// # Complexity
80///
81/// O((V + E) log V) using a binary heap
82///
83/// # Example
84///
85/// ```
86/// use trueno_graph::{CsrGraph, NodeId, dijkstra};
87///
88/// let edges = vec![
89///     (NodeId(0), NodeId(1), 4.0),
90///     (NodeId(0), NodeId(2), 1.0),
91///     (NodeId(2), NodeId(1), 2.0),
92/// ];
93/// let graph = CsrGraph::from_edge_list(&edges).unwrap();
94///
95/// let distances = dijkstra(&graph, NodeId(0));
96/// // Shortest to node 1: 0→2→1 = 3.0 (not 0→1 = 4.0)
97/// assert_eq!(distances.get(&NodeId(1)), Some(&3.0));
98/// ```
99#[must_use]
100pub fn dijkstra(graph: &CsrGraph, source: NodeId) -> HashMap<NodeId, f32> {
101    let n = graph.num_nodes();
102    if n == 0 {
103        return HashMap::new();
104    }
105
106    let source_idx = source.0 as usize;
107    if source_idx >= n {
108        return HashMap::new();
109    }
110
111    let mut distances: HashMap<NodeId, f32> = HashMap::new();
112    let mut heap = BinaryHeap::new();
113
114    // Start with source
115    distances.insert(source, 0.0);
116    heap.push(State { cost: 0.0, node: source_idx });
117
118    while let Some(State { cost, node }) = heap.pop() {
119        #[allow(clippy::cast_possible_truncation)]
120        let node_id = NodeId(node as u32);
121
122        // Skip if we've found a better path
123        if let Some(&d) = distances.get(&node_id) {
124            if cost > d {
125                continue;
126            }
127        }
128
129        // Explore neighbors using adjacency (targets, weights)
130        let (neighbors, weights) = graph.adjacency(node_id);
131        for (i, &neighbor) in neighbors.iter().enumerate() {
132            let neighbor_id = NodeId(neighbor);
133            let weight = weights.get(i).copied().unwrap_or(1.0);
134
135            let next_cost = cost + weight;
136
137            // Update if this path is shorter
138            let is_shorter = distances.get(&neighbor_id).map_or(true, |&d| next_cost < d);
139
140            if is_shorter {
141                distances.insert(neighbor_id, next_cost);
142                heap.push(State { cost: next_cost, node: neighbor as usize });
143            }
144        }
145    }
146
147    distances
148}
149
150/// Find the shortest path between two nodes
151///
152/// Returns the distance and the path (sequence of nodes) from source to target.
153///
154/// # Arguments
155///
156/// * `graph` - The CSR graph with edge weights as distances
157/// * `source` - The starting node
158/// * `target` - The destination node
159///
160/// # Returns
161///
162/// * `Some((distance, path))` if a path exists
163/// * `None` if target is unreachable from source
164///
165/// # Example
166///
167/// ```
168/// use trueno_graph::{CsrGraph, NodeId, dijkstra_path};
169///
170/// let edges = vec![
171///     (NodeId(0), NodeId(1), 1.0),
172///     (NodeId(1), NodeId(2), 2.0),
173/// ];
174/// let graph = CsrGraph::from_edge_list(&edges).unwrap();
175///
176/// let result = dijkstra_path(&graph, NodeId(0), NodeId(2));
177/// assert!(result.is_some());
178/// let (dist, path) = result.unwrap();
179/// assert_eq!(dist, 3.0);
180/// assert_eq!(path, vec![NodeId(0), NodeId(1), NodeId(2)]);
181/// ```
182#[must_use]
183/// Reconstruct the shortest path from predecessors map
184fn reconstruct_path(predecessors: &HashMap<NodeId, NodeId>, target: NodeId) -> Vec<NodeId> {
185    let mut path = vec![target];
186    let mut current = target;
187    while let Some(&pred) = predecessors.get(&current) {
188        path.push(pred);
189        current = pred;
190    }
191    path.reverse();
192    path
193}
194
195/// Dijkstra shortest path between `source` and `target`, returning `(cost, path)` if reachable.
196#[must_use]
197pub fn dijkstra_path(
198    graph: &CsrGraph,
199    source: NodeId,
200    target: NodeId,
201) -> Option<(f32, Vec<NodeId>)> {
202    let n = graph.num_nodes();
203    if n == 0 || source.0 as usize >= n || target.0 as usize >= n {
204        return None;
205    }
206
207    if source == target {
208        return Some((0.0, vec![source]));
209    }
210
211    let mut distances: HashMap<NodeId, f32> = HashMap::new();
212    let mut predecessors: HashMap<NodeId, NodeId> = HashMap::new();
213    let mut heap = BinaryHeap::new();
214
215    distances.insert(source, 0.0);
216    heap.push(State { cost: 0.0, node: source.0 as usize });
217
218    while let Some(State { cost, node }) = heap.pop() {
219        #[allow(clippy::cast_possible_truncation)]
220        let node_id = NodeId(node as u32);
221
222        if node_id == target {
223            let path = reconstruct_path(&predecessors, target);
224            return Some((cost, path));
225        }
226
227        if distances.get(&node_id).is_some_and(|&d| cost > d) {
228            continue;
229        }
230
231        let (neighbors, weights) = graph.adjacency(node_id);
232        for (i, &neighbor) in neighbors.iter().enumerate() {
233            let neighbor_id = NodeId(neighbor);
234            let weight = weights.get(i).copied().unwrap_or(1.0);
235            let next_cost = cost + weight;
236
237            if distances.get(&neighbor_id).map_or(true, |&d| next_cost < d) {
238                distances.insert(neighbor_id, next_cost);
239                predecessors.insert(neighbor_id, node_id);
240                heap.push(State { cost: next_cost, node: neighbor as usize });
241            }
242        }
243    }
244
245    None
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_empty_graph() {
254        let graph = CsrGraph::new();
255        let distances = dijkstra(&graph, NodeId(0));
256        assert!(distances.is_empty());
257    }
258
259    #[test]
260    fn test_single_edge() {
261        let edges = vec![(NodeId(0), NodeId(1), 5.0)];
262        let graph = CsrGraph::from_edge_list(&edges).unwrap();
263
264        let distances = dijkstra(&graph, NodeId(0));
265        assert_eq!(distances.get(&NodeId(0)), Some(&0.0));
266        assert_eq!(distances.get(&NodeId(1)), Some(&5.0));
267    }
268
269    #[test]
270    fn test_chain() {
271        // 0 --1.0--> 1 --2.0--> 2
272        let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(1), NodeId(2), 2.0)];
273        let graph = CsrGraph::from_edge_list(&edges).unwrap();
274
275        let distances = dijkstra(&graph, NodeId(0));
276        assert_eq!(distances.get(&NodeId(0)), Some(&0.0));
277        assert_eq!(distances.get(&NodeId(1)), Some(&1.0));
278        assert_eq!(distances.get(&NodeId(2)), Some(&3.0));
279    }
280
281    #[test]
282    fn test_shorter_path_via_intermediate() {
283        // Direct: 0 --5.0--> 2
284        // Via 1:  0 --1.0--> 1 --2.0--> 2 (total: 3.0)
285        let edges = vec![
286            (NodeId(0), NodeId(1), 1.0),
287            (NodeId(1), NodeId(2), 2.0),
288            (NodeId(0), NodeId(2), 5.0),
289        ];
290        let graph = CsrGraph::from_edge_list(&edges).unwrap();
291
292        let distances = dijkstra(&graph, NodeId(0));
293        assert_eq!(distances.get(&NodeId(2)), Some(&3.0)); // Not 5.0
294    }
295
296    #[test]
297    fn test_unreachable_node() {
298        // 0 → 1, 2 → 3 (disconnected)
299        let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(2), NodeId(3), 1.0)];
300        let graph = CsrGraph::from_edge_list(&edges).unwrap();
301
302        let distances = dijkstra(&graph, NodeId(0));
303        assert_eq!(distances.get(&NodeId(0)), Some(&0.0));
304        assert_eq!(distances.get(&NodeId(1)), Some(&1.0));
305        assert!(distances.get(&NodeId(2)).is_none());
306        assert!(distances.get(&NodeId(3)).is_none());
307    }
308
309    #[test]
310    fn test_dijkstra_path_simple() {
311        let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(1), NodeId(2), 2.0)];
312        let graph = CsrGraph::from_edge_list(&edges).unwrap();
313
314        let result = dijkstra_path(&graph, NodeId(0), NodeId(2));
315        assert!(result.is_some());
316        let (dist, path) = result.unwrap();
317        assert_eq!(dist, 3.0);
318        assert_eq!(path, vec![NodeId(0), NodeId(1), NodeId(2)]);
319    }
320
321    #[test]
322    fn test_dijkstra_path_same_node() {
323        let edges = vec![(NodeId(0), NodeId(1), 1.0)];
324        let graph = CsrGraph::from_edge_list(&edges).unwrap();
325
326        let result = dijkstra_path(&graph, NodeId(0), NodeId(0));
327        assert!(result.is_some());
328        let (dist, path) = result.unwrap();
329        assert_eq!(dist, 0.0);
330        assert_eq!(path, vec![NodeId(0)]);
331    }
332
333    #[test]
334    fn test_dijkstra_path_unreachable() {
335        let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(2), NodeId(3), 1.0)];
336        let graph = CsrGraph::from_edge_list(&edges).unwrap();
337
338        let result = dijkstra_path(&graph, NodeId(0), NodeId(3));
339        assert!(result.is_none());
340    }
341
342    #[test]
343    fn test_dijkstra_path_chooses_shorter() {
344        // 0 --5.0--> 2 (direct)
345        // 0 --1.0--> 1 --2.0--> 2 (via 1, total 3.0)
346        let edges = vec![
347            (NodeId(0), NodeId(1), 1.0),
348            (NodeId(1), NodeId(2), 2.0),
349            (NodeId(0), NodeId(2), 5.0),
350        ];
351        let graph = CsrGraph::from_edge_list(&edges).unwrap();
352
353        let result = dijkstra_path(&graph, NodeId(0), NodeId(2));
354        assert!(result.is_some());
355        let (dist, path) = result.unwrap();
356        assert_eq!(dist, 3.0);
357        assert_eq!(path, vec![NodeId(0), NodeId(1), NodeId(2)]);
358    }
359
360    #[test]
361    fn test_diamond_shortest_path() {
362        // Diamond with different weights
363        //     1 (weight 1)
364        //    / \
365        //   0   3  (1→3: 1, 2→3: 5)
366        //    \ /
367        //     2 (weight 2)
368        let edges = vec![
369            (NodeId(0), NodeId(1), 1.0),
370            (NodeId(0), NodeId(2), 2.0),
371            (NodeId(1), NodeId(3), 1.0),
372            (NodeId(2), NodeId(3), 5.0),
373        ];
374        let graph = CsrGraph::from_edge_list(&edges).unwrap();
375
376        let result = dijkstra_path(&graph, NodeId(0), NodeId(3));
377        assert!(result.is_some());
378        let (dist, path) = result.unwrap();
379        assert_eq!(dist, 2.0); // 0→1→3
380        assert_eq!(path, vec![NodeId(0), NodeId(1), NodeId(3)]);
381    }
382
383    #[test]
384    fn test_cycle_in_graph() {
385        // Cycle: 0 → 1 → 2 → 0, with 0 → 3
386        let edges = vec![
387            (NodeId(0), NodeId(1), 1.0),
388            (NodeId(1), NodeId(2), 1.0),
389            (NodeId(2), NodeId(0), 1.0),
390            (NodeId(0), NodeId(3), 10.0),
391        ];
392        let graph = CsrGraph::from_edge_list(&edges).unwrap();
393
394        let distances = dijkstra(&graph, NodeId(0));
395        assert_eq!(distances.get(&NodeId(0)), Some(&0.0));
396        assert_eq!(distances.get(&NodeId(1)), Some(&1.0));
397        assert_eq!(distances.get(&NodeId(2)), Some(&2.0));
398        assert_eq!(distances.get(&NodeId(3)), Some(&10.0));
399    }
400
401    #[test]
402    fn test_source_out_of_bounds() {
403        let edges = vec![(NodeId(0), NodeId(1), 1.0)];
404        let graph = CsrGraph::from_edge_list(&edges).unwrap();
405
406        let distances = dijkstra(&graph, NodeId(100));
407        assert!(distances.is_empty());
408    }
409
410    #[test]
411    fn test_zero_weight_edge() {
412        let edges = vec![(NodeId(0), NodeId(1), 0.0), (NodeId(1), NodeId(2), 0.0)];
413        let graph = CsrGraph::from_edge_list(&edges).unwrap();
414
415        let distances = dijkstra(&graph, NodeId(0));
416        assert_eq!(distances.get(&NodeId(2)), Some(&0.0));
417    }
418}