Skip to main content

rust_igraph/algorithms/paths/
floyd_warshall.rs

1//! Floyd-Warshall all-pairs shortest distances (ALGO-SP-004).
2//!
3//! Counterpart of `igraph_distances_floyd_warshall()` from
4//! `references/igraph/src/paths/floyd_warshall.c`. Phase-1 minimal
5//! slice: returns the dense distance matrix only (no path
6//! reconstruction), `IGRAPH_OUT` mode on directed graphs, undirected
7//! graphs walk both endpoints symmetrically. Implements the textbook
8//! O(V³) variant; the Brodnik-tree speedup ships later.
9//!
10//! Negative weights are accepted on directed graphs except for self-
11//! loops (a self-loop with `w < 0` is itself a negative cycle).
12//! Undirected graphs reject `w < 0` outright because every undirected
13//! edge would form a 2-cycle of weight `2w < 0`. After relaxation,
14//! `dist[i][i] < 0` triggers a negative-cycle error.
15//!
16//! `+inf` weights are silently ignored (matches igraph C).
17
18use crate::core::{Graph, IgraphError, IgraphResult};
19
20/// All-pairs shortest distances via Floyd-Warshall.
21///
22/// `weights = None` collapses to unit weights and reproduces unweighted
23/// BFS distances (modulo cost: BFS is O(V·(V+E)) vs FW's O(V³)). When
24/// `weights = Some(w)`, `w.len()` must equal `graph.ecount()`.
25///
26/// Returns a `vcount × vcount` matrix where `out[i][j] = Some(d)` if
27/// there is a path `i → j` of weighted length `d`, and `None` if `j`
28/// is unreachable from `i`. The diagonal is always `Some(0.0)` (a
29/// non-trivial cycle through `i` of total weight `0` is *not*
30/// surfaced — this matches igraph's C contract; only *negative* cycles
31/// are flagged as errors).
32///
33/// Errors:
34/// - `weights.len() != ecount` → `InvalidArgument`.
35/// - any `w == NaN` → `InvalidArgument`.
36/// - undirected graph with any `w < 0` → `InvalidArgument` (negative
37///   2-cycle).
38/// - directed graph with self-loop of `w < 0` → `InvalidArgument`.
39/// - negative cycle detected during relaxation → `InvalidArgument`.
40///
41/// # Examples
42///
43/// ```
44/// use rust_igraph::{Graph, floyd_warshall_distances};
45///
46/// // Triangle 0-1-2 with weights 1, 4, 2 → dist(0,2) = 1+2 = 3.
47/// let mut g = Graph::with_vertices(3);
48/// g.add_edge(0, 1).unwrap();
49/// g.add_edge(0, 2).unwrap();
50/// g.add_edge(1, 2).unwrap();
51/// let d = floyd_warshall_distances(&g, Some(&[1.0, 4.0, 2.0])).unwrap();
52/// assert_eq!(d[0][2], Some(3.0));
53/// assert_eq!(d[0][0], Some(0.0));
54/// ```
55pub fn floyd_warshall_distances(
56    graph: &Graph,
57    weights: Option<&[f64]>,
58) -> IgraphResult<Vec<Vec<Option<f64>>>> {
59    let n = graph.vcount();
60    let m = graph.ecount();
61    let n_us = n as usize;
62    let directed = graph.is_directed();
63
64    if let Some(ws) = weights {
65        if ws.len() != m {
66            return Err(IgraphError::InvalidArgument(format!(
67                "weights vector size ({}) differs from edge count ({})",
68                ws.len(),
69                m
70            )));
71        }
72        for (e, &w) in ws.iter().enumerate() {
73            if w.is_nan() {
74                return Err(IgraphError::InvalidArgument(format!(
75                    "weight at edge {e} is NaN"
76                )));
77            }
78        }
79    }
80
81    // Flat n×n row-major matrix with f64::INFINITY sentinel for "no
82    // edge yet" so `min(d[i][k] + d[k][j], d[i][j])` falls back
83    // cleanly. Diagonal seeded to 0.
84    let mut dist = vec![f64::INFINITY; n_us * n_us];
85    for v in 0..n_us {
86        dist[v * n_us + v] = 0.0;
87    }
88
89    // Seed direct edges (taking the minimum across multi-edges).
90    let m_u32 = u32::try_from(m).map_err(|_| IgraphError::Internal("edge count overflows u32"))?;
91    for eid in 0..m_u32 {
92        let (src, tgt) = graph.edge(eid)?;
93        let edge_w = weights.map_or(1.0, |ws| ws[eid as usize]);
94
95        if edge_w < 0.0 {
96            if !directed {
97                return Err(IgraphError::InvalidArgument(format!(
98                    "negative edge weight ({edge_w}) on undirected graph: every undirected edge induces a 2-cycle, so this is a negative cycle"
99                )));
100            }
101            if src == tgt {
102                return Err(IgraphError::InvalidArgument(format!(
103                    "self-loop with negative weight ({edge_w}) at vertex {src} is a negative cycle"
104                )));
105            }
106        }
107        if edge_w == f64::INFINITY {
108            // Match igraph C: ignore +inf edges.
109            continue;
110        }
111
112        let s_us = src as usize;
113        let t_us = tgt as usize;
114        // OUT mode: forward only on directed; both ways on undirected.
115        let fwd = s_us * n_us + t_us;
116        if edge_w < dist[fwd] {
117            dist[fwd] = edge_w;
118        }
119        if !directed {
120            let rev = t_us * n_us + s_us;
121            if edge_w < dist[rev] {
122                dist[rev] = edge_w;
123            }
124        }
125    }
126
127    if n_us > 1 {
128        // Textbook Floyd-Warshall: relax via every intermediate `k`.
129        for k in 0..n_us {
130            for i in 0..n_us {
131                let dik = dist[i * n_us + k];
132                if dik.is_infinite() {
133                    continue;
134                }
135                for j in 0..n_us {
136                    let dkj = dist[k * n_us + j];
137                    if dkj.is_infinite() {
138                        continue;
139                    }
140                    let candidate = dik + dkj;
141                    let cell = i * n_us + j;
142                    if candidate < dist[cell] {
143                        dist[cell] = candidate;
144                    }
145                }
146                let diag = i * n_us + i;
147                if dist[diag] < 0.0 {
148                    return Err(IgraphError::InvalidArgument(format!(
149                        "negative cycle found while computing Floyd-Warshall distances (vertex {i} closes a cycle of weight {})",
150                        dist[diag]
151                    )));
152                }
153            }
154        }
155    }
156
157    let mut out = Vec::with_capacity(n_us);
158    for i in 0..n_us {
159        let mut row = Vec::with_capacity(n_us);
160        for j in 0..n_us {
161            let d = dist[i * n_us + j];
162            row.push(if d.is_infinite() { None } else { Some(d) });
163        }
164        out.push(row);
165    }
166    Ok(out)
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    fn make_directed(n: u32) -> Graph {
174        Graph::new(n, true).expect("directed graph")
175    }
176
177    #[test]
178    fn empty_graph_returns_empty_matrix() {
179        let g = Graph::with_vertices(0);
180        let d = floyd_warshall_distances(&g, None).unwrap();
181        assert!(d.is_empty());
182    }
183
184    #[test]
185    fn singleton_distance_zero() {
186        let g = Graph::with_vertices(1);
187        let d = floyd_warshall_distances(&g, None).unwrap();
188        assert_eq!(d, vec![vec![Some(0.0)]]);
189    }
190
191    #[test]
192    fn unweighted_triangle_unit_distances() {
193        let mut g = Graph::with_vertices(3);
194        g.add_edge(0, 1).unwrap();
195        g.add_edge(1, 2).unwrap();
196        g.add_edge(0, 2).unwrap();
197        let d = floyd_warshall_distances(&g, None).unwrap();
198        for (i, row) in d.iter().enumerate() {
199            assert_eq!(row[i], Some(0.0));
200            for (j, cell) in row.iter().enumerate() {
201                if i != j {
202                    assert_eq!(*cell, Some(1.0));
203                }
204            }
205        }
206    }
207
208    #[test]
209    fn weighted_triangle_picks_shortcut() {
210        let mut g = Graph::with_vertices(3);
211        g.add_edge(0, 1).unwrap(); // weight 1
212        g.add_edge(0, 2).unwrap(); // weight 4
213        g.add_edge(1, 2).unwrap(); // weight 2
214        let d = floyd_warshall_distances(&g, Some(&[1.0, 4.0, 2.0])).unwrap();
215        assert_eq!(d[0][2], Some(3.0)); // 0→1→2 cheaper than direct
216        assert_eq!(d[2][0], Some(3.0)); // undirected → symmetric
217    }
218
219    #[test]
220    fn unreachable_pair_yields_none() {
221        let mut g = Graph::with_vertices(3);
222        g.add_edge(0, 1).unwrap();
223        let d = floyd_warshall_distances(&g, None).unwrap();
224        assert_eq!(d[0][2], None);
225        assert_eq!(d[2][0], None);
226        assert_eq!(d[1][2], None);
227    }
228
229    #[test]
230    fn directed_graph_respects_orientation() {
231        let mut g = make_directed(3);
232        g.add_edge(0, 1).unwrap();
233        g.add_edge(1, 2).unwrap();
234        let d = floyd_warshall_distances(&g, None).unwrap();
235        assert_eq!(d[0][2], Some(2.0));
236        assert_eq!(d[2][0], None); // backward unreachable
237    }
238
239    #[test]
240    fn directed_graph_with_negative_weight() {
241        // Directed weights can be negative if they don't induce a cycle.
242        let mut g = make_directed(3);
243        g.add_edge(0, 1).unwrap();
244        g.add_edge(1, 2).unwrap();
245        let d = floyd_warshall_distances(&g, Some(&[-5.0, 10.0])).unwrap();
246        assert_eq!(d[0][2], Some(5.0));
247        assert_eq!(d[0][1], Some(-5.0));
248    }
249
250    #[test]
251    fn directed_negative_cycle_errors() {
252        // 0→1→2→0 with weights -1, -1, -1 is a negative cycle.
253        let mut g = make_directed(3);
254        g.add_edge(0, 1).unwrap();
255        g.add_edge(1, 2).unwrap();
256        g.add_edge(2, 0).unwrap();
257        let result = floyd_warshall_distances(&g, Some(&[-1.0, -1.0, -1.0]));
258        assert!(result.is_err());
259    }
260
261    #[test]
262    fn directed_self_loop_negative_errors() {
263        let mut g = make_directed(2);
264        g.add_edge(0, 0).unwrap();
265        let result = floyd_warshall_distances(&g, Some(&[-0.5]));
266        assert!(result.is_err());
267    }
268
269    #[test]
270    fn undirected_negative_weight_rejected() {
271        let mut g = Graph::with_vertices(2);
272        g.add_edge(0, 1).unwrap();
273        let result = floyd_warshall_distances(&g, Some(&[-1.0]));
274        assert!(result.is_err());
275    }
276
277    #[test]
278    fn weights_size_mismatch_errors() {
279        let mut g = Graph::with_vertices(2);
280        g.add_edge(0, 1).unwrap();
281        let result = floyd_warshall_distances(&g, Some(&[1.0, 2.0]));
282        assert!(result.is_err());
283    }
284
285    #[test]
286    fn nan_weight_errors() {
287        let mut g = Graph::with_vertices(2);
288        g.add_edge(0, 1).unwrap();
289        let result = floyd_warshall_distances(&g, Some(&[f64::NAN]));
290        assert!(result.is_err());
291    }
292
293    #[test]
294    fn infinity_weight_silently_ignored() {
295        // 0—1 with weight inf, 0—2 with weight 1, 1—2 with weight 1.
296        // Without the inf edge, 0→1 must route via 2: dist(0,1) = 2.
297        let mut g = Graph::with_vertices(3);
298        g.add_edge(0, 1).unwrap();
299        g.add_edge(0, 2).unwrap();
300        g.add_edge(1, 2).unwrap();
301        let d = floyd_warshall_distances(&g, Some(&[f64::INFINITY, 1.0, 1.0])).unwrap();
302        assert_eq!(d[0][1], Some(2.0));
303    }
304
305    #[test]
306    fn parallel_edges_pick_minimum() {
307        // Two 0—1 edges with weights 5 and 1; FW must pick the lighter.
308        let mut g = Graph::with_vertices(2);
309        g.add_edge(0, 1).unwrap();
310        g.add_edge(0, 1).unwrap();
311        let d = floyd_warshall_distances(&g, Some(&[5.0, 1.0])).unwrap();
312        assert_eq!(d[0][1], Some(1.0));
313    }
314
315    #[test]
316    fn diagonal_always_zero_for_isolated_vertex() {
317        let g = Graph::with_vertices(5);
318        let d = floyd_warshall_distances(&g, None).unwrap();
319        for (i, row) in d.iter().enumerate() {
320            assert_eq!(row[i], Some(0.0));
321            for (j, cell) in row.iter().enumerate() {
322                if i != j {
323                    assert_eq!(*cell, None);
324                }
325            }
326        }
327    }
328
329    #[test]
330    fn unit_weights_match_unweighted() {
331        let mut g = Graph::with_vertices(4);
332        g.add_edge(0, 1).unwrap();
333        g.add_edge(1, 2).unwrap();
334        g.add_edge(2, 3).unwrap();
335        g.add_edge(0, 3).unwrap();
336        let unweighted = floyd_warshall_distances(&g, None).unwrap();
337        let weighted = floyd_warshall_distances(&g, Some(&[1.0, 1.0, 1.0, 1.0])).unwrap();
338        assert_eq!(unweighted, weighted);
339    }
340}