Skip to main content

rust_igraph/algorithms/paths/
k_shortest_paths.rs

1//! k shortest loopless paths via Yen's algorithm (ALGO-PA-034).
2//!
3//! Counterpart of `igraph_get_k_shortest_paths()` from
4//! `references/igraph/src/paths/shortest_paths.c:1402`.
5//!
6//! Finds the k shortest loopless paths between two vertices using
7//! Yen's algorithm (1970). Each iteration finds the next shortest
8//! path by systematically deviating from previously found paths.
9//!
10//! Reference: Yen, Jin Y. "An algorithm for finding shortest routes
11//! from all source nodes to a given destination in general networks."
12//! Quarterly of Applied Mathematics 27(4): 526–530 (1970).
13
14use crate::algorithms::paths::dijkstra::{DijkstraMode, dijkstra_path_to_with_mode};
15use crate::core::graph::EdgeId;
16use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
17
18/// Sentinel weight used to "remove" an edge: large enough that it will never
19/// appear on a shortest path, but finite so dijkstra's weight validation passes.
20const REMOVED_WEIGHT: f64 = f64::MAX / 4.0;
21
22/// Result of [`k_shortest_paths`].
23#[derive(Debug, Clone)]
24pub struct KShortestPath {
25    /// Vertex sequence of the path (including source and target).
26    pub vertices: Vec<VertexId>,
27    /// Edge sequence of the path.
28    pub edges: Vec<EdgeId>,
29    /// Total weight of the path.
30    pub weight: f64,
31}
32
33/// Finds the k shortest loopless paths between `source` and `target`.
34///
35/// Returns up to `k` paths in order of increasing total weight.
36/// If fewer than `k` paths exist, fewer are returned.
37///
38/// `weights` must have length `ecount()` and contain non-negative
39/// values. Edges with weight `f64::INFINITY` are treated as absent.
40///
41/// # Errors
42///
43/// - `VertexOutOfRange` if `source` or `target` exceeds `vcount()`.
44/// - `InvalidArgument` if `weights` length differs from `ecount()`.
45/// - `InvalidArgument` if `weights` contains negative values.
46///
47/// # Examples
48///
49/// ```
50/// use rust_igraph::{Graph, k_shortest_paths, DijkstraMode};
51///
52/// // Diamond: 0→1→3, 0→2→3 (two simple paths of equal length)
53/// let mut g = Graph::with_vertices(4);
54/// g.add_edge(0, 1).unwrap(); // edge 0
55/// g.add_edge(1, 3).unwrap(); // edge 1
56/// g.add_edge(0, 2).unwrap(); // edge 2
57/// g.add_edge(2, 3).unwrap(); // edge 3
58/// let w = vec![1.0; 4];
59/// let paths = k_shortest_paths(&g, 0, 3, &w, 3, DijkstraMode::All).unwrap();
60/// assert_eq!(paths.len(), 2); // only 2 simple paths exist
61/// assert!((paths[0].weight - 2.0).abs() < 1e-12);
62/// assert!((paths[1].weight - 2.0).abs() < 1e-12);
63/// ```
64#[allow(clippy::too_many_lines)]
65pub fn k_shortest_paths(
66    graph: &Graph,
67    source: VertexId,
68    target: VertexId,
69    weights: &[f64],
70    k: usize,
71    mode: DijkstraMode,
72) -> IgraphResult<Vec<KShortestPath>> {
73    let n = graph.vcount();
74    let m = graph.ecount();
75
76    if source >= n {
77        return Err(IgraphError::VertexOutOfRange { id: source, n });
78    }
79    if target >= n {
80        return Err(IgraphError::VertexOutOfRange { id: target, n });
81    }
82    if weights.len() != m {
83        return Err(IgraphError::InvalidArgument(format!(
84            "k_shortest_paths: weights length ({}) != ecount ({m})",
85            weights.len()
86        )));
87    }
88    for (i, &w) in weights.iter().enumerate() {
89        if w < 0.0 {
90            return Err(IgraphError::InvalidArgument(format!(
91                "k_shortest_paths: negative weight ({w}) at edge {i}"
92            )));
93        }
94    }
95
96    if k == 0 {
97        return Ok(Vec::new());
98    }
99
100    // Find the first shortest path.
101    let first = dijkstra_path_to_with_mode(graph, source, target, weights, mode)?;
102    let Some((init_vertices, init_edges)) = first else {
103        return Ok(Vec::new()); // no path exists
104    };
105
106    if has_infinite_edge(&init_edges, weights) {
107        return Ok(Vec::new());
108    }
109
110    let first_weight = path_weight(&init_edges, weights);
111    let mut result: Vec<KShortestPath> = Vec::with_capacity(k);
112    result.push(KShortestPath {
113        vertices: init_vertices,
114        edges: init_edges,
115        weight: first_weight,
116    });
117
118    if k == 1 {
119        return Ok(result);
120    }
121
122    // Candidate pool: (edges, weight)
123    let mut candidates: Vec<(Vec<EdgeId>, f64)> = Vec::new();
124
125    // Working copy of weights (infinity = removed edge).
126    let mut cur_weights: Vec<f64> = weights.to_vec();
127
128    for i_path in 1..k {
129        let prev_edges = &result[i_path - 1].edges;
130        let prev_len = prev_edges.len();
131
132        for spur_idx in 0..prev_len {
133            // Determine spur vertex: the source-side endpoint of the edge at spur_idx.
134            let spur_vertex = edge_source_vertex(graph, prev_edges, spur_idx, mode)?;
135
136            // Root path = edges[0..spur_idx].
137            let root_path = &prev_edges[..spur_idx];
138
139            // Remove edges that share the same root path in previously found shortest paths.
140            let mut removed_edges: Vec<usize> = Vec::new();
141            for found in &result {
142                if found.edges.len() > spur_idx && found.edges[..spur_idx] == *root_path {
143                    let eid = found.edges[spur_idx] as usize;
144                    if cur_weights[eid] != REMOVED_WEIGHT {
145                        cur_weights[eid] = REMOVED_WEIGHT;
146                        removed_edges.push(eid);
147                    }
148                }
149            }
150
151            // Remove root-path vertices (except spur) by setting their incident edges to infinity.
152            for root_idx in 0..spur_idx {
153                let root_vertex = edge_source_vertex(graph, prev_edges, root_idx, mode)?;
154                semi_delete_vertex(
155                    graph,
156                    &mut cur_weights,
157                    root_vertex,
158                    &mut removed_edges,
159                    mode,
160                )?;
161            }
162
163            // Find spur path from spur_vertex to target.
164            let spur_result =
165                dijkstra_path_to_with_mode(graph, spur_vertex, target, &cur_weights, mode)?;
166
167            if let Some((_spur_vs, spur_es)) = spur_result {
168                if !has_removed_edge(&spur_es, &cur_weights) {
169                    // Total path = root + spur.
170                    let mut total_edges: Vec<EdgeId> = root_path.to_vec();
171                    total_edges.extend_from_slice(&spur_es);
172
173                    // Only add if not already in candidates.
174                    let already_exists = candidates.iter().any(|(e, _)| *e == total_edges);
175                    if !already_exists {
176                        let w = path_weight(&total_edges, weights);
177                        candidates.push((total_edges, w));
178                    }
179                }
180            }
181
182            // Restore removed edges.
183            for &eid in &removed_edges {
184                cur_weights[eid] = weights[eid];
185            }
186        }
187
188        // Pick the lightest candidate.
189        if candidates.is_empty() {
190            break;
191        }
192
193        let mut best_idx = 0;
194        let mut best_weight = candidates[0].1;
195        for (idx, &(_, w)) in candidates.iter().enumerate().skip(1) {
196            if w < best_weight {
197                best_weight = w;
198                best_idx = idx;
199            }
200        }
201
202        let (best_edges, best_w) = candidates.swap_remove(best_idx);
203        let best_vs = edge_path_to_vertices(graph, source, &best_edges, mode)?;
204        result.push(KShortestPath {
205            vertices: best_vs,
206            edges: best_edges,
207            weight: best_w,
208        });
209    }
210
211    Ok(result)
212}
213
214fn has_infinite_edge(edges: &[EdgeId], weights: &[f64]) -> bool {
215    edges.iter().any(|&e| weights[e as usize] == f64::INFINITY)
216}
217
218fn has_removed_edge(edges: &[EdgeId], weights: &[f64]) -> bool {
219    edges.iter().any(|&e| weights[e as usize] >= REMOVED_WEIGHT)
220}
221
222fn path_weight(edges: &[EdgeId], weights: &[f64]) -> f64 {
223    edges.iter().map(|&e| weights[e as usize]).sum()
224}
225
226fn edge_source_vertex(
227    graph: &Graph,
228    edge_path: &[EdgeId],
229    idx: usize,
230    mode: DijkstraMode,
231) -> IgraphResult<VertexId> {
232    let (from, to) = graph.edge(edge_path[idx])?;
233    match mode {
234        DijkstraMode::Out => Ok(from),
235        DijkstraMode::In => Ok(to),
236        DijkstraMode::All => {
237            // For undirected/all mode: the source-side is the vertex shared
238            // with the previous edge (or the overall source for idx==0).
239            if idx == 0 {
240                // Determine by looking at the next edge if available.
241                if edge_path.len() > 1 {
242                    let (nf, nt) = graph.edge(edge_path[1])?;
243                    if from == nf || from == nt {
244                        Ok(to)
245                    } else {
246                        Ok(from)
247                    }
248                } else {
249                    // Single edge path — either endpoint could be the source.
250                    // We need context from the caller. For spur_idx=0 the spur
251                    // vertex is the overall source, which is tracked externally.
252                    // Return `from` as default; the caller handles the first-edge case.
253                    Ok(from)
254                }
255            } else {
256                let (pf, pt) = graph.edge(edge_path[idx - 1])?;
257                // The shared vertex between edge[idx-1] and edge[idx] is the
258                // "target" of the previous edge = "source" of this edge's next hop.
259                if from == pf || from == pt {
260                    Ok(from)
261                } else {
262                    Ok(to)
263                }
264            }
265        }
266    }
267}
268
269fn semi_delete_vertex(
270    graph: &Graph,
271    cur_weights: &mut [f64],
272    vertex: VertexId,
273    removed_edges: &mut Vec<usize>,
274    mode: DijkstraMode,
275) -> IgraphResult<()> {
276    let incident = incident_for_mode_yen(graph, vertex, mode)?;
277    for eid in incident {
278        let idx = eid as usize;
279        if cur_weights[idx] != REMOVED_WEIGHT {
280            cur_weights[idx] = REMOVED_WEIGHT;
281            removed_edges.push(idx);
282        }
283    }
284    Ok(())
285}
286
287fn incident_for_mode_yen(
288    graph: &Graph,
289    v: VertexId,
290    mode: DijkstraMode,
291) -> IgraphResult<Vec<EdgeId>> {
292    if !graph.is_directed() || mode == DijkstraMode::All {
293        let mut out = graph.incident(v)?;
294        if graph.is_directed() {
295            out.extend(graph.incident_in(v)?);
296        }
297        return Ok(out);
298    }
299    match mode {
300        DijkstraMode::Out => graph.incident(v),
301        DijkstraMode::In => graph.incident_in(v),
302        DijkstraMode::All => unreachable!(),
303    }
304}
305
306fn edge_path_to_vertices(
307    graph: &Graph,
308    source: VertexId,
309    edge_path: &[EdgeId],
310    mode: DijkstraMode,
311) -> IgraphResult<Vec<VertexId>> {
312    if edge_path.is_empty() {
313        return Ok(vec![source]);
314    }
315
316    let mut vertices = Vec::with_capacity(edge_path.len() + 1);
317    vertices.push(source);
318
319    let mut cur = source;
320    for &eid in edge_path {
321        let (from, to) = graph.edge(eid)?;
322        let next = match mode {
323            DijkstraMode::Out => to,
324            DijkstraMode::In => from,
325            DijkstraMode::All => {
326                if from == cur {
327                    to
328                } else {
329                    from
330                }
331            }
332        };
333        vertices.push(next);
334        cur = next;
335    }
336
337    Ok(vertices)
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn no_path() {
346        let g = Graph::with_vertices(3);
347        let w = vec![]; // no edges
348        let paths = k_shortest_paths(&g, 0, 2, &w, 5, DijkstraMode::All).unwrap();
349        assert!(paths.is_empty());
350    }
351
352    #[test]
353    fn k_zero() {
354        let mut g = Graph::with_vertices(2);
355        g.add_edge(0, 1).unwrap();
356        let paths = k_shortest_paths(&g, 0, 1, &[1.0], 0, DijkstraMode::All).unwrap();
357        assert!(paths.is_empty());
358    }
359
360    #[test]
361    fn single_edge() {
362        let mut g = Graph::with_vertices(2);
363        g.add_edge(0, 1).unwrap();
364        let paths = k_shortest_paths(&g, 0, 1, &[3.0], 5, DijkstraMode::All).unwrap();
365        assert_eq!(paths.len(), 1);
366        assert_eq!(paths[0].vertices, vec![0, 1]);
367        assert_eq!(paths[0].edges, vec![0]);
368        assert!((paths[0].weight - 3.0).abs() < 1e-12);
369    }
370
371    #[test]
372    fn diamond_two_paths() {
373        // 0-1-3 and 0-2-3
374        let mut g = Graph::with_vertices(4);
375        g.add_edge(0, 1).unwrap(); // 0
376        g.add_edge(1, 3).unwrap(); // 1
377        g.add_edge(0, 2).unwrap(); // 2
378        g.add_edge(2, 3).unwrap(); // 3
379        let w = vec![1.0; 4];
380        let paths = k_shortest_paths(&g, 0, 3, &w, 5, DijkstraMode::All).unwrap();
381        assert_eq!(paths.len(), 2);
382        assert!((paths[0].weight - 2.0).abs() < 1e-12);
383        assert!((paths[1].weight - 2.0).abs() < 1e-12);
384    }
385
386    #[test]
387    fn diamond_different_weights() {
388        // 0→1→3 (weight 2+1=3) and 0→2→3 (weight 1+1=2)
389        let mut g = Graph::with_vertices(4);
390        g.add_edge(0, 1).unwrap(); // 0, w=2
391        g.add_edge(1, 3).unwrap(); // 1, w=1
392        g.add_edge(0, 2).unwrap(); // 2, w=1
393        g.add_edge(2, 3).unwrap(); // 3, w=1
394        let w = vec![2.0, 1.0, 1.0, 1.0];
395        let paths = k_shortest_paths(&g, 0, 3, &w, 2, DijkstraMode::All).unwrap();
396        assert_eq!(paths.len(), 2);
397        assert!((paths[0].weight - 2.0).abs() < 1e-12); // 0→2→3
398        assert!((paths[1].weight - 3.0).abs() < 1e-12); // 0→1→3
399    }
400
401    #[test]
402    fn path_graph_single_path() {
403        // 0—1—2—3 only one simple path
404        let mut g = Graph::with_vertices(4);
405        g.add_edge(0, 1).unwrap();
406        g.add_edge(1, 2).unwrap();
407        g.add_edge(2, 3).unwrap();
408        let w = vec![1.0; 3];
409        let paths = k_shortest_paths(&g, 0, 3, &w, 3, DijkstraMode::All).unwrap();
410        assert_eq!(paths.len(), 1);
411        assert_eq!(paths[0].vertices, vec![0, 1, 2, 3]);
412    }
413
414    #[test]
415    fn three_paths() {
416        // Triangle with extra path: 0-1-2, 0-2 (direct), 0-1-3-2
417        let mut g = Graph::with_vertices(4);
418        g.add_edge(0, 1).unwrap(); // 0, w=1
419        g.add_edge(1, 2).unwrap(); // 1, w=1
420        g.add_edge(0, 2).unwrap(); // 2, w=3
421        g.add_edge(1, 3).unwrap(); // 3, w=1
422        g.add_edge(3, 2).unwrap(); // 4, w=1
423        let w = vec![1.0, 1.0, 3.0, 1.0, 1.0];
424        let paths = k_shortest_paths(&g, 0, 2, &w, 5, DijkstraMode::All).unwrap();
425        assert!(paths.len() >= 3);
426        // Shortest: 0-1-2 (weight 2)
427        assert!((paths[0].weight - 2.0).abs() < 1e-12);
428        // Second: 0-1-3-2 (weight 3) or 0-2 (weight 3)
429        assert!((paths[1].weight - 3.0).abs() < 1e-12);
430        assert!((paths[2].weight - 3.0).abs() < 1e-12);
431    }
432
433    #[test]
434    fn source_equals_target() {
435        let mut g = Graph::with_vertices(3);
436        g.add_edge(0, 1).unwrap();
437        g.add_edge(1, 2).unwrap();
438        let w = vec![1.0; 2];
439        let paths = k_shortest_paths(&g, 0, 0, &w, 1, DijkstraMode::All).unwrap();
440        assert_eq!(paths.len(), 1);
441        assert_eq!(paths[0].vertices, vec![0]);
442        assert!(paths[0].edges.is_empty());
443        assert!((paths[0].weight - 0.0).abs() < 1e-12);
444    }
445
446    #[test]
447    fn directed_out_mode() {
448        // 0→1→2, 0→2 (direct)
449        let mut g = Graph::new(3, true).unwrap();
450        g.add_edge(0, 1).unwrap(); // 0
451        g.add_edge(1, 2).unwrap(); // 1
452        g.add_edge(0, 2).unwrap(); // 2, w=5
453        let w = vec![1.0, 1.0, 5.0];
454        let paths = k_shortest_paths(&g, 0, 2, &w, 3, DijkstraMode::Out).unwrap();
455        assert_eq!(paths.len(), 2);
456        assert!((paths[0].weight - 2.0).abs() < 1e-12); // 0→1→2
457        assert!((paths[1].weight - 5.0).abs() < 1e-12); // 0→2
458    }
459
460    #[test]
461    fn invalid_source() {
462        let g = Graph::with_vertices(3);
463        assert!(k_shortest_paths(&g, 99, 0, &[], 1, DijkstraMode::All).is_err());
464    }
465
466    #[test]
467    fn negative_weight_error() {
468        let mut g = Graph::with_vertices(2);
469        g.add_edge(0, 1).unwrap();
470        assert!(k_shortest_paths(&g, 0, 1, &[-1.0], 1, DijkstraMode::All).is_err());
471    }
472}