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        .expect("Test: operation failed");
142    let target_idx = graph
143        .inner()
144        .node_indices()
145        .find(|&idx| graph.inner()[idx] == *target)
146        .expect("Test: operation failed");
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).expect("Operation failed");
234///
235/// let path = dijkstra_path(&graph, &"A".to_string(), &"B".to_string()).expect("Operation failed");
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        .expect("Test: operation failed");
306    let target_idx = graph
307        .inner()
308        .node_indices()
309        .find(|&idx| graph.inner()[idx] == *target)
310        .expect("Test: operation failed");
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()
607                        || tentative_g < *current_neighbor_g.expect("Test: operation failed")
608                    {
609                        came_from.insert(neighbor.clone(), current.clone());
610                        g_score.insert(neighbor.clone(), tentative_g);
611
612                        let mut new_path = current_state.path.clone();
613                        new_path.push(neighbor.clone());
614
615                        open_set.push(AStarState {
616                            node: neighbor.clone(),
617                            cost: tentative_g,
618                            heuristic: heuristic(&neighbor),
619                            path: new_path,
620                        });
621                    }
622                }
623            }
624        }
625    }
626
627    Err(GraphError::NoPath {
628        src_node: format!("{source:?}"),
629        target: format!("{target:?}"),
630        nodes: 0,
631        edges: 0,
632    })
633}
634
635/// A* search for directed graphs
636#[allow(dead_code)]
637pub fn astar_search_digraph<N, E, Ix, H>(
638    graph: &DiGraph<N, E, Ix>,
639    source: &N,
640    target: &N,
641    heuristic: H,
642) -> Result<AStarResult<N, E>>
643where
644    N: Node + std::fmt::Debug + Clone + Hash + Eq,
645    E: EdgeWeight
646        + Clone
647        + std::ops::Add<Output = E>
648        + scirs2_core::numeric::Zero
649        + PartialOrd
650        + Copy,
651    Ix: petgraph::graph::IndexType,
652    H: Fn(&N) -> E,
653{
654    if !graph.contains_node(source) || !graph.contains_node(target) {
655        return Err(GraphError::node_not_found("node"));
656    }
657
658    let mut open_set = BinaryHeap::new();
659    let mut g_score: HashMap<N, E> = HashMap::new();
660    let mut came_from: HashMap<N, N> = HashMap::new();
661
662    g_score.insert(source.clone(), E::zero());
663
664    open_set.push(AStarState {
665        node: source.clone(),
666        cost: E::zero(),
667        heuristic: heuristic(source),
668        path: vec![source.clone()],
669    });
670
671    while let Some(current_state) = open_set.pop() {
672        let current = &current_state.node;
673
674        if current == target {
675            return Ok(AStarResult {
676                path: current_state.path,
677                cost: current_state.cost,
678            });
679        }
680
681        let current_g = g_score.get(current).cloned().unwrap_or_else(E::zero);
682
683        if let Ok(successors) = graph.successors(current) {
684            for neighbor in successors {
685                if let Ok(edge_weight) = graph.edge_weight(current, &neighbor) {
686                    let tentative_g = current_g + edge_weight;
687
688                    let current_neighbor_g = g_score.get(&neighbor);
689                    if current_neighbor_g.is_none()
690                        || tentative_g < *current_neighbor_g.expect("Test: operation failed")
691                    {
692                        came_from.insert(neighbor.clone(), current.clone());
693                        g_score.insert(neighbor.clone(), tentative_g);
694
695                        let mut new_path = current_state.path.clone();
696                        new_path.push(neighbor.clone());
697
698                        open_set.push(AStarState {
699                            node: neighbor.clone(),
700                            cost: tentative_g,
701                            heuristic: heuristic(&neighbor),
702                            path: new_path,
703                        });
704                    }
705                }
706            }
707        }
708    }
709
710    Err(GraphError::NoPath {
711        src_node: format!("{source:?}"),
712        target: format!("{target:?}"),
713        nodes: 0,
714        edges: 0,
715    })
716}
717
718/// Finds K shortest paths between two nodes using Yen's algorithm
719///
720/// Returns up to k shortest paths sorted by total weight.
721/// Each path includes the total weight and the sequence of nodes.
722#[allow(dead_code)]
723pub fn k_shortest_paths<N, E, Ix>(
724    graph: &Graph<N, E, Ix>,
725    source: &N,
726    target: &N,
727    k: usize,
728) -> Result<Vec<(f64, Vec<N>)>>
729where
730    N: Node + std::fmt::Debug + Clone + Hash + Eq + Ord,
731    E: EdgeWeight
732        + Into<f64>
733        + Clone
734        + scirs2_core::numeric::Zero
735        + scirs2_core::numeric::One
736        + std::ops::Add<Output = E>
737        + PartialOrd
738        + std::marker::Copy
739        + std::fmt::Debug
740        + std::default::Default,
741    Ix: petgraph::graph::IndexType,
742{
743    if k == 0 {
744        return Ok(vec![]);
745    }
746
747    // Check if nodes exist
748    if !graph.contains_node(source) || !graph.contains_node(target) {
749        return Err(GraphError::node_not_found("node"));
750    }
751
752    let mut paths = Vec::new();
753    let mut candidates = std::collections::BinaryHeap::new();
754
755    // Find the shortest path first
756    match dijkstra_path(graph, source, target) {
757        Ok(Some(path)) => {
758            let weight: f64 = path.total_weight.into();
759            paths.push((weight, path.nodes));
760        }
761        Ok(None) => return Ok(vec![]), // No path exists
762        Err(e) => return Err(e),
763    }
764
765    // Find k-1 more paths
766    for i in 0..k - 1 {
767        if i >= paths.len() {
768            break;
769        }
770
771        let (_, prev_path) = &paths[i];
772
773        // For each node in the previous path (except the last)
774        for j in 0..prev_path.len() - 1 {
775            let spur_node = &prev_path[j];
776            let root_path = &prev_path[..=j];
777
778            // Store edges to remove temporarily
779            let mut removed_edges = Vec::new();
780
781            // Remove edges that are part of previous paths with same root
782            for (_, path) in &paths {
783                if path.len() > j && &path[..=j] == root_path && j + 1 < path.len() {
784                    removed_edges.push((path[j].clone(), path[j + 1].clone()));
785                }
786            }
787
788            // Find shortest path from spur_node to target avoiding removed edges
789            if let Ok((spur_weight, spur_path)) =
790                shortest_path_avoiding_edges(graph, spur_node, target, &removed_edges, root_path)
791            {
792                // Calculate total weight of the new path
793                let mut total_weight = spur_weight;
794                for idx in 0..j {
795                    if let Ok(edge_weight) = graph.edge_weight(&prev_path[idx], &prev_path[idx + 1])
796                    {
797                        let weight: f64 = edge_weight.into();
798                        total_weight += weight;
799                    }
800                }
801
802                // Construct the complete path
803                let mut complete_path = root_path[..j].to_vec();
804                complete_path.extend(spur_path);
805
806                // Add to candidates with negative weight for min-heap behavior
807                candidates.push((
808                    std::cmp::Reverse(ordered_float::OrderedFloat(total_weight)),
809                    complete_path.clone(),
810                ));
811            }
812        }
813    }
814
815    // Extract paths from candidates
816    while paths.len() < k && !candidates.is_empty() {
817        let (std::cmp::Reverse(ordered_float::OrderedFloat(weight)), path) =
818            candidates.pop().expect("Operation failed");
819
820        // Check if this path is already in our result
821        let is_duplicate = paths.iter().any(|(_, p)| p == &path);
822        if !is_duplicate {
823            paths.push((weight, path));
824        }
825    }
826
827    Ok(paths)
828}
829
830/// Helper function for k-shortest paths that finds shortest path avoiding certain edges
831#[allow(dead_code)]
832fn shortest_path_avoiding_edges<N, E, Ix>(
833    graph: &Graph<N, E, Ix>,
834    source: &N,
835    target: &N,
836    avoided_edges: &[(N, N)],
837    excluded_nodes: &[N],
838) -> Result<(f64, Vec<N>)>
839where
840    N: Node + std::fmt::Debug + Clone + Hash + Eq + Ord,
841    E: EdgeWeight + Into<f64>,
842    Ix: petgraph::graph::IndexType,
843{
844    use std::cmp::Reverse;
845
846    let mut distances: HashMap<N, f64> = HashMap::new();
847    let mut previous: HashMap<N, N> = HashMap::new();
848    let mut heap = BinaryHeap::new();
849
850    distances.insert(source.clone(), 0.0);
851    heap.push((Reverse(ordered_float::OrderedFloat(0.0)), source.clone()));
852
853    while let Some((Reverse(ordered_float::OrderedFloat(dist)), node)) = heap.pop() {
854        if &node == target {
855            // Reconstruct path
856            let mut path = vec![target.clone()];
857            let mut current = target.clone();
858
859            while let Some(prev) = previous.get(&current) {
860                path.push(prev.clone());
861                current = prev.clone();
862            }
863
864            path.reverse();
865            return Ok((dist, path));
866        }
867
868        if distances.get(&node).is_none_or(|&d| dist > d) {
869            continue;
870        }
871
872        if let Ok(neighbors) = graph.neighbors(&node) {
873            for neighbor in neighbors {
874                // Skip if this edge should be avoided
875                if avoided_edges.contains(&(node.clone(), neighbor.clone())) {
876                    continue;
877                }
878
879                // Skip if this node is in excluded _nodes (except source and target)
880                if &neighbor != source && &neighbor != target && excluded_nodes.contains(&neighbor)
881                {
882                    continue;
883                }
884
885                if let Ok(edge_weight) = graph.edge_weight(&node, &neighbor) {
886                    let weight: f64 = edge_weight.into();
887                    let new_dist = dist + weight;
888
889                    if new_dist < *distances.get(&neighbor).unwrap_or(&f64::INFINITY) {
890                        distances.insert(neighbor.clone(), new_dist);
891                        previous.insert(neighbor.clone(), node.clone());
892                        heap.push((Reverse(ordered_float::OrderedFloat(new_dist)), neighbor));
893                    }
894                }
895            }
896        }
897    }
898
899    Err(GraphError::NoPath {
900        src_node: format!("{source:?}"),
901        target: format!("{target:?}"),
902        nodes: 0,
903        edges: 0,
904    })
905}
906
907#[cfg(test)]
908mod tests {
909    use super::*;
910
911    #[test]
912    #[allow(deprecated)]
913    fn test_shortest_path() {
914        let mut graph: Graph<i32, f64> = Graph::new();
915
916        // Create a simple graph
917        graph.add_edge(1, 2, 4.0).expect("Operation failed");
918        graph.add_edge(1, 3, 2.0).expect("Operation failed");
919        graph.add_edge(2, 3, 1.0).expect("Operation failed");
920        graph.add_edge(2, 4, 5.0).expect("Operation failed");
921        graph.add_edge(3, 4, 8.0).expect("Operation failed");
922
923        let path = shortest_path(&graph, &1, &4)
924            .expect("Operation failed")
925            .expect("Test: operation failed");
926
927        // The shortest path from 1 to 4 should be 1 -> 3 -> 2 -> 4 with total weight 8.0
928        assert_eq!(path.total_weight, 8.0);
929        assert_eq!(path.nodes, vec![1, 3, 2, 4]);
930    }
931
932    #[test]
933    fn test_floyd_warshall() {
934        let mut graph: Graph<i32, f64> = Graph::new();
935
936        // Create a triangle
937        graph.add_edge(0, 1, 1.0).expect("Operation failed");
938        graph.add_edge(1, 2, 2.0).expect("Operation failed");
939        graph.add_edge(2, 0, 3.0).expect("Operation failed");
940
941        let distances = floyd_warshall(&graph).expect("Operation failed");
942
943        // Check some distances
944        assert_eq!(distances[[0, 0]], 0.0);
945        assert_eq!(distances[[0, 1]], 1.0);
946        assert_eq!(distances[[0, 2]], 3.0); // 0->1->2 is shorter than 0->2
947        assert_eq!(distances[[1, 0]], 1.0); // Undirected graph
948    }
949
950    #[test]
951    fn test_astar_search() {
952        let mut graph: Graph<(i32, i32), f64> = Graph::new();
953
954        // Create a simple grid
955        graph
956            .add_edge((0, 0), (0, 1), 1.0)
957            .expect("Operation failed");
958        graph
959            .add_edge((0, 1), (1, 1), 1.0)
960            .expect("Operation failed");
961        graph
962            .add_edge((1, 1), (1, 0), 1.0)
963            .expect("Operation failed");
964        graph
965            .add_edge((1, 0), (0, 0), 1.0)
966            .expect("Operation failed");
967
968        // Manhattan distance heuristic
969        let heuristic = |&(x, y): &(i32, i32)| -> f64 { ((1 - x).abs() + (1 - y).abs()) as f64 };
970
971        let result = astar_search(&graph, &(0, 0), &(1, 1), heuristic);
972        let result = result.expect("Test: operation failed");
973        assert_eq!(result.cost, 2.0);
974        assert_eq!(result.path.len(), 3); // Start, one intermediate, goal
975    }
976
977    #[test]
978    fn test_k_shortest_paths() {
979        let mut graph: Graph<char, f64> = Graph::new();
980
981        // Create a graph with multiple paths
982        graph.add_edge('A', 'B', 2.0).expect("Operation failed");
983        graph.add_edge('B', 'D', 2.0).expect("Operation failed");
984        graph.add_edge('A', 'C', 1.0).expect("Operation failed");
985        graph.add_edge('C', 'D', 4.0).expect("Operation failed");
986        graph.add_edge('B', 'C', 1.0).expect("Operation failed");
987
988        let paths = k_shortest_paths(&graph, &'A', &'D', 3).expect("Operation failed");
989
990        assert!(paths.len() >= 2);
991        assert_eq!(paths[0].0, 4.0); // A->B->D
992        assert_eq!(paths[0].1, vec!['A', 'B', 'D']);
993    }
994}