Skip to main content

rust_igraph/algorithms/paths/
get_shortest_path.rs

1//! Single-pair shortest path (`ALGO-SP-035`).
2//!
3//! Counterpart of `igraph_get_shortest_path()` from
4//! `references/igraph/src/paths/unweighted.c:662-703`, the convenience
5//! wrapper over `igraph_get_shortest_paths()` for the special case of a
6//! single target. It returns one shortest path from `from` to `to` as
7//! both the vertex sequence (including both endpoints) and the edge
8//! sequence along it.
9//!
10//! Like upstream, the backend is selected by the `weights` argument
11//! (`igraph_get_shortest_paths`, `unweighted.c:404-426`):
12//! - `None` → unweighted BFS ([`igraph_i_get_shortest_paths_unweighted`]),
13//! - `Some` with all-non-negative weights → Dijkstra,
14//! - `Some` with at least one negative weight → Bellman-Ford.
15//!
16//! When several shortest paths exist an arbitrary one is returned; for
17//! the unweighted case the choice is determined by incident-edge order,
18//! matching upstream's BFS exactly (it records the edge each vertex was
19//! first reached through, scanning `igraph_incident` in edge-id order).
20
21use std::collections::VecDeque;
22
23use crate::core::graph::EdgeId;
24use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
25
26use super::bellman_ford::bellman_ford_path_to_with_mode;
27use super::dijkstra::{DijkstraMode, dijkstra_path_to_with_mode};
28
29/// Per-vertex mode-aware incident edges, mirroring upstream's
30/// `igraph_incident(_, _, v, mode, IGRAPH_LOOPS)`. For undirected graphs
31/// every mode collapses to [`Graph::incident`]'s merged adjacency.
32fn incident_for_mode(graph: &Graph, v: VertexId, mode: DijkstraMode) -> IgraphResult<Vec<EdgeId>> {
33    if !graph.is_directed() {
34        return graph.incident(v);
35    }
36    match mode {
37        DijkstraMode::Out => graph.incident(v),
38        DijkstraMode::In => graph.incident_in(v),
39        DijkstraMode::All => {
40            let mut out = graph.incident(v)?;
41            out.extend(graph.incident_in(v)?);
42            Ok(out)
43        }
44    }
45}
46
47/// A single shortest path: the vertex sequence (including `from` and
48/// `to`) and the edge sequence along it. For a path of `k` vertices the
49/// edge vector has `k - 1` entries.
50///
51/// Both vectors are empty when `to` is unreachable from `from`,
52/// matching upstream (which leaves the output vectors cleared). When
53/// `from == to` the vertex vector is `[from]` and the edge vector is
54/// empty.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ShortestPath {
57    /// Vertex IDs along the path, source first and target last.
58    pub vertices: Vec<VertexId>,
59    /// Edge IDs along the path; `vertices.len().saturating_sub(1)` long.
60    pub edges: Vec<EdgeId>,
61}
62
63/// Faithful single-target port of
64/// `igraph_i_get_shortest_paths_unweighted` (`unweighted.c:428-619`).
65///
66/// `parent_eids` follows the upstream encoding:
67/// - `0` — vertex is not the target and not yet reached,
68/// - `-1` — vertex is the target and not yet reached,
69/// - `1` — the start vertex,
70/// - `e + 2` — reached via edge `e`.
71fn bfs_single_path(
72    graph: &Graph,
73    from: VertexId,
74    to: VertexId,
75    mode: DijkstraMode,
76) -> IgraphResult<ShortestPath> {
77    let n = graph.vcount() as usize;
78    let mut parent_eids: Vec<i64> = vec![0; n];
79    parent_eids[to as usize] = -1;
80
81    let to_reach = 1_i64;
82    let mut reached = 0_i64;
83
84    let mut q: VecDeque<VertexId> = VecDeque::new();
85    q.push_back(from);
86    if parent_eids[from as usize] < 0 {
87        reached += 1; // from == to
88    }
89    parent_eids[from as usize] = 1;
90
91    while reached < to_reach {
92        let Some(act) = q.pop_front() else { break };
93        for edge in incident_for_mode(graph, act, mode)? {
94            let neighbor = graph.edge_other(edge, act)?;
95            let pe = parent_eids[neighbor as usize];
96            // pe > 0 means already reached (or the start) — skip it.
97            if pe <= 0 {
98                if pe < 0 {
99                    reached += 1; // a wanted target reached for the first time
100                }
101                // edge + 2 cannot overflow i64: edge fits in u32.
102                parent_eids[neighbor as usize] = i64::from(edge) + 2;
103                q.push_back(neighbor);
104            }
105        }
106    }
107
108    // Reconstruct the single target path, walking parent edges back to
109    // the start, then filling the output vectors front-to-back.
110    if parent_eids[to as usize] <= 0 {
111        // `to` was never reached.
112        return Ok(ShortestPath {
113            vertices: Vec::new(),
114            edges: Vec::new(),
115        });
116    }
117
118    let mut size = 0_usize;
119    let mut act = to;
120    while parent_eids[act as usize] > 1 {
121        size += 1;
122        let edge = decode_edge(parent_eids[act as usize])?;
123        act = graph.edge_other(edge, act)?;
124    }
125
126    let mut vertices = vec![0 as VertexId; size + 1];
127    let mut edges = vec![0 as EdgeId; size];
128    vertices[size] = to;
129
130    let mut idx = size;
131    let mut act = to;
132    while parent_eids[act as usize] > 1 {
133        idx -= 1;
134        let edge = decode_edge(parent_eids[act as usize])?;
135        act = graph.edge_other(edge, act)?;
136        vertices[idx] = act;
137        edges[idx] = edge;
138    }
139
140    Ok(ShortestPath { vertices, edges })
141}
142
143/// Recover the edge id from a `parent_eids` entry (`e + 2`).
144fn decode_edge(parent_eid: i64) -> IgraphResult<EdgeId> {
145    EdgeId::try_from(parent_eid - 2)
146        .map_err(|_| IgraphError::Internal("get_shortest_path: edge id overflow"))
147}
148
149/// Calculate a single shortest path from `from` to `to`.
150///
151/// Returns the path as both its vertex sequence (source first, target
152/// last) and the edge sequence along it. If several shortest paths
153/// exist, an arbitrary one is returned.
154///
155/// The backend mirrors `igraph_get_shortest_paths`:
156/// - `weights == None`: unweighted breadth-first search,
157/// - `weights == Some(w)` with all `w[e] >= 0`: Dijkstra,
158/// - `weights == Some(w)` with any `w[e] < 0`: Bellman-Ford.
159///
160/// On directed graphs `mode` selects which adjacency is followed
161/// ([`DijkstraMode::Out`] / [`DijkstraMode::In`] / [`DijkstraMode::All`]);
162/// it is ignored on undirected graphs.
163///
164/// # Errors
165///
166/// Returns [`IgraphError::VertexOutOfRange`] if `from` or `to` is not a
167/// valid vertex, or [`IgraphError::InvalidArgument`] if `weights` is
168/// provided and its length differs from the edge count or contains NaN.
169///
170/// # Examples
171///
172/// ```
173/// use rust_igraph::{Graph, get_shortest_path, DijkstraMode};
174///
175/// let mut g = Graph::new(4, false).unwrap();
176/// g.add_edge(0, 1).unwrap(); // edge 0
177/// g.add_edge(1, 2).unwrap(); // edge 1
178/// g.add_edge(2, 3).unwrap(); // edge 2
179/// let p = get_shortest_path(&g, 0, 3, None, DijkstraMode::Out).unwrap();
180/// assert_eq!(p.vertices, vec![0, 1, 2, 3]);
181/// assert_eq!(p.edges, vec![0, 1, 2]);
182///
183/// // Unreachable target → empty path.
184/// let mut h = Graph::new(2, true).unwrap();
185/// h.add_edge(0, 1).unwrap();
186/// let q = get_shortest_path(&h, 1, 0, None, DijkstraMode::Out).unwrap();
187/// assert!(q.vertices.is_empty());
188/// assert!(q.edges.is_empty());
189/// ```
190pub fn get_shortest_path(
191    graph: &Graph,
192    from: VertexId,
193    to: VertexId,
194    weights: Option<&[f64]>,
195    mode: DijkstraMode,
196) -> IgraphResult<ShortestPath> {
197    let n = graph.vcount();
198    if from >= n {
199        return Err(IgraphError::VertexOutOfRange { id: from, n });
200    }
201    if to >= n {
202        return Err(IgraphError::VertexOutOfRange { id: to, n });
203    }
204
205    let Some(w) = weights else {
206        return bfs_single_path(graph, from, to, mode);
207    };
208
209    let m = graph.ecount();
210    if w.len() != m {
211        return Err(IgraphError::InvalidArgument(format!(
212            "get_shortest_path: weights length {} != edge count {m}",
213            w.len()
214        )));
215    }
216    let mut has_negative = false;
217    for (e, &x) in w.iter().enumerate() {
218        if x.is_nan() {
219            return Err(IgraphError::InvalidArgument(format!(
220                "get_shortest_path: weight at edge {e} is NaN"
221            )));
222        }
223        if x < 0.0 {
224            has_negative = true;
225        }
226    }
227
228    let result = if has_negative {
229        bellman_ford_path_to_with_mode(graph, from, to, w, mode)?
230    } else {
231        dijkstra_path_to_with_mode(graph, from, to, w, mode)?
232    };
233
234    Ok(match result {
235        Some((vertices, edges)) => ShortestPath { vertices, edges },
236        None => ShortestPath {
237            vertices: Vec::new(),
238            edges: Vec::new(),
239        },
240    })
241}
242
243#[cfg(test)]
244#[allow(clippy::float_cmp)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn from_equals_to_unweighted() {
250        let mut g = Graph::new(3, false).unwrap();
251        g.add_edge(0, 1).unwrap();
252        let p = get_shortest_path(&g, 1, 1, None, DijkstraMode::Out).unwrap();
253        assert_eq!(p.vertices, vec![1]);
254        assert!(p.edges.is_empty());
255    }
256
257    #[test]
258    fn simple_path_unweighted_undirected() {
259        let mut g = Graph::new(4, false).unwrap();
260        g.add_edge(0, 1).unwrap(); // 0
261        g.add_edge(1, 2).unwrap(); // 1
262        g.add_edge(2, 3).unwrap(); // 2
263        let p = get_shortest_path(&g, 0, 3, None, DijkstraMode::Out).unwrap();
264        assert_eq!(p.vertices, vec![0, 1, 2, 3]);
265        assert_eq!(p.edges, vec![0, 1, 2]);
266    }
267
268    #[test]
269    fn picks_shorter_of_two_routes() {
270        // 0-1-3 (len 2) vs 0-2-... ; direct shortcut 0-3.
271        let mut g = Graph::new(4, false).unwrap();
272        g.add_edge(0, 1).unwrap(); // 0
273        g.add_edge(1, 3).unwrap(); // 1
274        g.add_edge(0, 3).unwrap(); // 2  (shortcut)
275        let p = get_shortest_path(&g, 0, 3, None, DijkstraMode::Out).unwrap();
276        assert_eq!(p.vertices, vec![0, 3]);
277        assert_eq!(p.edges, vec![2]);
278    }
279
280    #[test]
281    fn unreachable_returns_empty() {
282        let mut g = Graph::new(3, true).unwrap();
283        g.add_edge(0, 1).unwrap();
284        let p = get_shortest_path(&g, 0, 2, None, DijkstraMode::Out).unwrap();
285        assert!(p.vertices.is_empty());
286        assert!(p.edges.is_empty());
287    }
288
289    #[test]
290    fn directed_mode_in_follows_reverse() {
291        let mut g = Graph::new(3, true).unwrap();
292        g.add_edge(0, 1).unwrap(); // 0
293        g.add_edge(1, 2).unwrap(); // 1
294        // OUT: 0 -> 2 works.
295        let out = get_shortest_path(&g, 0, 2, None, DijkstraMode::Out).unwrap();
296        assert_eq!(out.vertices, vec![0, 1, 2]);
297        // OUT: 2 -> 0 impossible.
298        let none = get_shortest_path(&g, 2, 0, None, DijkstraMode::Out).unwrap();
299        assert!(none.vertices.is_empty());
300        // IN: 2 -> 0 follows reversed edges.
301        let inp = get_shortest_path(&g, 2, 0, None, DijkstraMode::In).unwrap();
302        assert_eq!(inp.vertices, vec![2, 1, 0]);
303        assert_eq!(inp.edges, vec![1, 0]);
304    }
305
306    #[test]
307    fn directed_mode_all_ignores_direction() {
308        let mut g = Graph::new(3, true).unwrap();
309        g.add_edge(0, 1).unwrap();
310        g.add_edge(1, 2).unwrap();
311        let p = get_shortest_path(&g, 2, 0, None, DijkstraMode::All).unwrap();
312        assert_eq!(p.vertices, vec![2, 1, 0]);
313    }
314
315    #[test]
316    fn weighted_dijkstra_prefers_cheaper_route() {
317        let mut g = Graph::new(4, false).unwrap();
318        g.add_edge(0, 1).unwrap(); // 0
319        g.add_edge(1, 3).unwrap(); // 1
320        g.add_edge(0, 3).unwrap(); // 2 direct but expensive
321        let w = vec![1.0, 1.0, 10.0];
322        let p = get_shortest_path(&g, 0, 3, Some(&w), DijkstraMode::Out).unwrap();
323        assert_eq!(p.vertices, vec![0, 1, 3]);
324        assert_eq!(p.edges, vec![0, 1]);
325    }
326
327    #[test]
328    fn weighted_negative_uses_bellman_ford() {
329        let mut g = Graph::new(3, true).unwrap();
330        g.add_edge(0, 1).unwrap(); // 0
331        g.add_edge(1, 2).unwrap(); // 1
332        g.add_edge(0, 2).unwrap(); // 2 direct
333        // Direct edge cheaper only thanks to a negative leg on 0->1->2.
334        let w = vec![1.0, -5.0, 1.0];
335        let p = get_shortest_path(&g, 0, 2, Some(&w), DijkstraMode::Out).unwrap();
336        assert_eq!(p.vertices, vec![0, 1, 2]);
337        assert_eq!(p.edges, vec![0, 1]);
338    }
339
340    #[test]
341    fn weighted_unreachable_returns_empty() {
342        let mut g = Graph::new(3, true).unwrap();
343        g.add_edge(0, 1).unwrap();
344        let w = vec![1.0];
345        let p = get_shortest_path(&g, 0, 2, Some(&w), DijkstraMode::Out).unwrap();
346        assert!(p.vertices.is_empty());
347        assert!(p.edges.is_empty());
348    }
349
350    #[test]
351    fn invalid_source_errors() {
352        let g = Graph::new(2, false).unwrap();
353        assert!(get_shortest_path(&g, 5, 0, None, DijkstraMode::Out).is_err());
354        assert!(get_shortest_path(&g, 0, 5, None, DijkstraMode::Out).is_err());
355    }
356
357    #[test]
358    fn weights_length_mismatch_errors() {
359        let mut g = Graph::new(2, false).unwrap();
360        g.add_edge(0, 1).unwrap();
361        let w = vec![1.0, 2.0];
362        assert!(get_shortest_path(&g, 0, 1, Some(&w), DijkstraMode::Out).is_err());
363    }
364
365    #[test]
366    fn weights_nan_errors() {
367        let mut g = Graph::new(2, false).unwrap();
368        g.add_edge(0, 1).unwrap();
369        let w = vec![f64::NAN];
370        assert!(get_shortest_path(&g, 0, 1, Some(&w), DijkstraMode::Out).is_err());
371    }
372}
373
374#[cfg(all(test, feature = "proptest-harness"))]
375mod proptests {
376    use super::*;
377    use crate::create;
378    use proptest::prelude::*;
379
380    fn arb_graph(max_v: u32) -> impl Strategy<Value = Graph> {
381        (2..=max_v).prop_flat_map(|n| {
382            let max_e = (n as usize)
383                .saturating_mul(n.saturating_sub(1) as usize)
384                .min(20);
385            proptest::collection::vec((0..n, 0..n), 0..=max_e).prop_map(move |edges| {
386                let edge_tuples: Vec<(u32, u32)> = edges.into_iter().collect();
387                create(&edge_tuples, n, false).expect("arb graph")
388            })
389        })
390    }
391
392    proptest! {
393        /// A non-empty result is a genuine simple walk from `from` to
394        /// `to`: endpoints match, each edge joins consecutive vertices,
395        /// and no vertex repeats (shortest paths are loopless).
396        #[test]
397        fn path_is_a_valid_simple_walk(
398            g in arb_graph(6),
399            from in 0u32..6,
400            to in 0u32..6,
401        ) {
402            let n = g.vcount();
403            prop_assume!(from < n && to < n);
404            let p = get_shortest_path(&g, from, to, None, DijkstraMode::All).expect("ok");
405            if p.vertices.is_empty() {
406                return Ok(());
407            }
408            prop_assert_eq!(p.vertices[0], from);
409            prop_assert_eq!(*p.vertices.last().expect("non-empty"), to);
410            prop_assert_eq!(p.edges.len() + 1, p.vertices.len());
411
412            // No repeated vertices.
413            let mut seen = vec![false; n as usize];
414            for &v in &p.vertices {
415                prop_assert!(!seen[v as usize], "vertex {} repeats", v);
416                seen[v as usize] = true;
417            }
418
419            // Each edge joins the two consecutive vertices (undirected).
420            for (i, &e) in p.edges.iter().enumerate() {
421                let (a, b) = g.edge(e).expect("edge id valid");
422                let (u, v) = (p.vertices[i], p.vertices[i + 1]);
423                prop_assert!(
424                    (a == u && b == v) || (a == v && b == u),
425                    "edge {} = ({},{}) does not join {} and {}",
426                    e, a, b, u, v
427                );
428            }
429        }
430
431        /// On an unweighted graph the BFS backend and the Dijkstra
432        /// backend (all weights 1.0) must agree on path length.
433        #[test]
434        fn bfs_and_unit_dijkstra_agree_on_length(
435            g in arb_graph(6),
436            from in 0u32..6,
437            to in 0u32..6,
438        ) {
439            let n = g.vcount();
440            prop_assume!(from < n && to < n);
441            let unweighted = get_shortest_path(&g, from, to, None, DijkstraMode::All)
442                .expect("ok");
443            let ones = vec![1.0_f64; g.ecount()];
444            let weighted = get_shortest_path(&g, from, to, Some(&ones), DijkstraMode::All)
445                .expect("ok");
446            prop_assert_eq!(unweighted.vertices.is_empty(), weighted.vertices.is_empty());
447            prop_assert_eq!(unweighted.edges.len(), weighted.edges.len());
448        }
449    }
450}