graph_shortest_path/
floyd_warshall.rs

1use graph_core::{Graph, GraphError, NodeId};
2
3type FwResult = (Vec<Vec<f64>>, Vec<Vec<Option<NodeId>>>);
4
5/// Runs the Floyd-Warshall all-pairs shortest-path algorithm.
6///
7/// Returns a `V × V` distance matrix indexed by node position (the index of
8/// the [`NodeId`] as returned by [`NodeId::index`]). Entry `[i][j]` is the
9/// shortest distance from node `i` to node `j`, or `f64::INFINITY` if no
10/// path exists.
11///
12/// # Errors
13///
14/// Returns [`GraphError::NegativeCycle`] if a negative-weight cycle is
15/// detected (i.e., `dist[i][i] < 0` for any `i` after the algorithm).
16///
17/// # Complexity
18///
19/// O(V³) time, O(V²) space.
20///
21/// # Examples
22///
23/// ```
24/// use graph_core::{AdjacencyList, Graph};
25/// use graph_shortest_path::floyd_warshall;
26///
27/// let mut g: AdjacencyList<()> = AdjacencyList::directed();
28/// let a = g.add_node(());  // index 0
29/// let b = g.add_node(());  // index 1
30/// let c = g.add_node(());  // index 2
31/// g.add_edge(a, b, 3.0).unwrap();
32/// g.add_edge(b, c, 2.0).unwrap();
33/// g.add_edge(a, c, 10.0).unwrap();
34///
35/// let dist = floyd_warshall(&g).unwrap();
36/// assert_eq!(dist[0][2], 5.0); // a→b→c costs 5, cheaper than a→c at 10
37/// assert_eq!(dist[1][0], f64::INFINITY); // no path back
38/// ```
39///
40/// [`NodeId::index`]: graph_core::NodeId::index
41#[allow(clippy::needless_range_loop)] // 2-D matrix: dist[i][k], dist[k][j] require numeric indices
42pub fn floyd_warshall<G>(graph: &G) -> Result<Vec<Vec<f64>>, GraphError>
43where
44    G: Graph<Weight = f64>,
45{
46    let n = graph.node_count();
47
48    // Initialise the distance matrix.
49    let mut dist = vec![vec![f64::INFINITY; n]; n];
50
51    // Diagonal: distance from a node to itself is 0.
52    for i in 0..n {
53        dist[i][i] = 0.0;
54    }
55
56    // Seed from the graph's edges.
57    for u in graph.nodes() {
58        for (v, &w) in graph.neighbors(u) {
59            let i = u.index();
60            let j = v.index();
61            // Keep minimum if there are parallel edges.
62            if w < dist[i][j] {
63                dist[i][j] = w;
64            }
65        }
66    }
67
68    // Triple loop: k is the intermediate node being considered.
69    for k in 0..n {
70        for i in 0..n {
71            // Skip if there is no path from i to k.
72            if dist[i][k] == f64::INFINITY {
73                continue;
74            }
75            for j in 0..n {
76                if dist[k][j] == f64::INFINITY {
77                    continue;
78                }
79                let through_k = dist[i][k] + dist[k][j];
80                if through_k < dist[i][j] {
81                    dist[i][j] = through_k;
82                }
83            }
84        }
85    }
86
87    // Negative-cycle detection: any negative diagonal entry means a cycle.
88    for i in 0..n {
89        if dist[i][i] < 0.0 {
90            return Err(GraphError::NegativeCycle);
91        }
92    }
93
94    Ok(dist)
95}
96
97/// Returns the shortest path (as a sequence of [`NodeId`]s) between two nodes
98/// using a pre-computed Floyd-Warshall distance matrix and next-hop table.
99///
100/// Call [`floyd_warshall_with_paths`] instead of [`floyd_warshall`] if you
101/// need path reconstruction.
102///
103/// # Examples
104///
105/// ```
106/// use graph_core::{AdjacencyList, Graph};
107/// use graph_shortest_path::floyd_warshall::{floyd_warshall_with_paths, reconstruct_fw_path};
108///
109/// let mut g: AdjacencyList<()> = AdjacencyList::directed();
110/// let a = g.add_node(());
111/// let b = g.add_node(());
112/// let c = g.add_node(());
113/// g.add_edge(a, b, 1.0).unwrap();
114/// g.add_edge(b, c, 1.0).unwrap();
115///
116/// let (dist, next) = floyd_warshall_with_paths(&g).unwrap();
117/// assert_eq!(dist[0][2], 2.0);
118///
119/// // Reconstruct path a → c
120/// let path = reconstruct_fw_path(&next, a, c);
121/// assert_eq!(path, Some(vec![a, b, c]));
122/// ```
123#[allow(clippy::needless_range_loop)] // 2-D matrix: dist[i][k], next[i][j] require numeric indices
124pub fn floyd_warshall_with_paths<G>(graph: &G) -> Result<FwResult, GraphError>
125where
126    G: Graph<Weight = f64>,
127{
128    let n = graph.node_count();
129
130    let mut dist = vec![vec![f64::INFINITY; n]; n];
131    // next[i][j] = first hop from i toward j (None if no path).
132    let mut next: Vec<Vec<Option<NodeId>>> = vec![vec![None; n]; n];
133
134    for i in 0..n {
135        dist[i][i] = 0.0;
136    }
137
138    // Seed from edges.
139    for u in graph.nodes() {
140        for (v, &w) in graph.neighbors(u) {
141            let i = u.index();
142            let j = v.index();
143            if w < dist[i][j] {
144                dist[i][j] = w;
145                next[i][j] = Some(v);
146            }
147        }
148    }
149    // Self-hops.
150    for u in graph.nodes() {
151        let i = u.index();
152        next[i][i] = Some(u);
153    }
154
155    for k in 0..n {
156        for i in 0..n {
157            if dist[i][k] == f64::INFINITY {
158                continue;
159            }
160            for j in 0..n {
161                if dist[k][j] == f64::INFINITY {
162                    continue;
163                }
164                let through_k = dist[i][k] + dist[k][j];
165                if through_k < dist[i][j] {
166                    dist[i][j] = through_k;
167                    next[i][j] = next[i][k]; // route i→j via k
168                }
169            }
170        }
171    }
172
173    for i in 0..n {
174        if dist[i][i] < 0.0 {
175            return Err(GraphError::NegativeCycle);
176        }
177    }
178
179    Ok((dist, next))
180}
181
182/// Reconstructs a shortest path using the `next` table from
183/// [`floyd_warshall_with_paths`].
184///
185/// Returns `None` if no path exists from `start` to `end`.
186pub fn reconstruct_fw_path(
187    next: &[Vec<Option<NodeId>>],
188    start: NodeId,
189    end: NodeId,
190) -> Option<Vec<NodeId>> {
191    next[start.index()][end.index()]?;
192
193    let mut path = vec![start];
194    let mut current = start;
195
196    while current != end {
197        current = next[current.index()][end.index()]?;
198        path.push(current);
199        // Guard against infinite loops from negative cycles.
200        if path.len() > next.len() + 1 {
201            return None;
202        }
203    }
204
205    Some(path)
206}