Skip to main content

rust_igraph/algorithms/flow/
gomory_hu_tree.rs

1//! `gomory_hu_tree` (ALGO-FL-020) — Gomory-Hu cut tree.
2//!
3//! Counterpart of `igraph_gomory_hu_tree` in
4//! `references/igraph/src/flow/flow.c:2479-2616`. The Gomory-Hu tree is
5//! a compact representation of the *all-pairs* minimum s-t cut: it is
6//! an undirected weighted tree on the same vertex set as the input
7//! graph, with `V - 1` edges, such that the minimum cut value between
8//! any two vertices `u, v` in the original graph equals the *minimum*
9//! edge weight on the unique tree path from `u` to `v`.
10//!
11//! We use Gusfield's algorithm (1990), which computes the tree with
12//! exactly `V - 1` calls to s-t max-flow / min-cut. Each call pulls a
13//! `(value, source-side-partition)` pair from the FL-018 `st_mincut`
14//! backend and rewrites a parent-pointer array in place; once the
15//! loop finishes, the parent pointers describe the tree edges and the
16//! saved flow values become the tree edge weights.
17//!
18//! # Reference
19//!
20//! Gusfield D., *Very simple methods for all pairs network flow
21//! analysis*. SIAM J Comput 19(1):143-155, 1990.
22//! <https://doi.org/10.1137/0219009>
23
24use crate::core::{Graph, IgraphError, IgraphResult};
25
26use super::st_mincut::st_mincut;
27
28/// Output of [`gomory_hu_tree`] — a weighted undirected tree on the
29/// same vertex set as the input graph, plus one flow value per tree
30/// edge.
31///
32/// The flow vector is indexed by tree edge id in the order they were
33/// added (`tree.edge_endpoints(eid)` gives the corresponding endpoints).
34/// `flows.len() == tree.ecount() == max(vcount - 1, 0)`.
35#[derive(Debug, Clone)]
36pub struct GomoryHuTree {
37    /// Undirected tree with the same vertex count as the input graph
38    /// and exactly `max(vcount - 1, 0)` edges.
39    pub tree: Graph,
40    /// Tree-edge weights, in the same order as the edges of [`tree`].
41    /// Each entry is the value of the s-t min cut between the two
42    /// endpoints of that tree edge in the *original* graph (and, by
43    /// the Gomory-Hu property, the minimum cut value between every
44    /// pair separated by that edge in the tree).
45    ///
46    /// [`tree`]: GomoryHuTree::tree
47    pub flows: Vec<f64>,
48}
49
50/// Gomory-Hu cut tree of an undirected graph (Gusfield 1990).
51///
52/// Returns a [`GomoryHuTree`] whose `tree` field is an undirected
53/// weighted tree on the same vertex set as `graph` with `V - 1` edges,
54/// and whose `flows` field gives one weight per tree edge. The
55/// minimum s-t cut value between any two vertices `u, v` in the
56/// original graph is exactly the *minimum* edge weight along the
57/// unique tree path from `u` to `v`.
58///
59/// # Arguments
60///
61/// * `graph` — input graph. **Must be undirected**; directed graphs
62///   are rejected with [`IgraphError::InvalidArgument`].
63/// * `capacity` — optional per-edge capacity in the graph's edge-id
64///   order. When `None`, each edge contributes unit capacity. When
65///   `Some(c)`, `c.len()` must equal `graph.ecount()`, and every
66///   entry must be finite and `≥ 0` (same contract as
67///   [`crate::st_mincut`]).
68///
69/// # Returns
70///
71/// A [`GomoryHuTree`] with `tree.vcount() == graph.vcount()` and
72/// `flows.len() == tree.ecount() == max(graph.vcount() - 1, 0)`.
73///
74/// For `graph.vcount() == 0` the returned tree is empty and `flows`
75/// is empty. For `graph.vcount() == 1` the tree has one isolated
76/// vertex and `flows` is empty.
77///
78/// # Errors
79///
80/// * [`IgraphError::InvalidArgument`] if `graph` is directed, the
81///   capacity slice length differs from `ecount()`, or any capacity
82///   entry is negative / non-finite.
83///
84/// [`IgraphError::InvalidArgument`]: crate::core::IgraphError::InvalidArgument
85///
86/// # Examples
87///
88/// `K_4` with unit capacities — every pair has min-cut 3 (each vertex's
89/// degree), so every tree edge weight is 3.
90///
91/// ```
92/// use rust_igraph::{Graph, gomory_hu_tree};
93///
94/// let mut k4 = Graph::new(4, false).unwrap();
95/// for u in 0u32..4 {
96///     for v in (u + 1)..4 {
97///         k4.add_edge(u, v).unwrap();
98///     }
99/// }
100///
101/// let gh = gomory_hu_tree(&k4, None).unwrap();
102/// assert_eq!(gh.tree.vcount(), 4);
103/// assert_eq!(gh.tree.ecount(), 3);
104/// assert_eq!(gh.flows.len(), 3);
105/// for &w in &gh.flows {
106///     assert_eq!(w, 3.0);
107/// }
108/// ```
109///
110/// # References
111///
112/// Gusfield D., *Very simple methods for all pairs network flow
113/// analysis*. SIAM J Comput 19(1):143-155, 1990.
114pub fn gomory_hu_tree(graph: &Graph, capacity: Option<&[f64]>) -> IgraphResult<GomoryHuTree> {
115    // flow.c:2533 — Gomory-Hu is defined only on undirected graphs.
116    if graph.is_directed() {
117        return Err(IgraphError::InvalidArgument(
118            "Gomory-Hu tree can only be calculated for undirected graphs".into(),
119        ));
120    }
121
122    let n = graph.vcount();
123
124    // n=0 → empty tree, empty flows. We bypass capacity validation here
125    // because there are no max-flow calls to surface it through; an
126    // empty graph with a non-empty capacity slice is a caller bug, so
127    // we still surface it as InvalidArgument for consistency with FL-002.
128    if n == 0 {
129        if let Some(c) = capacity {
130            if !c.is_empty() {
131                return Err(IgraphError::InvalidArgument(format!(
132                    "capacity length {} does not match edge count 0",
133                    c.len()
134                )));
135            }
136        }
137        return Ok(GomoryHuTree {
138            tree: Graph::new(0, false)?,
139            flows: Vec::new(),
140        });
141    }
142
143    // n=1 → one-vertex tree, empty flows. Same capacity sanity check.
144    if n == 1 {
145        if let Some(c) = capacity {
146            let m = graph.ecount();
147            if c.len() != m {
148                return Err(IgraphError::InvalidArgument(format!(
149                    "capacity length {} does not match edge count {m}",
150                    c.len()
151                )));
152            }
153        }
154        return Ok(GomoryHuTree {
155            tree: Graph::new(1, false)?,
156            flows: Vec::new(),
157        });
158    }
159
160    // Parent-pointer array (`neighbors[i]` = current tree-neighbor of i)
161    // and per-vertex flow values. Both start at 0 — mirroring the
162    // implicit "every edge points to node 0" init at flow.c:2544-2546.
163    let n_usize = n as usize;
164    let mut neighbors: Vec<u32> = vec![0; n_usize];
165    let mut flow_values: Vec<f64> = vec![0.0; n_usize];
166
167    // Main Gusfield loop (flow.c:2549-2581). Capacity validation is
168    // delegated to the first st_mincut call — if the slice is malformed,
169    // that call returns InvalidArgument and we propagate via `?`.
170    for source in 1..n {
171        let target = neighbors[source as usize];
172
173        let cut = st_mincut(graph, source, target, capacity)?;
174        let value = cut.value;
175        flow_values[source as usize] = value;
176
177        // Tree update (flow.c:2568-2580). `partition` is the source-side
178        // vertex set returned by st_mincut, guaranteed to contain
179        // `source` and not contain `target`. For every other vertex
180        // `mid` in `partition` we either re-parent `mid` to `source` or
181        // perform the swap-rotate that keeps the tree consistent when
182        // `target`'s parent crosses into the source side.
183        for &mid in &cut.partition {
184            if mid == source {
185                continue;
186            }
187            let mid_us = mid as usize;
188            let target_us = target as usize;
189            if neighbors[mid_us] == target {
190                neighbors[mid_us] = source;
191            } else if neighbors[target_us] == mid {
192                // flow.c:2573 swap branch: rotate the tree so that
193                // (source, mid) and (target, source) replace
194                // (source, target) and (target, mid). The previous
195                // flow_values[source] = value assignment above is
196                // overwritten by flow_values[target]'s value, and
197                // flow_values[target] receives `value`.
198                neighbors[target_us] = source;
199                neighbors[source as usize] = mid;
200                flow_values[source as usize] = flow_values[target_us];
201                flow_values[target_us] = value;
202            }
203        }
204    }
205
206    // Assemble the output tree (undirected, vcount unchanged, V-1 edges).
207    let mut tree = Graph::new(n, false)?;
208    let mut flows: Vec<f64> = Vec::with_capacity(n_usize - 1);
209    for i in 1..n {
210        // Edge (i, neighbors[i]); both endpoints are valid vertex ids
211        // by construction of `neighbors` (always within [0, n)).
212        tree.add_edge(i, neighbors[i as usize])?;
213        flows.push(flow_values[i as usize]);
214    }
215
216    Ok(GomoryHuTree { tree, flows })
217}
218
219#[cfg(test)]
220mod tests {
221    //! Step-4 sanity tests — the full unit suite (boundary, error
222    //! variants, multigraph, `K_4` issue #1810, …) is filled in at
223    //! Step-5 by `/awu-test`. The cases here are the minimum needed
224    //! to confirm Gusfield's loop produces a correct tree, not a
225    //! comprehensive spec.
226
227    use super::*;
228    use crate::core::IgraphError;
229    use crate::max_flow_value;
230
231    const TOL: f64 = 1e-9;
232
233    /// Validate the Gomory-Hu property: for every `(i, j)` pair, the
234    /// minimum edge weight on the unique tree path equals the s-t
235    /// max-flow in the original graph.
236    fn validate_tree(graph: &Graph, gh: &GomoryHuTree, caps: Option<&[f64]>) {
237        let n = gh.tree.vcount();
238        // Build adjacency of the tree (vertex → (neighbor, flow value)).
239        let mut adj: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n as usize];
240        for eid in 0..gh.tree.ecount() {
241            let eid_u32 = u32::try_from(eid).expect("tree.ecount() within u32 in validate_tree");
242            let (u, v) = gh.tree.edge(eid_u32).expect("tree edge");
243            let weight = gh.flows[eid];
244            adj[u as usize].push((v, weight));
245            adj[v as usize].push((u, weight));
246        }
247        for src in 0..n {
248            for dst in (src + 1)..n {
249                // BFS in the tree from src to dst; the tree is acyclic
250                // so there is a unique path; track the minimum edge
251                // weight along it.
252                let mut parent: Vec<i64> = vec![-1; n as usize];
253                let mut parent_w: Vec<f64> = vec![0.0; n as usize];
254                let mut queue: Vec<u32> = vec![src];
255                let mut head = 0_usize;
256                parent[src as usize] = i64::from(src);
257                while head < queue.len() {
258                    let u = queue[head];
259                    head += 1;
260                    if u == dst {
261                        break;
262                    }
263                    for &(w, weight) in &adj[u as usize] {
264                        if parent[w as usize] < 0 {
265                            parent[w as usize] = i64::from(u);
266                            parent_w[w as usize] = weight;
267                            queue.push(w);
268                        }
269                    }
270                }
271                let mut min_w = f64::INFINITY;
272                let mut cur = dst;
273                while cur != src {
274                    if parent_w[cur as usize] < min_w {
275                        min_w = parent_w[cur as usize];
276                    }
277                    cur = u32::try_from(parent[cur as usize])
278                        .expect("parent within u32 (set by BFS above)");
279                }
280                let mf = max_flow_value(graph, src, dst, caps).expect("max_flow_value");
281                assert!(
282                    (min_w - mf).abs() <= TOL,
283                    "Gomory-Hu property failed for (src={src}, dst={dst}): tree min={min_w}, max-flow={mf}"
284                );
285            }
286        }
287    }
288
289    #[test]
290    fn empty_graph_returns_empty_tree() {
291        let g = Graph::new(0, false).expect("graph");
292        let gh = gomory_hu_tree(&g, None).expect("gh");
293        assert_eq!(gh.tree.vcount(), 0);
294        assert_eq!(gh.tree.ecount(), 0);
295        assert!(gh.flows.is_empty());
296    }
297
298    #[test]
299    fn single_vertex_returns_one_vertex_tree() {
300        let g = Graph::new(1, false).expect("graph");
301        let gh = gomory_hu_tree(&g, None).expect("gh");
302        assert_eq!(gh.tree.vcount(), 1);
303        assert_eq!(gh.tree.ecount(), 0);
304        assert!(gh.flows.is_empty());
305    }
306
307    #[test]
308    fn rejects_directed_graph() {
309        let mut g = Graph::new(3, true).expect("graph");
310        g.add_edge(0, 1).expect("edge");
311        let err = gomory_hu_tree(&g, None).expect_err("must reject directed");
312        match err {
313            IgraphError::InvalidArgument(_) => {}
314            other => panic!("expected InvalidArgument, got {other:?}"),
315        }
316    }
317
318    #[test]
319    fn six_vertex_weighted_matches_c_reference() {
320        // From references/igraph/tests/unit/igraph_gomory_hu_tree.c —
321        // edges (0,1)(0,2)(1,2)(1,3)(1,4)(2,4)(3,4)(3,5)(4,5),
322        // caps [1, 7, 1, 3, 2, 4, 1, 6, 2]. Validate via the
323        // all-pairs Gomory-Hu property.
324        let mut g = Graph::new(6, false).expect("graph");
325        let edges = [
326            (0u32, 1u32),
327            (0, 2),
328            (1, 2),
329            (1, 3),
330            (1, 4),
331            (2, 4),
332            (3, 4),
333            (3, 5),
334            (4, 5),
335        ];
336        for (u, v) in edges {
337            g.add_edge(u, v).expect("edge");
338        }
339        let caps = [1.0, 7.0, 1.0, 3.0, 2.0, 4.0, 1.0, 6.0, 2.0];
340        let gh = gomory_hu_tree(&g, Some(&caps)).expect("gh");
341        assert_eq!(gh.tree.vcount(), 6);
342        assert_eq!(gh.tree.ecount(), 5);
343        assert_eq!(gh.flows.len(), 5);
344        validate_tree(&g, &gh, Some(&caps));
345    }
346
347    #[test]
348    fn k4_unit_capacities_matches_c_reference() {
349        // K_4 unit caps (regression for issue #1810). Every pair has
350        // min cut = 3, so every tree edge weight = 3.
351        let mut g = Graph::new(4, false).expect("graph");
352        for i in 0..4u32 {
353            for j in (i + 1)..4u32 {
354                g.add_edge(i, j).expect("edge");
355            }
356        }
357        let gh = gomory_hu_tree(&g, None).expect("gh");
358        assert_eq!(gh.tree.vcount(), 4);
359        assert_eq!(gh.tree.ecount(), 3);
360        for &w in &gh.flows {
361            assert!((w - 3.0).abs() <= TOL, "expected weight 3, got {w}");
362        }
363        validate_tree(&g, &gh, None);
364    }
365
366    #[test]
367    fn two_vertices_no_edge_zero_flow() {
368        // Disconnected pair → only tree edge, weight 0.
369        let g = Graph::new(2, false).expect("graph");
370        let gh = gomory_hu_tree(&g, None).expect("gh");
371        assert_eq!(gh.tree.vcount(), 2);
372        assert_eq!(gh.tree.ecount(), 1);
373        assert!(gh.flows[0].abs() <= TOL);
374    }
375
376    #[test]
377    fn two_vertices_one_edge_unit_flow() {
378        let mut g = Graph::new(2, false).expect("graph");
379        g.add_edge(0, 1).expect("edge");
380        let gh = gomory_hu_tree(&g, None).expect("gh");
381        assert_eq!(gh.tree.ecount(), 1);
382        assert!((gh.flows[0] - 1.0).abs() <= TOL);
383        validate_tree(&g, &gh, None);
384    }
385
386    #[test]
387    fn two_vertices_parallel_edges_multigraph() {
388        // Multigraph: parallel edges raise the min cut.
389        let mut g = Graph::new(2, false).expect("graph");
390        g.add_edge(0, 1).expect("edge");
391        g.add_edge(0, 1).expect("edge");
392        g.add_edge(0, 1).expect("edge");
393        let gh = gomory_hu_tree(&g, None).expect("gh");
394        assert!((gh.flows[0] - 3.0).abs() <= TOL);
395        validate_tree(&g, &gh, None);
396    }
397
398    #[test]
399    fn path_5v_unit_caps_all_pairs_one() {
400        // Path 0-1-2-3-4 has min cut 1 for every pair (the cheapest
401        // single edge disconnects them), so every tree edge weight = 1.
402        let mut g = Graph::new(5, false).expect("graph");
403        for i in 0..4u32 {
404            g.add_edge(i, i + 1).expect("edge");
405        }
406        let gh = gomory_hu_tree(&g, None).expect("gh");
407        assert_eq!(gh.tree.ecount(), 4);
408        for &w in &gh.flows {
409            assert!((w - 1.0).abs() <= TOL, "expected 1.0, got {w}");
410        }
411        validate_tree(&g, &gh, None);
412    }
413
414    #[test]
415    fn ring_5v_unit_caps_all_pairs_two() {
416        // Cycle 0-1-2-3-4-0: every pair has min cut 2 (must remove
417        // two edges to disconnect).
418        let mut g = Graph::new(5, false).expect("graph");
419        for i in 0..5u32 {
420            g.add_edge(i, (i + 1) % 5).expect("edge");
421        }
422        let gh = gomory_hu_tree(&g, None).expect("gh");
423        for &w in &gh.flows {
424            assert!((w - 2.0).abs() <= TOL, "expected 2.0, got {w}");
425        }
426        validate_tree(&g, &gh, None);
427    }
428
429    #[test]
430    fn disconnected_two_components_zero_cross_flows() {
431        // 0-1 and 2-3, no cross edges. min cut(0,2) = 0, etc. The tree
432        // must still be a single tree on 4 vertices (Gusfield always
433        // produces one tree regardless of connectivity); some weights
434        // will be 0.
435        let mut g = Graph::new(4, false).expect("graph");
436        g.add_edge(0, 1).expect("edge");
437        g.add_edge(2, 3).expect("edge");
438        let gh = gomory_hu_tree(&g, None).expect("gh");
439        assert_eq!(gh.tree.vcount(), 4);
440        assert_eq!(gh.tree.ecount(), 3);
441        validate_tree(&g, &gh, None);
442    }
443
444    #[test]
445    fn weighted_bridge_dominates_min_cut() {
446        // Two triangles 0-1-2 and 3-4-5 joined by a single weak edge
447        // (2,3) of capacity 0.5. min cut(any in {0,1,2}, any in {3,4,5})
448        // is 0.5 — the bridge. Tree must contain that bridge with
449        // weight 0.5; all other tree edges must have weight ≥ 0.5.
450        let mut g = Graph::new(6, false).expect("graph");
451        for (u, v) in [(0u32, 1u32), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3), (2, 3)] {
452            g.add_edge(u, v).expect("edge");
453        }
454        let caps = [10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 0.5];
455        let gh = gomory_hu_tree(&g, Some(&caps)).expect("gh");
456        let min_flow = gh.flows.iter().copied().fold(f64::INFINITY, f64::min);
457        assert!(
458            (min_flow - 0.5).abs() <= TOL,
459            "min flow should be 0.5, got {min_flow}"
460        );
461        validate_tree(&g, &gh, Some(&caps));
462    }
463
464    #[test]
465    fn capacity_wrong_length_errors() {
466        let mut g = Graph::new(3, false).expect("graph");
467        g.add_edge(0, 1).expect("edge");
468        g.add_edge(1, 2).expect("edge");
469        let bad = vec![1.0_f64; 99];
470        let err = gomory_hu_tree(&g, Some(&bad)).expect_err("must err");
471        assert!(matches!(err, IgraphError::InvalidArgument(_)));
472    }
473
474    #[test]
475    fn capacity_negative_errors() {
476        let mut g = Graph::new(3, false).expect("graph");
477        g.add_edge(0, 1).expect("edge");
478        g.add_edge(1, 2).expect("edge");
479        let caps = [1.0_f64, -0.5];
480        let err = gomory_hu_tree(&g, Some(&caps)).expect_err("must err");
481        assert!(matches!(err, IgraphError::InvalidArgument(_)));
482    }
483
484    #[test]
485    fn capacity_nan_errors() {
486        let mut g = Graph::new(3, false).expect("graph");
487        g.add_edge(0, 1).expect("edge");
488        g.add_edge(1, 2).expect("edge");
489        let caps = [1.0_f64, f64::NAN];
490        let err = gomory_hu_tree(&g, Some(&caps)).expect_err("must err");
491        assert!(matches!(err, IgraphError::InvalidArgument(_)));
492    }
493
494    #[test]
495    fn empty_graph_rejects_nonempty_capacity() {
496        let g = Graph::new(0, false).expect("graph");
497        let caps = [1.0_f64];
498        let err = gomory_hu_tree(&g, Some(&caps)).expect_err("must err");
499        assert!(matches!(err, IgraphError::InvalidArgument(_)));
500    }
501
502    #[test]
503    fn unit_caps_match_explicit_unit_capacity_vector() {
504        // None should produce the same tree topology + weights as a
505        // fully-unit capacity vector. (Order of edges may differ, but
506        // both must satisfy validate_tree against the same graph, and
507        // the multisets of weights must match.)
508        let mut g = Graph::new(5, false).expect("graph");
509        for i in 0..5u32 {
510            for j in (i + 1)..5u32 {
511                g.add_edge(i, j).expect("edge");
512            }
513        }
514        let gh_none = gomory_hu_tree(&g, None).expect("gh_none");
515        let caps = vec![1.0_f64; g.ecount()];
516        let gh_unit = gomory_hu_tree(&g, Some(&caps)).expect("gh_unit");
517        let mut a = gh_none.flows.clone();
518        let mut b = gh_unit.flows.clone();
519        a.sort_by(|x, y| x.partial_cmp(y).expect("no NaN"));
520        b.sort_by(|x, y| x.partial_cmp(y).expect("no NaN"));
521        for (x, y) in a.iter().zip(b.iter()) {
522            assert!((x - y).abs() <= TOL, "weight mismatch: {x} vs {y}");
523        }
524    }
525
526    #[test]
527    fn signature_symbol_is_stable() {
528        // Frozen signature guard — prevents accidental signature drift.
529        const SYMBOL: fn(&Graph, Option<&[f64]>) -> IgraphResult<GomoryHuTree> = gomory_hu_tree;
530        let _ = SYMBOL;
531    }
532}
533
534#[cfg(all(test, feature = "proptest-harness"))]
535mod proptests {
536    //! Proptest invariants for Gomory-Hu tree:
537    //!
538    //! 1. **Shape**: `tree.vcount() == graph.vcount()`,
539    //!    `tree.ecount() == max(vcount - 1, 0)`, `flows.len() ==
540    //!    tree.ecount()`, and every flow value is finite and `≥ 0`.
541    //! 2. **Gomory-Hu property**: for every `(i, j)` pair, the minimum
542    //!    edge weight on the unique tree path equals
543    //!    `max_flow_value(graph, i, j, capacity)` within tolerance.
544    //! 3. **Tree connectivity**: the produced tree is acyclic and (for
545    //!    `vcount ≥ 1`) connected — a BFS from vertex 0 reaches all
546    //!    vertices.
547    //! 4. **Unit-caps parity**: `gomory_hu_tree(g, None)` and
548    //!    `gomory_hu_tree(g, Some(unit caps))` produce equal flow
549    //!    multisets.
550
551    use super::*;
552    use crate::max_flow_value;
553    use proptest::prelude::*;
554
555    const TOL: f64 = 1e-9;
556
557    fn xorshift(mut r: u64) -> u64 {
558        r ^= r << 13;
559        r ^= r >> 7;
560        r ^= r << 17;
561        r
562    }
563
564    fn build_random_undirected(seed: u64, n: u32, m_max: u32) -> Graph {
565        let mut g = Graph::new(n, false).expect("graph");
566        let mut state = seed | 1;
567        for _ in 0..m_max {
568            state = xorshift(state);
569            let u = u32::try_from(state % u64::from(n)).expect("modulo fits");
570            state = xorshift(state);
571            let v = u32::try_from(state % u64::from(n)).expect("modulo fits");
572            if u == v {
573                continue;
574            }
575            g.add_edge(u, v).expect("edge");
576        }
577        g
578    }
579
580    fn tree_min_weight_on_path(gh: &GomoryHuTree, src: u32, dst: u32) -> f64 {
581        let n = gh.tree.vcount();
582        let mut adj: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n as usize];
583        for eid in 0..gh.tree.ecount() {
584            let eid_u32 = u32::try_from(eid).expect("ecount fits in u32");
585            let (u, v) = gh.tree.edge(eid_u32).expect("edge");
586            let w = gh.flows[eid];
587            adj[u as usize].push((v, w));
588            adj[v as usize].push((u, w));
589        }
590        let mut parent: Vec<i64> = vec![-1; n as usize];
591        let mut parent_w: Vec<f64> = vec![0.0; n as usize];
592        let mut queue: Vec<u32> = vec![src];
593        let mut head = 0_usize;
594        parent[src as usize] = i64::from(src);
595        while head < queue.len() {
596            let u = queue[head];
597            head += 1;
598            if u == dst {
599                break;
600            }
601            for &(w, weight) in &adj[u as usize] {
602                if parent[w as usize] < 0 {
603                    parent[w as usize] = i64::from(u);
604                    parent_w[w as usize] = weight;
605                    queue.push(w);
606                }
607            }
608        }
609        let mut min_w = f64::INFINITY;
610        let mut cur = dst;
611        while cur != src {
612            if parent_w[cur as usize] < min_w {
613                min_w = parent_w[cur as usize];
614            }
615            cur = u32::try_from(parent[cur as usize]).expect("parent within u32");
616        }
617        min_w
618    }
619
620    proptest! {
621        #[test]
622        fn shape_invariants(
623            seed in any::<u64>(),
624            n in 1u32..7,
625            m in 0u32..12,
626        ) {
627            let g = build_random_undirected(seed, n, m);
628            let gh = gomory_hu_tree(&g, None).expect("gh");
629            prop_assert_eq!(gh.tree.vcount(), n);
630            prop_assert_eq!(gh.tree.ecount(), (n - 1) as usize);
631            prop_assert_eq!(gh.flows.len(), (n - 1) as usize);
632            for &w in &gh.flows {
633                prop_assert!(w.is_finite(), "flow {w} not finite");
634                prop_assert!(w >= 0.0, "flow {w} negative");
635            }
636        }
637
638        #[test]
639        fn gomory_hu_property_holds(
640            seed in any::<u64>(),
641            n in 2u32..6,
642            m in 0u32..10,
643        ) {
644            let g = build_random_undirected(seed, n, m);
645            let gh = gomory_hu_tree(&g, None).expect("gh");
646            for src in 0..n {
647                for dst in (src + 1)..n {
648                    let tree_min = tree_min_weight_on_path(&gh, src, dst);
649                    let mf = max_flow_value(&g, src, dst, None).expect("mf");
650                    prop_assert!(
651                        (tree_min - mf).abs() <= TOL,
652                        "Gomory-Hu property violated for (src={}, dst={}): tree min={}, max-flow={}, seed={}",
653                        src, dst, tree_min, mf, seed
654                    );
655                }
656            }
657        }
658
659        #[test]
660        fn tree_is_connected(
661            seed in any::<u64>(),
662            n in 1u32..7,
663            m in 0u32..12,
664        ) {
665            let g = build_random_undirected(seed, n, m);
666            let gh = gomory_hu_tree(&g, None).expect("gh");
667            // BFS from vertex 0 — should reach every other vertex
668            // because the tree has n-1 edges and is connected by
669            // construction.
670            let mut seen = vec![false; n as usize];
671            seen[0] = true;
672            let mut queue: Vec<u32> = vec![0];
673            let mut head = 0_usize;
674            let mut adj: Vec<Vec<u32>> = vec![Vec::new(); n as usize];
675            for eid in 0..gh.tree.ecount() {
676                let eid_u32 = u32::try_from(eid).expect("ecount fits");
677                let (u, v) = gh.tree.edge(eid_u32).expect("edge");
678                adj[u as usize].push(v);
679                adj[v as usize].push(u);
680            }
681            while head < queue.len() {
682                let v = queue[head];
683                head += 1;
684                for &w in &adj[v as usize] {
685                    if !seen[w as usize] {
686                        seen[w as usize] = true;
687                        queue.push(w);
688                    }
689                }
690            }
691            for (v, &s) in seen.iter().enumerate() {
692                prop_assert!(s, "vertex {v} not reachable from 0 in the tree (seed={seed})");
693            }
694        }
695
696        #[test]
697        fn unit_caps_parity(
698            seed in any::<u64>(),
699            n in 1u32..6,
700            m in 0u32..10,
701        ) {
702            let g = build_random_undirected(seed, n, m);
703            let caps = vec![1.0_f64; g.ecount()];
704            let gh_none = gomory_hu_tree(&g, None).expect("gh_none");
705            let gh_unit = gomory_hu_tree(&g, Some(&caps)).expect("gh_unit");
706            let mut a = gh_none.flows.clone();
707            let mut b = gh_unit.flows.clone();
708            a.sort_by(|x, y| x.partial_cmp(y).expect("no NaN in a"));
709            b.sort_by(|x, y| x.partial_cmp(y).expect("no NaN in b"));
710            prop_assert_eq!(a.len(), b.len());
711            for (x, y) in a.iter().zip(b.iter()) {
712                prop_assert!((x - y).abs() <= TOL, "weight mismatch: {} vs {}", x, y);
713            }
714        }
715    }
716}