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}