scirs2_graph/algorithms/
shortest_path.rs

1//! Shortest path algorithms
2//!
3//! This module provides various shortest path algorithms including:
4//! - Dijkstra's algorithm
5//! - A* search
6//! - Floyd-Warshall all-pairs shortest paths
7//! - K-shortest paths (Yen's algorithm)
8
9use petgraph::algo::dijkstra;
10use petgraph::visit::EdgeRef;
11use std::cmp::Ordering;
12use std::collections::{BinaryHeap, HashMap};
13use std::hash::Hash;
14
15use crate::base::{DiGraph, EdgeWeight, Graph, Node};
16use crate::error::{GraphError, Result};
17
18/// Path between two nodes in a graph
19#[derive(Debug, Clone)]
20pub struct Path<N: Node + std::fmt::Debug, E: EdgeWeight> {
21    /// The nodes in the path, in order
22    pub nodes: Vec<N>,
23    /// The total weight of the path
24    pub total_weight: E,
25}
26
27/// Result of A* search algorithm
28#[derive(Debug, Clone)]
29pub struct AStarResult<N: Node + std::fmt::Debug, E: EdgeWeight> {
30    /// The path from source to target
31    pub path: Vec<N>,
32    /// The total cost of the path
33    pub cost: E,
34}
35
36/// State for A* priority queue
37#[derive(Clone)]
38struct AStarState<N: Node + std::fmt::Debug, E: EdgeWeight> {
39    node: N,
40    cost: E,
41    heuristic: E,
42    path: Vec<N>,
43}
44
45impl<N: Node + std::fmt::Debug, E: EdgeWeight> PartialEq for AStarState<N, E> {
46    fn eq(&self, other: &Self) -> bool {
47        self.node == other.node
48    }
49}
50
51impl<N: Node + std::fmt::Debug, E: EdgeWeight> Eq for AStarState<N, E> {}
52
53impl<N: Node + std::fmt::Debug, E: EdgeWeight + std::ops::Add<Output = E> + Copy + PartialOrd> Ord
54    for AStarState<N, E>
55{
56    fn cmp(&self, other: &Self) -> Ordering {
57        // Reverse order for min-heap behavior
58        let self_total = self.cost + self.heuristic;
59        let other_total = other.cost + other.heuristic;
60        other_total
61            .partial_cmp(&self_total)
62            .unwrap_or(Ordering::Equal)
63            .then_with(|| {
64                other
65                    .cost
66                    .partial_cmp(&self.cost)
67                    .unwrap_or(Ordering::Equal)
68            })
69    }
70}
71
72impl<N: Node + std::fmt::Debug, E: EdgeWeight + std::ops::Add<Output = E> + Copy + PartialOrd>
73    PartialOrd for AStarState<N, E>
74{
75    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
76        Some(self.cmp(other))
77    }
78}
79
80/// Finds the shortest path between source and target nodes using Dijkstra's algorithm
81///
82/// # Arguments
83/// * `graph` - The graph to search in
84/// * `source` - The source node
85/// * `target` - The target node
86///
87/// # Returns
88/// * `Ok(Some(Path))` - If a path exists
89/// * `Ok(None)` - If no path exists
90/// * `Err(GraphError)` - If the source or target node is not in the graph
91///
92/// # Time Complexity
93/// O((V + E) log V) where V is the number of vertices and E is the number of edges.
94/// Using a binary heap (min-heap) priority queue implementation.
95///
96/// # Space Complexity
97/// O(V) for the distance array and predecessor tracking.
98///
99/// # Deprecation Notice
100/// This function will be updated in v1.0 to return a standardized `PathResult` type
101/// that provides more detailed information about the path and search process.
102/// The current API will remain available but deprecated.
103#[deprecated(
104    note = "Use `dijkstra_path` for future compatibility. This function will return PathResult in v1.0"
105)]
106#[allow(dead_code)]
107pub fn shortest_path<N, E, Ix>(
108    graph: &Graph<N, E, Ix>,
109    source: &N,
110    target: &N,
111) -> Result<Option<Path<N, E>>>
112where
113    N: Node + std::fmt::Debug,
114    E: EdgeWeight
115        + scirs2_core::numeric::Zero
116        + scirs2_core::numeric::One
117        + std::ops::Add<Output = E>
118        + PartialOrd
119        + std::marker::Copy
120        + std::fmt::Debug
121        + std::default::Default,
122    Ix: petgraph::graph::IndexType,
123{
124    // Check if source and target are in the graph
125    if !graph.has_node(source) {
126        return Err(GraphError::InvalidGraph(format!(
127            "Source node {source:?} not found"
128        )));
129    }
130    if !graph.has_node(target) {
131        return Err(GraphError::InvalidGraph(format!(
132            "Target node {target:?} not found"
133        )));
134    }
135
136    let source_idx = graph
137        .inner()
138        .node_indices()
139        .find(|&idx| graph.inner()[idx] == *source)
140        .expect("Test: operation failed");
141    let target_idx = graph
142        .inner()
143        .node_indices()
144        .find(|&idx| graph.inner()[idx] == *target)
145        .expect("Test: operation failed");
146
147    // Use petgraph's Dijkstra algorithm implementation
148    let results = dijkstra(graph.inner(), source_idx, Some(target_idx), |e| *e.weight());
149
150    // If target is not reachable, return None
151    if !results.contains_key(&target_idx) {
152        return Ok(None);
153    }
154
155    let total_weight = results[&target_idx];
156
157    // Reconstruct the path
158    let mut path = Vec::new();
159    let mut current = target_idx;
160
161    path.push(graph.inner()[current].clone());
162
163    // Backtrack to find the path
164    while current != source_idx {
165        let min_prev = graph
166            .inner()
167            .edges_directed(current, petgraph::Direction::Incoming)
168            .filter_map(|e| {
169                let from = e.source();
170                let edge_weight = *e.weight();
171
172                // Check if this node is part of the shortest path
173                if let Some(from_dist) = results.get(&from) {
174                    // If this edge is part of the shortest path
175                    if *from_dist + edge_weight == results[&current] {
176                        return Some((from, *from_dist));
177                    }
178                }
179                None
180            })
181            .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
182
183        if let Some((prev, _)) = min_prev {
184            current = prev;
185            path.push(graph.inner()[current].clone());
186        } else {
187            // This shouldn't happen if Dijkstra's algorithm works correctly
188            return Err(GraphError::AlgorithmError(
189                "Failed to reconstruct path".to_string(),
190            ));
191        }
192    }
193
194    // Reverse the path to get it from source to target
195    path.reverse();
196
197    Ok(Some(Path {
198        nodes: path,
199        total_weight,
200    }))
201}
202
203/// Finds the shortest path between source and target nodes using Dijkstra's algorithm (modern API)
204///
205/// This function provides the same functionality as `shortest_path` but with a clearer name
206/// that explicitly indicates the algorithm being used. This is the recommended function to use
207/// going forward.
208///
209/// # Arguments
210/// * `graph` - The graph to search in
211/// * `source` - The source node
212/// * `target` - The target node
213///
214/// # Returns
215/// * `Ok(Some(Path))` - If a path exists
216/// * `Ok(None)` - If no path exists
217/// * `Err(GraphError)` - If the source or target node is not in the graph
218///
219/// # Time Complexity
220/// O((V + E) log V) where V is the number of vertices and E is the number of edges.
221///
222/// # Space Complexity
223/// O(V) for the distance array and predecessor tracking.
224///
225/// # Example
226/// ```rust
227/// use scirs2_graph::{Graph, dijkstra_path};
228///
229/// let mut graph: Graph<String, f64> = Graph::new();
230/// graph.add_node("A".to_string());
231/// graph.add_node("B".to_string());
232/// graph.add_edge("A".to_string(), "B".to_string(), 1.0).expect("Operation failed");
233///
234/// let path = dijkstra_path(&graph, &"A".to_string(), &"B".to_string()).expect("Operation failed");
235/// assert!(path.is_some());
236/// ```
237#[allow(dead_code)]
238pub fn dijkstra_path<N, E, Ix>(
239    graph: &Graph<N, E, Ix>,
240    source: &N,
241    target: &N,
242) -> Result<Option<Path<N, E>>>
243where
244    N: Node + std::fmt::Debug,
245    E: EdgeWeight
246        + scirs2_core::numeric::Zero
247        + scirs2_core::numeric::One
248        + std::ops::Add<Output = E>
249        + PartialOrd
250        + std::marker::Copy
251        + std::fmt::Debug
252        + std::default::Default,
253    Ix: petgraph::graph::IndexType,
254{
255    #[allow(deprecated)]
256    shortest_path(graph, source, target)
257}
258
259/// Finds the shortest path in a directed graph
260///
261/// # Deprecation Notice
262/// This function will be updated in v1.0 to return a standardized `PathResult` type
263/// that provides more detailed information about the path and search process.
264/// The current API will remain available but deprecated.
265#[deprecated(
266    note = "Use `dijkstra_path_digraph` for future compatibility. This function will return PathResult in v1.0"
267)]
268#[allow(dead_code)]
269pub fn shortest_path_digraph<N, E, Ix>(
270    graph: &DiGraph<N, E, Ix>,
271    source: &N,
272    target: &N,
273) -> Result<Option<Path<N, E>>>
274where
275    N: Node + std::fmt::Debug,
276    E: EdgeWeight
277        + scirs2_core::numeric::Zero
278        + scirs2_core::numeric::One
279        + std::ops::Add<Output = E>
280        + PartialOrd
281        + std::marker::Copy
282        + std::fmt::Debug
283        + std::default::Default,
284    Ix: petgraph::graph::IndexType,
285{
286    // Implementation similar to undirected version
287    // Check if source and target are in the graph
288    if !graph.has_node(source) {
289        return Err(GraphError::InvalidGraph(format!(
290            "Source node {source:?} not found"
291        )));
292    }
293    if !graph.has_node(target) {
294        return Err(GraphError::InvalidGraph(format!(
295            "Target node {target:?} not found"
296        )));
297    }
298
299    let source_idx = graph
300        .inner()
301        .node_indices()
302        .find(|&idx| graph.inner()[idx] == *source)
303        .expect("Test: operation failed");
304    let target_idx = graph
305        .inner()
306        .node_indices()
307        .find(|&idx| graph.inner()[idx] == *target)
308        .expect("Test: operation failed");
309
310    // Use petgraph's Dijkstra algorithm implementation
311    let results = dijkstra(graph.inner(), source_idx, Some(target_idx), |e| *e.weight());
312
313    // If target is not reachable, return None
314    if !results.contains_key(&target_idx) {
315        return Ok(None);
316    }
317
318    let total_weight = results[&target_idx];
319
320    // Reconstruct the path
321    let mut path = Vec::new();
322    let mut current = target_idx;
323
324    path.push(graph.inner()[current].clone());
325
326    // Backtrack to find the path
327    while current != source_idx {
328        let min_prev = graph
329            .inner()
330            .edges_directed(current, petgraph::Direction::Incoming)
331            .filter_map(|e| {
332                let from = e.source();
333                let edge_weight = *e.weight();
334
335                // Check if this node is part of the shortest path
336                if let Some(from_dist) = results.get(&from) {
337                    // If this edge is part of the shortest path
338                    if *from_dist + edge_weight == results[&current] {
339                        return Some((from, *from_dist));
340                    }
341                }
342                None
343            })
344            .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
345
346        if let Some((prev, _)) = min_prev {
347            current = prev;
348            path.push(graph.inner()[current].clone());
349        } else {
350            // This shouldn't happen if Dijkstra's algorithm works correctly
351            return Err(GraphError::AlgorithmError(
352                "Failed to reconstruct path".to_string(),
353            ));
354        }
355    }
356
357    // Reverse the path to get it from source to target
358    path.reverse();
359
360    Ok(Some(Path {
361        nodes: path,
362        total_weight,
363    }))
364}
365
366/// Finds the shortest path between source and target nodes in a directed graph using Dijkstra's algorithm
367///
368/// This is the modern API that provides a clearer name for directed graph path finding.
369/// This is the recommended function to use for directed graphs.
370///
371/// # Arguments
372/// * `graph` - The directed graph to search in
373/// * `source` - The source node
374/// * `target` - The target node
375///
376/// # Returns
377/// * `Ok(Some(Path))` - If a path exists
378/// * `Ok(None)` - If no path exists
379/// * `Err(GraphError)` - If the source or target node is not in the graph
380///
381/// # Time Complexity
382/// O((V + E) log V) where V is the number of vertices and E is the number of edges.
383///
384/// # Space Complexity
385/// O(V) for the distance array and predecessor tracking.
386#[allow(dead_code)]
387pub fn dijkstra_path_digraph<N, E, Ix>(
388    graph: &DiGraph<N, E, Ix>,
389    source: &N,
390    target: &N,
391) -> Result<Option<Path<N, E>>>
392where
393    N: Node + std::fmt::Debug,
394    E: EdgeWeight
395        + scirs2_core::numeric::Zero
396        + scirs2_core::numeric::One
397        + std::ops::Add<Output = E>
398        + PartialOrd
399        + std::marker::Copy
400        + std::fmt::Debug
401        + std::default::Default,
402    Ix: petgraph::graph::IndexType,
403{
404    #[allow(deprecated)]
405    shortest_path_digraph(graph, source, target)
406}
407
408/// Computes all-pairs shortest paths using the Floyd-Warshall algorithm
409///
410/// Returns a matrix where entry (i, j) contains the shortest distance from node i to node j.
411/// If there's no path, the entry will be infinity.
412///
413/// # Arguments
414/// * `graph` - The graph to analyze (works for both directed and undirected)
415///
416/// # Returns
417/// * `Result<Array2<f64>>` - A matrix of shortest distances
418///
419/// # Time Complexity
420/// O(V³) where V is the number of vertices. This algorithm computes shortest paths
421/// between all pairs of vertices using dynamic programming.
422///
423/// # Space Complexity
424/// O(V²) for the distance matrix.
425///
426/// # Note
427/// This algorithm can detect negative cycles if the diagonal contains negative values
428/// after completion. It works correctly with negative edge weights but not with
429/// negative cycles.
430#[allow(dead_code)]
431pub fn floyd_warshall<N, E, Ix>(
432    graph: &Graph<N, E, Ix>,
433) -> Result<scirs2_core::ndarray::Array2<f64>>
434where
435    N: Node + std::fmt::Debug,
436    E: EdgeWeight + Into<f64> + scirs2_core::numeric::Zero + Copy,
437    Ix: petgraph::graph::IndexType,
438{
439    let n = graph.node_count();
440
441    if n == 0 {
442        return Ok(scirs2_core::ndarray::Array2::zeros((0, 0)));
443    }
444
445    // Initialize distance matrix
446    let mut dist = scirs2_core::ndarray::Array2::from_elem((n, n), f64::INFINITY);
447
448    // Set diagonal to 0 (distance from a node to itself)
449    for i in 0..n {
450        dist[[i, i]] = 0.0;
451    }
452
453    // Initialize with direct edge weights
454    for edge in graph.inner().edge_references() {
455        let i = edge.source().index();
456        let j = edge.target().index();
457        let weight: f64 = (*edge.weight()).into();
458
459        dist[[i, j]] = weight;
460        // For undirected graphs, also set the reverse direction
461        dist[[j, i]] = weight;
462    }
463
464    // Floyd-Warshall algorithm
465    for k in 0..n {
466        for i in 0..n {
467            for j in 0..n {
468                let alt = dist[[i, k]] + dist[[k, j]];
469                if alt < dist[[i, j]] {
470                    dist[[i, j]] = alt;
471                }
472            }
473        }
474    }
475
476    Ok(dist)
477}
478
479/// Computes all-pairs shortest paths for a directed graph using Floyd-Warshall
480#[allow(dead_code)]
481pub fn floyd_warshall_digraph<N, E, Ix>(
482    graph: &DiGraph<N, E, Ix>,
483) -> Result<scirs2_core::ndarray::Array2<f64>>
484where
485    N: Node + std::fmt::Debug,
486    E: EdgeWeight + Into<f64> + scirs2_core::numeric::Zero + Copy,
487    Ix: petgraph::graph::IndexType,
488{
489    let n = graph.node_count();
490
491    if n == 0 {
492        return Ok(scirs2_core::ndarray::Array2::zeros((0, 0)));
493    }
494
495    // Initialize distance matrix
496    let mut dist = scirs2_core::ndarray::Array2::from_elem((n, n), f64::INFINITY);
497
498    // Set diagonal to 0 (distance from a node to itself)
499    for i in 0..n {
500        dist[[i, i]] = 0.0;
501    }
502
503    // Initialize with direct edge weights
504    for edge in graph.inner().edge_references() {
505        let i = edge.source().index();
506        let j = edge.target().index();
507        let weight: f64 = (*edge.weight()).into();
508
509        dist[[i, j]] = weight;
510    }
511
512    // Floyd-Warshall algorithm
513    for k in 0..n {
514        for i in 0..n {
515            for j in 0..n {
516                let alt = dist[[i, k]] + dist[[k, j]];
517                if alt < dist[[i, j]] {
518                    dist[[i, j]] = alt;
519                }
520            }
521        }
522    }
523
524    Ok(dist)
525}
526
527/// A* search algorithm for finding the shortest path with a heuristic
528///
529/// # Arguments
530/// * `graph` - The graph to search
531/// * `source` - Source node
532/// * `target` - Target node
533/// * `heuristic` - Heuristic function that estimates distance from a node to the target
534///
535/// # Returns
536/// * `Result<AStarResult>` - The shortest path and its cost
537///
538/// # Time Complexity
539/// O(b^d) where b is the branching factor and d is the depth of the solution.
540/// In the worst case with an inadmissible heuristic: O(V²).
541/// With a perfect heuristic: O(V).
542/// Typical case with good heuristic: O(V log V).
543///
544/// # Space Complexity
545/// O(V) for the open set, closed set, and path reconstruction.
546///
547/// # Note
548/// The heuristic function must be admissible (never overestimate the actual cost)
549/// and consistent (satisfy the triangle inequality) to guarantee finding the
550/// optimal path.
551#[allow(dead_code)]
552pub fn astar_search<N, E, Ix, H>(
553    graph: &Graph<N, E, Ix>,
554    source: &N,
555    target: &N,
556    heuristic: H,
557) -> Result<AStarResult<N, E>>
558where
559    N: Node + std::fmt::Debug + Clone + Hash + Eq,
560    E: EdgeWeight
561        + Clone
562        + std::ops::Add<Output = E>
563        + scirs2_core::numeric::Zero
564        + PartialOrd
565        + Copy,
566    Ix: petgraph::graph::IndexType,
567    H: Fn(&N) -> E,
568{
569    if !graph.contains_node(source) || !graph.contains_node(target) {
570        return Err(GraphError::node_not_found("node"));
571    }
572
573    let mut open_set = BinaryHeap::new();
574    let mut g_score: HashMap<N, E> = HashMap::new();
575    let mut came_from: HashMap<N, N> = HashMap::new();
576
577    g_score.insert(source.clone(), E::zero());
578
579    open_set.push(AStarState {
580        node: source.clone(),
581        cost: E::zero(),
582        heuristic: heuristic(source),
583        path: vec![source.clone()],
584    });
585
586    while let Some(current_state) = open_set.pop() {
587        let current = &current_state.node;
588
589        if current == target {
590            return Ok(AStarResult {
591                path: current_state.path,
592                cost: current_state.cost,
593            });
594        }
595
596        let current_g = g_score.get(current).cloned().unwrap_or_else(E::zero);
597
598        if let Ok(neighbors) = graph.neighbors(current) {
599            for neighbor in neighbors {
600                if let Ok(edge_weight) = graph.edge_weight(current, &neighbor) {
601                    let tentative_g = current_g + edge_weight;
602
603                    let current_neighbor_g = g_score.get(&neighbor);
604                    if current_neighbor_g.is_none()
605                        || tentative_g < *current_neighbor_g.expect("Test: operation failed")
606                    {
607                        came_from.insert(neighbor.clone(), current.clone());
608                        g_score.insert(neighbor.clone(), tentative_g);
609
610                        let mut new_path = current_state.path.clone();
611                        new_path.push(neighbor.clone());
612
613                        open_set.push(AStarState {
614                            node: neighbor.clone(),
615                            cost: tentative_g,
616                            heuristic: heuristic(&neighbor),
617                            path: new_path,
618                        });
619                    }
620                }
621            }
622        }
623    }
624
625    Err(GraphError::NoPath {
626        src_node: format!("{source:?}"),
627        target: format!("{target:?}"),
628        nodes: 0,
629        edges: 0,
630    })
631}
632
633/// A* search for directed graphs
634#[allow(dead_code)]
635pub fn astar_search_digraph<N, E, Ix, H>(
636    graph: &DiGraph<N, E, Ix>,
637    source: &N,
638    target: &N,
639    heuristic: H,
640) -> Result<AStarResult<N, E>>
641where
642    N: Node + std::fmt::Debug + Clone + Hash + Eq,
643    E: EdgeWeight
644        + Clone
645        + std::ops::Add<Output = E>
646        + scirs2_core::numeric::Zero
647        + PartialOrd
648        + Copy,
649    Ix: petgraph::graph::IndexType,
650    H: Fn(&N) -> E,
651{
652    if !graph.contains_node(source) || !graph.contains_node(target) {
653        return Err(GraphError::node_not_found("node"));
654    }
655
656    let mut open_set = BinaryHeap::new();
657    let mut g_score: HashMap<N, E> = HashMap::new();
658    let mut came_from: HashMap<N, N> = HashMap::new();
659
660    g_score.insert(source.clone(), E::zero());
661
662    open_set.push(AStarState {
663        node: source.clone(),
664        cost: E::zero(),
665        heuristic: heuristic(source),
666        path: vec![source.clone()],
667    });
668
669    while let Some(current_state) = open_set.pop() {
670        let current = &current_state.node;
671
672        if current == target {
673            return Ok(AStarResult {
674                path: current_state.path,
675                cost: current_state.cost,
676            });
677        }
678
679        let current_g = g_score.get(current).cloned().unwrap_or_else(E::zero);
680
681        if let Ok(successors) = graph.successors(current) {
682            for neighbor in successors {
683                if let Ok(edge_weight) = graph.edge_weight(current, &neighbor) {
684                    let tentative_g = current_g + edge_weight;
685
686                    let current_neighbor_g = g_score.get(&neighbor);
687                    if current_neighbor_g.is_none()
688                        || tentative_g < *current_neighbor_g.expect("Test: operation failed")
689                    {
690                        came_from.insert(neighbor.clone(), current.clone());
691                        g_score.insert(neighbor.clone(), tentative_g);
692
693                        let mut new_path = current_state.path.clone();
694                        new_path.push(neighbor.clone());
695
696                        open_set.push(AStarState {
697                            node: neighbor.clone(),
698                            cost: tentative_g,
699                            heuristic: heuristic(&neighbor),
700                            path: new_path,
701                        });
702                    }
703                }
704            }
705        }
706    }
707
708    Err(GraphError::NoPath {
709        src_node: format!("{source:?}"),
710        target: format!("{target:?}"),
711        nodes: 0,
712        edges: 0,
713    })
714}
715
716/// Finds K shortest paths between two nodes using Yen's algorithm
717///
718/// Returns up to k shortest paths sorted by total weight.
719/// Each path includes the total weight and the sequence of nodes.
720#[allow(dead_code)]
721pub fn k_shortest_paths<N, E, Ix>(
722    graph: &Graph<N, E, Ix>,
723    source: &N,
724    target: &N,
725    k: usize,
726) -> Result<Vec<(f64, Vec<N>)>>
727where
728    N: Node + std::fmt::Debug + Clone + Hash + Eq + Ord,
729    E: EdgeWeight
730        + Into<f64>
731        + Clone
732        + scirs2_core::numeric::Zero
733        + scirs2_core::numeric::One
734        + std::ops::Add<Output = E>
735        + PartialOrd
736        + std::marker::Copy
737        + std::fmt::Debug
738        + std::default::Default,
739    Ix: petgraph::graph::IndexType,
740{
741    if k == 0 {
742        return Ok(vec![]);
743    }
744
745    // Check if nodes exist
746    if !graph.contains_node(source) || !graph.contains_node(target) {
747        return Err(GraphError::node_not_found("node"));
748    }
749
750    let mut paths = Vec::new();
751    let mut candidates = std::collections::BinaryHeap::new();
752
753    // Find the shortest path first
754    match dijkstra_path(graph, source, target) {
755        Ok(Some(path)) => {
756            let weight: f64 = path.total_weight.into();
757            paths.push((weight, path.nodes));
758        }
759        Ok(None) => return Ok(vec![]), // No path exists
760        Err(e) => return Err(e),
761    }
762
763    // Find k-1 more paths
764    for i in 0..k - 1 {
765        if i >= paths.len() {
766            break;
767        }
768
769        let (_, prev_path) = &paths[i];
770
771        // For each node in the previous path (except the last)
772        for j in 0..prev_path.len() - 1 {
773            let spur_node = &prev_path[j];
774            let root_path = &prev_path[..=j];
775
776            // Store edges to remove temporarily
777            let mut removed_edges = Vec::new();
778
779            // Remove edges that are part of previous paths with same root
780            for (_, path) in &paths {
781                if path.len() > j && &path[..=j] == root_path && j + 1 < path.len() {
782                    removed_edges.push((path[j].clone(), path[j + 1].clone()));
783                }
784            }
785
786            // Find shortest path from spur_node to target avoiding removed edges
787            if let Ok((spur_weight, spur_path)) =
788                shortest_path_avoiding_edges(graph, spur_node, target, &removed_edges, root_path)
789            {
790                // Calculate total weight of the new path
791                let mut total_weight = spur_weight;
792                for idx in 0..j {
793                    if let Ok(edge_weight) = graph.edge_weight(&prev_path[idx], &prev_path[idx + 1])
794                    {
795                        let weight: f64 = edge_weight.into();
796                        total_weight += weight;
797                    }
798                }
799
800                // Construct the complete path
801                let mut complete_path = root_path[..j].to_vec();
802                complete_path.extend(spur_path);
803
804                // Add to candidates with negative weight for min-heap behavior
805                candidates.push((
806                    std::cmp::Reverse(ordered_float::OrderedFloat(total_weight)),
807                    complete_path.clone(),
808                ));
809            }
810        }
811    }
812
813    // Extract paths from candidates
814    while paths.len() < k && !candidates.is_empty() {
815        let (std::cmp::Reverse(ordered_float::OrderedFloat(weight)), path) =
816            candidates.pop().expect("Operation failed");
817
818        // Check if this path is already in our result
819        let is_duplicate = paths.iter().any(|(_, p)| p == &path);
820        if !is_duplicate {
821            paths.push((weight, path));
822        }
823    }
824
825    Ok(paths)
826}
827
828/// Helper function for k-shortest paths that finds shortest path avoiding certain edges
829#[allow(dead_code)]
830fn shortest_path_avoiding_edges<N, E, Ix>(
831    graph: &Graph<N, E, Ix>,
832    source: &N,
833    target: &N,
834    avoided_edges: &[(N, N)],
835    excluded_nodes: &[N],
836) -> Result<(f64, Vec<N>)>
837where
838    N: Node + std::fmt::Debug + Clone + Hash + Eq + Ord,
839    E: EdgeWeight + Into<f64>,
840    Ix: petgraph::graph::IndexType,
841{
842    use std::cmp::Reverse;
843
844    let mut distances: HashMap<N, f64> = HashMap::new();
845    let mut previous: HashMap<N, N> = HashMap::new();
846    let mut heap = BinaryHeap::new();
847
848    distances.insert(source.clone(), 0.0);
849    heap.push((Reverse(ordered_float::OrderedFloat(0.0)), source.clone()));
850
851    while let Some((Reverse(ordered_float::OrderedFloat(dist)), node)) = heap.pop() {
852        if &node == target {
853            // Reconstruct path
854            let mut path = vec![target.clone()];
855            let mut current = target.clone();
856
857            while let Some(prev) = previous.get(&current) {
858                path.push(prev.clone());
859                current = prev.clone();
860            }
861
862            path.reverse();
863            return Ok((dist, path));
864        }
865
866        if distances.get(&node).is_none_or(|&d| dist > d) {
867            continue;
868        }
869
870        if let Ok(neighbors) = graph.neighbors(&node) {
871            for neighbor in neighbors {
872                // Skip if this edge should be avoided
873                if avoided_edges.contains(&(node.clone(), neighbor.clone())) {
874                    continue;
875                }
876
877                // Skip if this node is in excluded _nodes (except source and target)
878                if &neighbor != source && &neighbor != target && excluded_nodes.contains(&neighbor)
879                {
880                    continue;
881                }
882
883                if let Ok(edge_weight) = graph.edge_weight(&node, &neighbor) {
884                    let weight: f64 = edge_weight.into();
885                    let new_dist = dist + weight;
886
887                    if new_dist < *distances.get(&neighbor).unwrap_or(&f64::INFINITY) {
888                        distances.insert(neighbor.clone(), new_dist);
889                        previous.insert(neighbor.clone(), node.clone());
890                        heap.push((Reverse(ordered_float::OrderedFloat(new_dist)), neighbor));
891                    }
892                }
893            }
894        }
895    }
896
897    Err(GraphError::NoPath {
898        src_node: format!("{source:?}"),
899        target: format!("{target:?}"),
900        nodes: 0,
901        edges: 0,
902    })
903}
904
905#[cfg(test)]
906mod tests {
907    use super::*;
908
909    #[test]
910    #[allow(deprecated)]
911    fn test_shortest_path() {
912        let mut graph: Graph<i32, f64> = Graph::new();
913
914        // Create a simple graph
915        graph.add_edge(1, 2, 4.0).expect("Operation failed");
916        graph.add_edge(1, 3, 2.0).expect("Operation failed");
917        graph.add_edge(2, 3, 1.0).expect("Operation failed");
918        graph.add_edge(2, 4, 5.0).expect("Operation failed");
919        graph.add_edge(3, 4, 8.0).expect("Operation failed");
920
921        let path = shortest_path(&graph, &1, &4)
922            .expect("Operation failed")
923            .expect("Test: operation failed");
924
925        // The shortest path from 1 to 4 should be 1 -> 3 -> 2 -> 4 with total weight 8.0
926        assert_eq!(path.total_weight, 8.0);
927        assert_eq!(path.nodes, vec![1, 3, 2, 4]);
928    }
929
930    #[test]
931    fn test_floyd_warshall() {
932        let mut graph: Graph<i32, f64> = Graph::new();
933
934        // Create a triangle
935        graph.add_edge(0, 1, 1.0).expect("Operation failed");
936        graph.add_edge(1, 2, 2.0).expect("Operation failed");
937        graph.add_edge(2, 0, 3.0).expect("Operation failed");
938
939        let distances = floyd_warshall(&graph).expect("Operation failed");
940
941        // Check some distances
942        assert_eq!(distances[[0, 0]], 0.0);
943        assert_eq!(distances[[0, 1]], 1.0);
944        assert_eq!(distances[[0, 2]], 3.0); // 0->1->2 is shorter than 0->2
945        assert_eq!(distances[[1, 0]], 1.0); // Undirected graph
946    }
947
948    #[test]
949    fn test_astar_search() {
950        let mut graph: Graph<(i32, i32), f64> = Graph::new();
951
952        // Create a simple grid
953        graph
954            .add_edge((0, 0), (0, 1), 1.0)
955            .expect("Operation failed");
956        graph
957            .add_edge((0, 1), (1, 1), 1.0)
958            .expect("Operation failed");
959        graph
960            .add_edge((1, 1), (1, 0), 1.0)
961            .expect("Operation failed");
962        graph
963            .add_edge((1, 0), (0, 0), 1.0)
964            .expect("Operation failed");
965
966        // Manhattan distance heuristic
967        let heuristic = |&(x, y): &(i32, i32)| -> f64 { ((1 - x).abs() + (1 - y).abs()) as f64 };
968
969        let result = astar_search(&graph, &(0, 0), &(1, 1), heuristic);
970        let result = result.expect("Test: operation failed");
971        assert_eq!(result.cost, 2.0);
972        assert_eq!(result.path.len(), 3); // Start, one intermediate, goal
973    }
974
975    #[test]
976    fn test_k_shortest_paths() {
977        let mut graph: Graph<char, f64> = Graph::new();
978
979        // Create a graph with multiple paths
980        graph.add_edge('A', 'B', 2.0).expect("Operation failed");
981        graph.add_edge('B', 'D', 2.0).expect("Operation failed");
982        graph.add_edge('A', 'C', 1.0).expect("Operation failed");
983        graph.add_edge('C', 'D', 4.0).expect("Operation failed");
984        graph.add_edge('B', 'C', 1.0).expect("Operation failed");
985
986        let paths = k_shortest_paths(&graph, &'A', &'D', 3).expect("Operation failed");
987
988        assert!(paths.len() >= 2);
989        assert_eq!(paths[0].0, 4.0); // A->B->D
990        assert_eq!(paths[0].1, vec!['A', 'B', 'D']);
991    }
992}