Skip to main content

rust_igraph/algorithms/paths/
get_all_shortest_paths_dijkstra.rs

1//! Weighted all-shortest-paths from a single source (`ALGO-SP-037`).
2//!
3//! Counterpart of `igraph_get_all_shortest_paths_dijkstra()` from
4//! `references/igraph/src/paths/dijkstra.c:797-1228`.
5//!
6//! Returns every shortest path from a source vertex to all reachable
7//! targets, using Dijkstra with multi-parent tracking.
8
9use std::collections::BinaryHeap;
10
11use crate::core::graph::EdgeId;
12use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
13
14use super::dijkstra::{DijkstraMode, incident_for_mode, validate_weights};
15
16const EPSILON: f64 = 1e-10;
17
18#[derive(PartialEq)]
19struct Frontier(f64, VertexId);
20
21impl Eq for Frontier {}
22
23impl Ord for Frontier {
24    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
25        other.0.total_cmp(&self.0).then(other.1.cmp(&self.1))
26    }
27}
28
29impl PartialOrd for Frontier {
30    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
31        Some(self.cmp(other))
32    }
33}
34
35/// Result of [`get_all_shortest_paths_dijkstra`].
36#[derive(Debug, Clone)]
37pub struct AllShortestPathsDijkstra {
38    /// All shortest paths grouped by target vertex. `paths[v]` is the
39    /// list of all shortest paths from the source to vertex `v`. If `v`
40    /// is unreachable, `paths[v]` is empty. Each path includes both
41    /// endpoints.
42    pub paths: Vec<Vec<Vec<VertexId>>>,
43    /// Edge paths grouped by target vertex. `edge_paths[v]` is the list
44    /// of edge sequences along each shortest path from source to `v`.
45    pub edge_paths: Vec<Vec<Vec<EdgeId>>>,
46    /// Number of shortest paths to each vertex.
47    pub nrgeo: Vec<u64>,
48}
49
50/// Find all shortest paths from `source` to every reachable vertex using
51/// Dijkstra's algorithm with non-negative edge weights.
52///
53/// When `weights` is `None`, all edges are treated as having weight 1
54/// (unweighted BFS-like traversal via Dijkstra).
55///
56/// For directed graphs, use [`get_all_shortest_paths_dijkstra_with_mode`]
57/// to control direction.
58///
59/// # Errors
60///
61/// - [`IgraphError::VertexOutOfRange`] if `source >= vcount()`.
62/// - [`IgraphError::InvalidArgument`] if weights contain negative, NaN,
63///   or non-finite values, or if `weights.len() != ecount()`.
64///
65/// # Examples
66///
67/// ```
68/// use rust_igraph::{Graph, get_all_shortest_paths_dijkstra};
69///
70/// // Diamond: 0-1, 0-2, 1-3, 2-3. Weights: 1, 1, 1, 1.
71/// // Two shortest paths from 0 to 3: 0→1→3 and 0→2→3, both cost 2.
72/// let mut g = Graph::with_vertices(4);
73/// g.add_edge(0, 1).unwrap();
74/// g.add_edge(0, 2).unwrap();
75/// g.add_edge(1, 3).unwrap();
76/// g.add_edge(2, 3).unwrap();
77/// let r = get_all_shortest_paths_dijkstra(&g, 0, &[1.0, 1.0, 1.0, 1.0]).unwrap();
78/// assert_eq!(r.paths[3].len(), 2);
79/// assert_eq!(r.nrgeo[3], 2);
80/// ```
81pub fn get_all_shortest_paths_dijkstra(
82    graph: &Graph,
83    source: VertexId,
84    weights: &[f64],
85) -> IgraphResult<AllShortestPathsDijkstra> {
86    get_all_shortest_paths_dijkstra_with_mode(graph, source, weights, DijkstraMode::Out)
87}
88
89/// Find all shortest paths from `source` with direction control.
90///
91/// For undirected graphs, `mode` is ignored.
92///
93/// # Errors
94///
95/// Same as [`get_all_shortest_paths_dijkstra`].
96///
97/// # Examples
98///
99/// ```
100/// use rust_igraph::{Graph, DijkstraMode, get_all_shortest_paths_dijkstra_with_mode};
101///
102/// let mut g = Graph::new(4, true).unwrap();
103/// g.add_edge(0, 1).unwrap();
104/// g.add_edge(0, 2).unwrap();
105/// g.add_edge(1, 3).unwrap();
106/// g.add_edge(2, 3).unwrap();
107/// let r = get_all_shortest_paths_dijkstra_with_mode(
108///     &g, 0, &[1.0, 1.0, 1.0, 1.0], DijkstraMode::Out,
109/// ).unwrap();
110/// assert_eq!(r.paths[3].len(), 2);
111/// ```
112pub fn get_all_shortest_paths_dijkstra_with_mode(
113    graph: &Graph,
114    source: VertexId,
115    weights: &[f64],
116    mode: DijkstraMode,
117) -> IgraphResult<AllShortestPathsDijkstra> {
118    let n = graph.vcount();
119    if source >= n {
120        return Err(IgraphError::VertexOutOfRange { id: source, n });
121    }
122    validate_weights(graph, weights)?;
123
124    let n_us = n as usize;
125
126    // Distance from source to each vertex (-1.0 means not yet reached).
127    let mut dist = vec![-1.0_f64; n_us];
128    // Parents: for each vertex, list of (parent_vertex, edge_used).
129    let mut parents: Vec<Vec<(VertexId, EdgeId)>> = vec![Vec::new(); n_us];
130    // Order in which vertices are settled.
131    let mut order: Vec<VertexId> = Vec::with_capacity(n_us);
132
133    let mut heap: BinaryHeap<Frontier> = BinaryHeap::new();
134    dist[source as usize] = 0.0;
135    heap.push(Frontier(0.0, source));
136
137    let mut settled = vec![false; n_us];
138
139    while let Some(Frontier(d, v)) = heap.pop() {
140        let v_us = v as usize;
141        if settled[v_us] {
142            continue;
143        }
144        settled[v_us] = true;
145        order.push(v);
146
147        for eid in incident_for_mode(graph, v, mode)? {
148            let w = weights[eid as usize];
149            if !w.is_finite() {
150                continue;
151            }
152            let other = graph.edge_other(eid, v)?;
153            let other_us = other as usize;
154            if settled[other_us] {
155                continue;
156            }
157            let altdist = d + w;
158            let curdist = dist[other_us];
159
160            if curdist < 0.0 {
161                // First time reaching this vertex.
162                dist[other_us] = altdist;
163                parents[other_us].push((v, eid));
164                heap.push(Frontier(altdist, other));
165            } else if (altdist - curdist).abs() < EPSILON && w > 0.0 {
166                // Equal-cost alternative path (skip zero-weight edges to
167                // avoid infinite loops in undirected graphs).
168                parents[other_us].push((v, eid));
169            } else if altdist < curdist - EPSILON {
170                // Strictly shorter path found.
171                dist[other_us] = altdist;
172                parents[other_us].clear();
173                parents[other_us].push((v, eid));
174                heap.push(Frontier(altdist, other));
175            }
176        }
177    }
178
179    // Build nrgeo from parent tree in settlement order.
180    let mut nrgeo = vec![0_u64; n_us];
181    nrgeo[source as usize] = 1;
182    for &v in order.iter().skip(1) {
183        let v_us = v as usize;
184        for &(pv, _) in &parents[v_us] {
185            nrgeo[v_us] = nrgeo[v_us].saturating_add(nrgeo[pv as usize]);
186        }
187    }
188
189    // Reconstruct all paths for every vertex in settlement order.
190    let mut vertex_paths: Vec<Vec<Vec<VertexId>>> = vec![Vec::new(); n_us];
191    let mut edge_seqs: Vec<Vec<Vec<EdgeId>>> = vec![Vec::new(); n_us];
192
193    vertex_paths[source as usize] = vec![vec![source]];
194    edge_seqs[source as usize] = vec![vec![]];
195
196    for &v in order.iter().skip(1) {
197        let v_us = v as usize;
198        let parent_list: Vec<(VertexId, EdgeId)> = parents[v_us].clone();
199
200        for &(pv, pe) in &parent_list {
201            let pv_us = pv as usize;
202            let prev_vertices: Vec<Vec<VertexId>> = vertex_paths[pv_us].clone();
203            let prev_edges: Vec<Vec<EdgeId>> = edge_seqs[pv_us].clone();
204
205            for mut vpath in prev_vertices {
206                vpath.push(v);
207                vertex_paths[v_us].push(vpath);
208            }
209            for mut epath in prev_edges {
210                epath.push(pe);
211                edge_seqs[v_us].push(epath);
212            }
213        }
214    }
215
216    Ok(AllShortestPathsDijkstra {
217        paths: vertex_paths,
218        edge_paths: edge_seqs,
219        nrgeo,
220    })
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    fn diamond() -> Graph {
228        let mut g = Graph::with_vertices(4);
229        g.add_edge(0, 1).expect("e01");
230        g.add_edge(0, 2).expect("e02");
231        g.add_edge(1, 3).expect("e13");
232        g.add_edge(2, 3).expect("e23");
233        g
234    }
235
236    #[test]
237    fn diamond_equal_weights() {
238        let g = diamond();
239        let w = vec![1.0; 4];
240        let r = get_all_shortest_paths_dijkstra(&g, 0, &w).expect("ok");
241
242        assert_eq!(r.paths[0], vec![vec![0]]);
243        assert_eq!(r.nrgeo[0], 1);
244        assert_eq!(r.paths[1].len(), 1);
245        assert_eq!(r.paths[2].len(), 1);
246        assert_eq!(r.paths[3].len(), 2);
247        assert_eq!(r.nrgeo[3], 2);
248
249        let mut sorted = r.paths[3].clone();
250        sorted.sort();
251        assert_eq!(sorted, vec![vec![0, 1, 3], vec![0, 2, 3]]);
252    }
253
254    #[test]
255    fn diamond_asymmetric_weights() {
256        let g = diamond();
257        // 0→1 cost 1, 0→2 cost 10, 1→3 cost 1, 2→3 cost 1
258        // Only one shortest path to 3: 0→1→3 cost 2.
259        let w = vec![1.0, 10.0, 1.0, 1.0];
260        let r = get_all_shortest_paths_dijkstra(&g, 0, &w).expect("ok");
261        assert_eq!(r.paths[3].len(), 1);
262        assert_eq!(r.paths[3][0], vec![0, 1, 3]);
263        assert_eq!(r.nrgeo[3], 1);
264    }
265
266    #[test]
267    fn single_vertex() {
268        let g = Graph::with_vertices(1);
269        let r = get_all_shortest_paths_dijkstra(&g, 0, &[]).expect("ok");
270        assert_eq!(r.paths[0], vec![vec![0]]);
271        assert_eq!(r.nrgeo[0], 1);
272    }
273
274    #[test]
275    fn disconnected_graph() {
276        let mut g = Graph::with_vertices(3);
277        g.add_edge(0, 1).expect("e");
278        // Vertex 2 unreachable.
279        let r = get_all_shortest_paths_dijkstra(&g, 0, &[1.0]).expect("ok");
280        assert!(r.paths[2].is_empty());
281        assert_eq!(r.nrgeo[2], 0);
282    }
283
284    #[test]
285    fn directed_out_mode() {
286        let mut g = Graph::new(3, true).expect("g");
287        g.add_edge(0, 1).expect("e01");
288        g.add_edge(1, 2).expect("e12");
289        g.add_edge(2, 0).expect("e20");
290        let w = vec![1.0; 3];
291        let r =
292            get_all_shortest_paths_dijkstra_with_mode(&g, 0, &w, DijkstraMode::Out).expect("ok");
293        assert_eq!(r.paths[2].len(), 1);
294        assert_eq!(r.paths[2][0], vec![0, 1, 2]);
295    }
296
297    #[test]
298    fn directed_in_mode() {
299        let mut g = Graph::new(3, true).expect("g");
300        g.add_edge(0, 1).expect("e01");
301        g.add_edge(1, 2).expect("e12");
302        g.add_edge(2, 0).expect("e20");
303        let w = vec![1.0; 3];
304        let r = get_all_shortest_paths_dijkstra_with_mode(&g, 2, &w, DijkstraMode::In).expect("ok");
305        assert_eq!(r.paths[0].len(), 1);
306        assert_eq!(r.paths[0][0], vec![2, 1, 0]);
307    }
308
309    #[test]
310    fn edge_paths_match_vertex_paths() {
311        let g = diamond();
312        let w = vec![1.0; 4];
313        let r = get_all_shortest_paths_dijkstra(&g, 0, &w).expect("ok");
314
315        for v in 0..4u32 {
316            let v_us = v as usize;
317            assert_eq!(r.paths[v_us].len(), r.edge_paths[v_us].len());
318            for (vp, ep) in r.paths[v_us].iter().zip(r.edge_paths[v_us].iter()) {
319                if vp.len() > 1 {
320                    assert_eq!(ep.len(), vp.len() - 1);
321                } else {
322                    assert!(ep.is_empty());
323                }
324            }
325        }
326    }
327
328    #[test]
329    fn negative_weight_error() {
330        let mut g = Graph::with_vertices(2);
331        g.add_edge(0, 1).expect("e");
332        let r = get_all_shortest_paths_dijkstra(&g, 0, &[-1.0]);
333        assert!(r.is_err());
334    }
335
336    #[test]
337    fn nan_weight_error() {
338        let mut g = Graph::with_vertices(2);
339        g.add_edge(0, 1).expect("e");
340        let r = get_all_shortest_paths_dijkstra(&g, 0, &[f64::NAN]);
341        assert!(r.is_err());
342    }
343
344    #[test]
345    fn source_out_of_range_error() {
346        let g = Graph::with_vertices(3);
347        let r = get_all_shortest_paths_dijkstra(&g, 5, &[]);
348        assert!(r.is_err());
349    }
350
351    #[test]
352    fn ring_multiple_shortest_paths() {
353        // Ring: 0-1-2-3-4-0, all weight 1.
354        // From 0 to 2: two paths of length 2: 0→1→2 and 0→4→3→... wait no.
355        // Ring 0-1-2-3-4-0: from 0 to 2, either 0→1→2 (cost 2) or
356        // 0→4→3→2 (cost 3). Only one shortest path. But from 0 to vertex
357        // on the opposite side of even ring: let's use ring of 6.
358        let mut g = Graph::with_vertices(6);
359        for i in 0..6u32 {
360            g.add_edge(i, (i + 1) % 6).expect("edge");
361        }
362        let w = vec![1.0; 6];
363        // From 0 to 3: 0→1→2→3 (cost 3) and 0→5→4→3 (cost 3).
364        let r = get_all_shortest_paths_dijkstra(&g, 0, &w).expect("ok");
365        assert_eq!(r.paths[3].len(), 2);
366        assert_eq!(r.nrgeo[3], 2);
367    }
368
369    #[test]
370    fn grid_multiple_paths() {
371        // 2x3 grid: 0-1-2 / 3-4-5, connections 0-3, 1-4, 2-5
372        let mut g = Graph::with_vertices(6);
373        g.add_edge(0, 1).expect("e"); // 0
374        g.add_edge(1, 2).expect("e"); // 1
375        g.add_edge(3, 4).expect("e"); // 2
376        g.add_edge(4, 5).expect("e"); // 3
377        g.add_edge(0, 3).expect("e"); // 4
378        g.add_edge(1, 4).expect("e"); // 5
379        g.add_edge(2, 5).expect("e"); // 6
380        let w = vec![1.0; 7];
381        // From 0 to 5: all shortest have length 3.
382        // 0→1→2→5, 0→1→4→5, 0→3→4→5 — 3 paths.
383        let r = get_all_shortest_paths_dijkstra(&g, 0, &w).expect("ok");
384        assert_eq!(r.paths[5].len(), 3);
385        assert_eq!(r.nrgeo[5], 3);
386    }
387
388    #[test]
389    fn zero_weight_does_not_create_duplicate_parents() {
390        // Zero-weight edges: ensure we don't add duplicate parents.
391        // 0→1 (w=0), 1→2 (w=1).
392        let mut g = Graph::with_vertices(3);
393        g.add_edge(0, 1).expect("e01");
394        g.add_edge(1, 2).expect("e12");
395        let w = vec![0.0, 1.0];
396        let r = get_all_shortest_paths_dijkstra(&g, 0, &w).expect("ok");
397        assert_eq!(r.paths[1].len(), 1);
398        assert_eq!(r.paths[2].len(), 1);
399    }
400}
401
402#[cfg(all(test, feature = "proptest-harness"))]
403#[allow(clippy::cast_possible_truncation)]
404mod proptests {
405    use super::*;
406    use crate::algorithms::paths::all_shortest_paths::{
407        AllShortestPathsMode, get_all_shortest_paths_with_mode,
408    };
409    use proptest::prelude::*;
410
411    fn arb_small_graph() -> impl Strategy<Value = (Graph, Vec<f64>)> {
412        (3..12u32)
413            .prop_flat_map(|n| {
414                let max_edges = n * (n - 1) / 2;
415                let n_edges = 0..max_edges.min(20);
416                (Just(n), n_edges)
417            })
418            .prop_flat_map(|(n, m)| {
419                let edges = proptest::collection::vec((0..n, 0..n), m as usize);
420                (Just(n), edges)
421            })
422            .prop_flat_map(|(n, edges)| {
423                let n_e = edges.len();
424                let weights = proptest::collection::vec(1.0..10.0_f64, n_e);
425                (Just(n), Just(edges), weights)
426            })
427            .prop_map(|(n, edges, weights)| {
428                let mut g = Graph::with_vertices(n);
429                let mut actual_weights = Vec::new();
430                for (i, &(u, v)) in edges.iter().enumerate() {
431                    if u != v && g.add_edge(u, v).is_ok() {
432                        actual_weights.push(weights[i]);
433                    }
434                }
435                (g, actual_weights)
436            })
437    }
438
439    proptest! {
440        #![proptest_config(ProptestConfig::with_cases(200))]
441
442        #[test]
443        fn nrgeo_matches_path_count((g, w) in arb_small_graph()) {
444            if w.len() != g.ecount() { return Ok(()); }
445            let r = get_all_shortest_paths_dijkstra(&g, 0, &w)?;
446            for v in 0..g.vcount() as usize {
447                prop_assert_eq!(
448                    r.nrgeo[v] as usize,
449                    r.paths[v].len(),
450                    "nrgeo mismatch at vertex {}",
451                    v
452                );
453            }
454        }
455
456        #[test]
457        fn unit_weights_match_unweighted((g, _) in arb_small_graph()) {
458            let unit_w = vec![1.0; g.ecount()];
459            let weighted = get_all_shortest_paths_dijkstra(&g, 0, &unit_w)?;
460            let unweighted = get_all_shortest_paths_with_mode(
461                &g, 0, AllShortestPathsMode::All,
462            )?;
463            for v in 0..g.vcount() as usize {
464                prop_assert_eq!(
465                    weighted.nrgeo[v],
466                    unweighted.nrgeo[v],
467                    "nrgeo mismatch at vertex {}",
468                    v
469                );
470            }
471        }
472
473        #[test]
474        fn paths_are_valid_walks((g, w) in arb_small_graph()) {
475            if w.len() != g.ecount() { return Ok(()); }
476            let r = get_all_shortest_paths_dijkstra(&g, 0, &w)?;
477            for v in 0..g.vcount() {
478                let v_us = v as usize;
479                for (pi, vpath) in r.paths[v_us].iter().enumerate() {
480                    prop_assert!(!vpath.is_empty());
481                    prop_assert_eq!(vpath[0], 0, "path {} to {} doesn't start at source", pi, v);
482                    prop_assert_eq!(*vpath.last().unwrap(), v, "path {} doesn't end at {}", pi, v);
483
484                    let epath = &r.edge_paths[v_us][pi];
485                    prop_assert_eq!(epath.len(), vpath.len().saturating_sub(1));
486
487                    for (i, &eid) in epath.iter().enumerate() {
488                        let (eu, ev) = g.edge(eid).unwrap();
489                        let pv = vpath[i];
490                        let pv_next = vpath[i + 1];
491                        let connects = (eu == pv && ev == pv_next)
492                            || (eu == pv_next && ev == pv);
493                        prop_assert!(
494                            connects,
495                            "edge {} ({},{}) doesn't connect path vertices {} and {}",
496                            eid, eu, ev, pv, pv_next
497                        );
498                    }
499                }
500            }
501        }
502    }
503}