Skip to main content

rust_igraph/algorithms/paths/
shortest_paths.rs

1//! Unweighted shortest paths (ALGO-SP-057).
2//!
3//! Counterpart of `igraph_get_shortest_paths()` from
4//! `references/igraph/src/paths/unweighted.c`.
5//!
6//! Returns one shortest path (as a vertex sequence) from a source
7//! vertex to every reachable target in the graph, using BFS.
8
9use std::collections::VecDeque;
10
11use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
12
13/// Returns one shortest path from `source` to every vertex in the graph.
14///
15/// Uses BFS (unweighted edges). The result is a `Vec` of length
16/// `vcount()`, where `result[v]` is the vertex sequence of a shortest
17/// path from `source` to `v` (inclusive at both ends). If `v` is
18/// unreachable from `source`, `result[v]` is empty.
19///
20/// For directed graphs, follows edges in the outgoing direction by
21/// default. Use [`get_shortest_paths_with_mode`] to control direction.
22///
23/// # Errors
24///
25/// - `InvalidArgument` if `source >= vcount()`.
26///
27/// # Examples
28///
29/// ```
30/// use rust_igraph::{Graph, get_shortest_paths};
31///
32/// // Path 0-1-2-3
33/// let mut g = Graph::with_vertices(4);
34/// g.add_edge(0, 1).unwrap();
35/// g.add_edge(1, 2).unwrap();
36/// g.add_edge(2, 3).unwrap();
37/// let paths = get_shortest_paths(&g, 0).unwrap();
38/// assert_eq!(paths[0], vec![0]);
39/// assert_eq!(paths[1], vec![0, 1]);
40/// assert_eq!(paths[2], vec![0, 1, 2]);
41/// assert_eq!(paths[3], vec![0, 1, 2, 3]);
42/// ```
43pub fn get_shortest_paths(graph: &Graph, source: VertexId) -> IgraphResult<Vec<Vec<VertexId>>> {
44    if graph.is_directed() {
45        return get_shortest_paths_directed(graph, source, true);
46    }
47    get_shortest_paths_impl(graph, source)
48}
49
50/// Direction mode for [`get_shortest_paths_with_mode`].
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ShortestPathMode {
53    /// Follow outgoing edges (directed graphs).
54    Out,
55    /// Follow incoming edges (directed graphs).
56    In,
57    /// Ignore edge direction.
58    All,
59}
60
61/// Returns one shortest path from `source` to every vertex, with
62/// direction control for directed graphs.
63///
64/// For undirected graphs, `mode` is ignored and all edges are
65/// traversed bidirectionally.
66///
67/// # Errors
68///
69/// - `InvalidArgument` if `source >= vcount()`.
70///
71/// # Examples
72///
73/// ```
74/// use rust_igraph::{Graph, get_shortest_paths_with_mode, ShortestPathMode};
75///
76/// // Directed: 0→1→2
77/// let mut g = Graph::new(3, true).unwrap();
78/// g.add_edge(0, 1).unwrap();
79/// g.add_edge(1, 2).unwrap();
80/// // Forward
81/// let fwd = get_shortest_paths_with_mode(&g, 0, ShortestPathMode::Out).unwrap();
82/// assert_eq!(fwd[2], vec![0, 1, 2]);
83/// // Backward from 2
84/// let bwd = get_shortest_paths_with_mode(&g, 2, ShortestPathMode::In).unwrap();
85/// assert_eq!(bwd[0], vec![2, 1, 0]);
86/// ```
87pub fn get_shortest_paths_with_mode(
88    graph: &Graph,
89    source: VertexId,
90    mode: ShortestPathMode,
91) -> IgraphResult<Vec<Vec<VertexId>>> {
92    if !graph.is_directed() {
93        return get_shortest_paths_impl(graph, source);
94    }
95    match mode {
96        ShortestPathMode::Out => get_shortest_paths_directed(graph, source, true),
97        ShortestPathMode::In => get_shortest_paths_directed(graph, source, false),
98        ShortestPathMode::All => get_shortest_paths_directed_all(graph, source),
99    }
100}
101
102/// BFS implementation for undirected graphs (uses `graph.neighbors`).
103fn get_shortest_paths_impl(graph: &Graph, source: VertexId) -> IgraphResult<Vec<Vec<VertexId>>> {
104    let n = graph.vcount();
105    if source >= n {
106        return Err(IgraphError::InvalidArgument(format!(
107            "get_shortest_paths: source {source} out of range (vcount={n})"
108        )));
109    }
110
111    let n_us = n as usize;
112    let mut parent: Vec<Option<VertexId>> = vec![None; n_us];
113    let mut visited = vec![false; n_us];
114
115    let mut queue = VecDeque::new();
116    visited[source as usize] = true;
117    queue.push_back(source);
118
119    while let Some(cur) = queue.pop_front() {
120        let neighbors = graph.neighbors(cur)?;
121        for &nb in &neighbors {
122            if !visited[nb as usize] {
123                visited[nb as usize] = true;
124                parent[nb as usize] = Some(cur);
125                queue.push_back(nb);
126            }
127        }
128    }
129
130    Ok(build_paths(source, n_us, &parent, &visited))
131}
132
133/// BFS for directed graphs with explicit direction control.
134fn get_shortest_paths_directed(
135    graph: &Graph,
136    source: VertexId,
137    follow_out: bool,
138) -> IgraphResult<Vec<Vec<VertexId>>> {
139    let n = graph.vcount();
140    if source >= n {
141        return Err(IgraphError::InvalidArgument(format!(
142            "get_shortest_paths: source {source} out of range (vcount={n})"
143        )));
144    }
145
146    let n_us = n as usize;
147    let m =
148        u32::try_from(graph.ecount()).map_err(|_| IgraphError::Internal("ecount overflows u32"))?;
149
150    // Build adjacency lists in the desired direction.
151    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n_us];
152    for eid in 0..m {
153        let (from, to) = graph.edge(eid)?;
154        if follow_out {
155            adj[from as usize].push(to);
156        } else {
157            adj[to as usize].push(from);
158        }
159    }
160
161    let mut parent: Vec<Option<VertexId>> = vec![None; n_us];
162    let mut visited = vec![false; n_us];
163
164    let mut queue = VecDeque::new();
165    visited[source as usize] = true;
166    queue.push_back(source);
167
168    while let Some(cur) = queue.pop_front() {
169        for &nb in &adj[cur as usize] {
170            if !visited[nb as usize] {
171                visited[nb as usize] = true;
172                parent[nb as usize] = Some(cur);
173                queue.push_back(nb);
174            }
175        }
176    }
177
178    Ok(build_paths(source, n_us, &parent, &visited))
179}
180
181/// BFS for directed graphs with `All` mode (ignore edge direction).
182fn get_shortest_paths_directed_all(
183    graph: &Graph,
184    source: VertexId,
185) -> IgraphResult<Vec<Vec<VertexId>>> {
186    let n = graph.vcount();
187    if source >= n {
188        return Err(IgraphError::InvalidArgument(format!(
189            "get_shortest_paths: source {source} out of range (vcount={n})"
190        )));
191    }
192
193    let n_us = n as usize;
194    let m =
195        u32::try_from(graph.ecount()).map_err(|_| IgraphError::Internal("ecount overflows u32"))?;
196
197    // Build symmetric adjacency lists (both directions).
198    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n_us];
199    for eid in 0..m {
200        let (from, to) = graph.edge(eid)?;
201        adj[from as usize].push(to);
202        if from != to {
203            adj[to as usize].push(from);
204        }
205    }
206
207    let mut parent: Vec<Option<VertexId>> = vec![None; n_us];
208    let mut visited = vec![false; n_us];
209
210    let mut queue = VecDeque::new();
211    visited[source as usize] = true;
212    queue.push_back(source);
213
214    while let Some(cur) = queue.pop_front() {
215        for &nb in &adj[cur as usize] {
216            if !visited[nb as usize] {
217                visited[nb as usize] = true;
218                parent[nb as usize] = Some(cur);
219                queue.push_back(nb);
220            }
221        }
222    }
223
224    Ok(build_paths(source, n_us, &parent, &visited))
225}
226
227/// Reconstruct paths from the parent array.
228fn build_paths(
229    source: VertexId,
230    n_us: usize,
231    parent: &[Option<VertexId>],
232    visited: &[bool],
233) -> Vec<Vec<VertexId>> {
234    let mut result: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
235    for (v, &is_visited) in visited.iter().enumerate().take(n_us) {
236        if !is_visited {
237            result.push(Vec::new());
238            continue;
239        }
240        #[allow(clippy::cast_possible_truncation)] // v < n which is u32
241        let v_id = v as u32;
242        if v_id == source {
243            result.push(vec![source]);
244            continue;
245        }
246        let mut path = Vec::new();
247        let mut cur = v_id;
248        loop {
249            path.push(cur);
250            if cur == source {
251                break;
252            }
253            cur = parent[cur as usize].unwrap_or(source);
254        }
255        path.reverse();
256        result.push(path);
257    }
258    result
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn empty_graph() {
267        let g = Graph::with_vertices(0);
268        // source out of range for empty graph — but 0 >= 0, err
269        // Actually vcount=0, any source is invalid
270        assert!(get_shortest_paths(&g, 0).is_err());
271    }
272
273    #[test]
274    fn singleton() {
275        let g = Graph::with_vertices(1);
276        let paths = get_shortest_paths(&g, 0).unwrap();
277        assert_eq!(paths, vec![vec![0]]);
278    }
279
280    #[test]
281    fn path_graph() {
282        let mut g = Graph::with_vertices(4);
283        g.add_edge(0, 1).unwrap();
284        g.add_edge(1, 2).unwrap();
285        g.add_edge(2, 3).unwrap();
286        let paths = get_shortest_paths(&g, 0).unwrap();
287        assert_eq!(paths[0], vec![0]);
288        assert_eq!(paths[1], vec![0, 1]);
289        assert_eq!(paths[2], vec![0, 1, 2]);
290        assert_eq!(paths[3], vec![0, 1, 2, 3]);
291    }
292
293    #[test]
294    fn two_components_unreachable() {
295        let mut g = Graph::with_vertices(4);
296        g.add_edge(0, 1).unwrap();
297        g.add_edge(2, 3).unwrap();
298        let paths = get_shortest_paths(&g, 0).unwrap();
299        assert_eq!(paths[0], vec![0]);
300        assert_eq!(paths[1], vec![0, 1]);
301        assert!(paths[2].is_empty());
302        assert!(paths[3].is_empty());
303    }
304
305    #[test]
306    fn shortest_path_prefers_shorter() {
307        // Graph: 0-1-2-3 and 0-3 (direct edge).
308        // Shortest path from 0 to 3 should be [0, 3].
309        let mut g = Graph::with_vertices(4);
310        g.add_edge(0, 1).unwrap();
311        g.add_edge(1, 2).unwrap();
312        g.add_edge(2, 3).unwrap();
313        g.add_edge(0, 3).unwrap();
314        let paths = get_shortest_paths(&g, 0).unwrap();
315        assert_eq!(paths[3].len(), 2); // direct path length 2 (vertices)
316        assert_eq!(paths[3][0], 0);
317        assert_eq!(paths[3][1], 3);
318    }
319
320    #[test]
321    fn cycle_graph() {
322        // 5-cycle: 0-1-2-3-4-0. From 0, path to 3 can be 0-1-2-3 or 0-4-3.
323        // Both length 3. BFS finds one of them.
324        let mut g = Graph::with_vertices(5);
325        g.add_edge(0, 1).unwrap();
326        g.add_edge(1, 2).unwrap();
327        g.add_edge(2, 3).unwrap();
328        g.add_edge(3, 4).unwrap();
329        g.add_edge(4, 0).unwrap();
330        let paths = get_shortest_paths(&g, 0).unwrap();
331        // Path to 3 should have length 3 (3 vertices).
332        assert_eq!(paths[3].len(), 3);
333        assert_eq!(paths[3][0], 0);
334        assert_eq!(*paths[3].last().unwrap(), 3);
335    }
336
337    #[test]
338    fn directed_out() {
339        // 0→1→2, no path from 0 to anywhere except forward.
340        let mut g = Graph::new(3, true).unwrap();
341        g.add_edge(0, 1).unwrap();
342        g.add_edge(1, 2).unwrap();
343        let paths = get_shortest_paths_with_mode(&g, 0, ShortestPathMode::Out).unwrap();
344        assert_eq!(paths[0], vec![0]);
345        assert_eq!(paths[1], vec![0, 1]);
346        assert_eq!(paths[2], vec![0, 1, 2]);
347    }
348
349    #[test]
350    fn directed_in() {
351        // 0→1→2. From vertex 2, following incoming edges.
352        let mut g = Graph::new(3, true).unwrap();
353        g.add_edge(0, 1).unwrap();
354        g.add_edge(1, 2).unwrap();
355        let paths = get_shortest_paths_with_mode(&g, 2, ShortestPathMode::In).unwrap();
356        assert_eq!(paths[2], vec![2]);
357        assert_eq!(paths[1], vec![2, 1]);
358        assert_eq!(paths[0], vec![2, 1, 0]);
359    }
360
361    #[test]
362    fn directed_unreachable() {
363        // 0→1→2. From 2, out mode: nothing reachable.
364        let mut g = Graph::new(3, true).unwrap();
365        g.add_edge(0, 1).unwrap();
366        g.add_edge(1, 2).unwrap();
367        let paths = get_shortest_paths_with_mode(&g, 2, ShortestPathMode::Out).unwrap();
368        assert_eq!(paths[2], vec![2]);
369        assert!(paths[0].is_empty());
370        assert!(paths[1].is_empty());
371    }
372
373    #[test]
374    fn directed_all_mode() {
375        // 0→1→2. All mode: treat as undirected.
376        let mut g = Graph::new(3, true).unwrap();
377        g.add_edge(0, 1).unwrap();
378        g.add_edge(1, 2).unwrap();
379        let paths = get_shortest_paths_with_mode(&g, 2, ShortestPathMode::All).unwrap();
380        assert_eq!(paths[2], vec![2]);
381        assert_eq!(paths[1], vec![2, 1]);
382        assert_eq!(paths[0], vec![2, 1, 0]);
383    }
384
385    #[test]
386    fn star_graph() {
387        // Star: center 0, leaves 1,2,3,4.
388        let mut g = Graph::with_vertices(5);
389        g.add_edge(0, 1).unwrap();
390        g.add_edge(0, 2).unwrap();
391        g.add_edge(0, 3).unwrap();
392        g.add_edge(0, 4).unwrap();
393        let paths = get_shortest_paths(&g, 0).unwrap();
394        for leaf in 1..5 {
395            assert_eq!(paths[leaf as usize], vec![0, leaf]);
396        }
397        // From leaf 1 to leaf 2: [1, 0, 2].
398        let paths_1 = get_shortest_paths(&g, 1).unwrap();
399        assert_eq!(paths_1[2], vec![1, 0, 2]);
400    }
401
402    #[test]
403    fn source_out_of_range() {
404        let g = Graph::with_vertices(3);
405        assert!(get_shortest_paths(&g, 5).is_err());
406    }
407
408    #[test]
409    fn self_loop_ignored() {
410        // Vertex 1 has a self-loop. Should not affect path finding.
411        let mut g = Graph::with_vertices(3);
412        g.add_edge(0, 1).unwrap();
413        g.add_edge(1, 1).unwrap();
414        g.add_edge(1, 2).unwrap();
415        let paths = get_shortest_paths(&g, 0).unwrap();
416        assert_eq!(paths[2], vec![0, 1, 2]);
417    }
418
419    #[test]
420    fn oracle_6_vertices() {
421        // Verified against python-igraph.
422        let mut g = Graph::with_vertices(6);
423        g.add_edge(0, 1).unwrap();
424        g.add_edge(1, 2).unwrap();
425        g.add_edge(2, 3).unwrap();
426        g.add_edge(0, 4).unwrap();
427        g.add_edge(4, 5).unwrap();
428        g.add_edge(5, 3).unwrap();
429        let paths = get_shortest_paths(&g, 0).unwrap();
430        assert_eq!(paths[0], vec![0]);
431        assert_eq!(paths[1], vec![0, 1]);
432        assert_eq!(paths[4], vec![0, 4]);
433        assert_eq!(paths[5], vec![0, 4, 5]);
434        // Path to 3: either via 1-2-3 or 4-5-3 (both length 3 vertices).
435        assert_eq!(paths[3].len(), 4);
436        assert_eq!(paths[3][0], 0);
437        assert_eq!(*paths[3].last().unwrap(), 3);
438    }
439}