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