Skip to main content

oxicuda_graphalg/flow/
gomory_hu.rs

1//! Gomory-Hu cut tree (Gusfield's 1990 simplified construction).
2//!
3//! # Overview
4//!
5//! Given an **undirected** weighted graph `G` on `n` vertices, the *Gomory-Hu tree*
6//! is a weighted tree `T` on the same vertex set with exactly `n − 1` edges such that
7//! for **every** pair of vertices `(s, t)`, the minimum-weight edge on the unique
8//! `s`–`t` path in `T` equals the value of a minimum `s`–`t` cut in `G`.
9//!
10//! The classical Gomory-Hu construction (1961) requires graph contractions. Gusfield
11//! (1990, *Very Simple Methods for All Pairs Network Flow Analysis*) showed the same
12//! tree can be built with `n − 1` max-flow computations performed **directly on the
13//! original graph** — no contractions, no Steiner trees. That is what we implement here.
14//!
15//! ## Gusfield's algorithm
16//!
17//! ```text
18//! parent[i] ← 0          for all i in 1..n        (tree rooted at vertex 0)
19//! weight[i] ← 0          for all i
20//! for s in 1..n:
21//!     t ← parent[s]
22//!     (f, S) ← min-cut(s, t)          # S = side of the cut containing s
23//!     weight[s] ← f
24//!     for v in 0..n:                  # re-parent vertices that fell on s's side
25//!         if v ≠ s and v in S and parent[v] == t:
26//!             parent[v] ← s
27//!     if parent[t] in S:              # Gusfield's extra re-parent step
28//!         parent[s] ← parent[t]
29//!         parent[t] ← s
30//!         weight[s] ← weight[t]
31//!         weight[t] ← f
32//! ```
33//!
34//! The resulting tree is encoded by the `(parent, weight)` arrays: for `i ≥ 1` there is
35//! a tree edge `{i, parent[i]}` of weight `weight[i]`. Vertex `0` is the root.
36//!
37//! Min-cuts are obtained by reusing the crate's [`min_cut_from_max_flow`], which runs
38//! Edmonds-Karp on a symmetric [`FlowNetwork`] and returns both the cut value and the
39//! source-side vertex set.
40
41use crate::error::{GraphalgError, GraphalgResult};
42use crate::max_flow::edmonds_karp::FlowNetwork;
43use crate::max_flow::min_cut::min_cut_from_max_flow;
44
45/// A Gomory-Hu cut tree for an undirected weighted graph.
46///
47/// The tree is rooted at vertex `0`. For every non-root vertex `i`, `parent[i]` is its
48/// parent in the tree and `weight[i]` is the weight of the tree edge `{i, parent[i]}`,
49/// which equals the minimum `i`–`parent[i]` cut value in the original graph.
50#[derive(Debug, Clone)]
51pub struct GomoryHuTree {
52    /// Number of vertices (same as the input graph).
53    pub n: usize,
54    /// `parent[i]` = parent of vertex `i` in the tree; `parent[0] == 0` (the root).
55    pub parent: Vec<usize>,
56    /// `weight[i]` = weight of the tree edge `{i, parent[i]}`; `weight[0] == 0.0`.
57    pub weight: Vec<f64>,
58}
59
60impl GomoryHuTree {
61    /// The `n − 1` undirected tree edges as `(u, v, weight)` triples (`u < v`).
62    pub fn tree_edges(&self) -> Vec<(usize, usize, f64)> {
63        let mut edges = Vec::with_capacity(self.n.saturating_sub(1));
64        for i in 1..self.n {
65            let u = i.min(self.parent[i]);
66            let v = i.max(self.parent[i]);
67            edges.push((u, v, self.weight[i]));
68        }
69        edges
70    }
71
72    /// Minimum `s`–`t` cut value, read off as the lightest edge on the tree path
73    /// from `s` to `t`.
74    ///
75    /// Equal vertices yield `0.0`. Two vertices in different tree components (which can
76    /// only happen if the original graph is disconnected, producing one or more
77    /// zero-weight tree edges) also yield a `0.0` minimum on the path.
78    ///
79    /// # Errors
80    /// - [`GraphalgError::IndexOutOfBounds`] if `s` or `t` is not a valid vertex.
81    pub fn min_cut(&self, s: usize, t: usize) -> GraphalgResult<f64> {
82        if s >= self.n {
83            return Err(GraphalgError::IndexOutOfBounds {
84                index: s,
85                len: self.n,
86            });
87        }
88        if t >= self.n {
89            return Err(GraphalgError::IndexOutOfBounds {
90                index: t,
91                len: self.n,
92            });
93        }
94        if s == t {
95            return Ok(0.0);
96        }
97
98        // Depth of each node from the root, plus its parent, lets us walk both endpoints
99        // up to their lowest common ancestor while tracking the minimum edge weight.
100        let depth = self.depths();
101        let (mut a, mut b) = (s, t);
102        let mut best = f64::INFINITY;
103
104        // Bring the deeper node up first.
105        while depth[a] > depth[b] {
106            best = best.min(self.weight[a]);
107            a = self.parent[a];
108        }
109        while depth[b] > depth[a] {
110            best = best.min(self.weight[b]);
111            b = self.parent[b];
112        }
113        // Now ascend together until they meet at the LCA.
114        while a != b {
115            best = best.min(self.weight[a]);
116            a = self.parent[a];
117            best = best.min(self.weight[b]);
118            b = self.parent[b];
119        }
120        Ok(best)
121    }
122
123    /// Depth (number of edges to the root) of every vertex.
124    fn depths(&self) -> Vec<usize> {
125        let mut depth = vec![0usize; self.n];
126        // The parent of any node has a strictly smaller index in Gusfield's tree is
127        // *not* guaranteed, so resolve depths by repeatedly walking to the root. With
128        // memoization this is linear overall.
129        let mut resolved = vec![false; self.n];
130        resolved[0] = true;
131        for start in 1..self.n {
132            // Collect the chain up to the first resolved ancestor.
133            let mut chain = Vec::new();
134            let mut cur = start;
135            while !resolved[cur] {
136                chain.push(cur);
137                cur = self.parent[cur];
138            }
139            let mut d = depth[cur];
140            for &node in chain.iter().rev() {
141                d += 1;
142                depth[node] = d;
143                resolved[node] = true;
144            }
145        }
146        depth
147    }
148}
149
150/// Build a symmetric [`FlowNetwork`] from an undirected weighted edge list.
151///
152/// Each undirected edge `{u, v}` with weight `w` contributes capacity `w` to both
153/// `u→v` and `v→u`, which is the standard reduction for undirected min-cut via max-flow.
154fn build_undirected_network(
155    n: usize,
156    edges: &[(usize, usize, f64)],
157) -> GraphalgResult<FlowNetwork> {
158    let mut net = FlowNetwork::new(n);
159    for &(u, v, w) in edges {
160        if u >= n || v >= n {
161            return Err(GraphalgError::IndexOutOfBounds {
162                index: u.max(v),
163                len: n,
164            });
165        }
166        if !w.is_finite() {
167            return Err(GraphalgError::InvalidEdgeWeight(format!(
168                "edge ({u},{v}) weight not finite: {w}"
169            )));
170        }
171        if w < 0.0 {
172            return Err(GraphalgError::NegativeWeight {
173                edge: (u, v),
174                weight: w,
175            });
176        }
177        if u == v {
178            // Self-loops never cross any cut; ignore them.
179            continue;
180        }
181        net.add_edge(u, v, w)?;
182        net.add_edge(v, u, w)?;
183    }
184    Ok(net)
185}
186
187/// Construct the Gomory-Hu cut tree of an undirected weighted graph using Gusfield's
188/// `n − 1` max-flow computations.
189///
190/// `edges` is an undirected weighted edge list: each `(u, v, w)` denotes an undirected
191/// edge of non-negative weight `w`. Parallel edges are summed (their capacities add),
192/// self-loops are ignored.
193///
194/// # Errors
195/// - [`GraphalgError::IndexOutOfBounds`] if any endpoint is `>= n`.
196/// - [`GraphalgError::NegativeWeight`] if any edge weight is negative.
197/// - [`GraphalgError::InvalidEdgeWeight`] if any edge weight is non-finite.
198pub fn gomory_hu_tree(n: usize, edges: &[(usize, usize, f64)]) -> GraphalgResult<GomoryHuTree> {
199    if n == 0 {
200        return Ok(GomoryHuTree {
201            n: 0,
202            parent: Vec::new(),
203            weight: Vec::new(),
204        });
205    }
206    if n == 1 {
207        return Ok(GomoryHuTree {
208            n: 1,
209            parent: vec![0],
210            weight: vec![0.0],
211        });
212    }
213
214    let net = build_undirected_network(n, edges)?;
215
216    // Gusfield: tree rooted at 0.
217    let mut parent = vec![0usize; n];
218    let mut weight = vec![0.0f64; n];
219
220    for s in 1..n {
221        let t = parent[s];
222        let cut = min_cut_from_max_flow(&net, s, t)?;
223        let f = cut.value;
224        weight[s] = f;
225
226        // Membership test on the source side (the side containing `s`).
227        let mut in_source = vec![false; n];
228        for &v in &cut.source_side {
229            in_source[v] = true;
230        }
231
232        // Re-parent every other vertex that shares parent `t` and landed on `s`'s side.
233        for v in 0..n {
234            if v != s && parent[v] == t && in_source[v] {
235                parent[v] = s;
236            }
237        }
238
239        // Gusfield's additional adjustment: if `parent[t]` lies on `s`'s side, rotate.
240        let pt = parent[t];
241        if in_source[pt] {
242            parent[s] = pt;
243            parent[t] = s;
244            weight[s] = weight[t];
245            weight[t] = f;
246        }
247    }
248
249    Ok(GomoryHuTree { n, parent, weight })
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::max_flow::edmonds_karp::FlowNetwork;
256    use crate::max_flow::min_cut::min_cut_from_max_flow;
257
258    /// Direct min `s`–`t` cut via max-flow on a fresh symmetric network — the oracle.
259    fn direct_min_cut(n: usize, edges: &[(usize, usize, f64)], s: usize, t: usize) -> f64 {
260        let mut net = FlowNetwork::new(n);
261        for &(u, v, w) in edges {
262            if u == v {
263                continue;
264            }
265            net.add_edge(u, v, w).expect("add ok");
266            net.add_edge(v, u, w).expect("add ok");
267        }
268        min_cut_from_max_flow(&net, s, t).expect("cut ok").value
269    }
270
271    /// Verify: for EVERY ordered pair, the tree path-minimum equals the oracle cut.
272    fn assert_all_pairs(n: usize, edges: &[(usize, usize, f64)]) {
273        let tree = gomory_hu_tree(n, edges).expect("tree ok");
274        for s in 0..n {
275            for t in 0..n {
276                if s == t {
277                    continue;
278                }
279                let tree_cut = tree.min_cut(s, t).expect("min_cut ok");
280                let oracle = direct_min_cut(n, edges, s, t);
281                assert!(
282                    (tree_cut - oracle).abs() < 1e-6,
283                    "pair ({s},{t}): tree={tree_cut} oracle={oracle}"
284                );
285            }
286        }
287    }
288
289    #[test]
290    fn all_pairs_match_on_weighted_graph() {
291        // The classic 6-node weighted graph from Gomory & Hu's 1961 paper.
292        let edges = [
293            (0, 1, 1.0),
294            (0, 2, 7.0),
295            (1, 2, 1.0),
296            (1, 3, 3.0),
297            (1, 4, 2.0),
298            (2, 4, 4.0),
299            (3, 4, 1.0),
300            (3, 5, 6.0),
301            (4, 5, 2.0),
302        ];
303        assert_all_pairs(6, &edges);
304    }
305
306    #[test]
307    fn all_pairs_match_on_dense_small() {
308        // A small dense graph (K4 with varied weights) — every pair checked.
309        let edges = [
310            (0, 1, 3.0),
311            (0, 2, 1.0),
312            (0, 3, 5.0),
313            (1, 2, 4.0),
314            (1, 3, 2.0),
315            (2, 3, 6.0),
316        ];
317        assert_all_pairs(4, &edges);
318    }
319
320    #[test]
321    fn tree_has_n_minus_one_edges_and_is_connected() {
322        let edges = [
323            (0, 1, 2.0),
324            (1, 2, 3.0),
325            (2, 3, 4.0),
326            (3, 0, 5.0),
327            (0, 2, 1.0),
328        ];
329        let n = 4;
330        let tree = gomory_hu_tree(n, &edges).expect("tree ok");
331        let tree_edges = tree.tree_edges();
332        assert_eq!(tree_edges.len(), n - 1);
333
334        // Connectivity: union-find over the n-1 tree edges must form a single component.
335        let mut comp: Vec<usize> = (0..n).collect();
336        fn find(comp: &mut [usize], x: usize) -> usize {
337            let mut r = x;
338            while comp[r] != r {
339                r = comp[r];
340            }
341            let mut c = x;
342            while comp[c] != c {
343                let nxt = comp[c];
344                comp[c] = r;
345                c = nxt;
346            }
347            r
348        }
349        for &(u, v, _) in &tree_edges {
350            let (ru, rv) = (find(&mut comp, u), find(&mut comp, v));
351            comp[ru] = rv;
352        }
353        let root = find(&mut comp, 0);
354        for v in 0..n {
355            assert_eq!(find(&mut comp, v), root, "vertex {v} not connected in tree");
356        }
357    }
358
359    #[test]
360    fn simple_cycle_all_pairs() {
361        // A cycle of unit weights: every min cut should be 2 (two edges must be cut).
362        let edges = [(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0), (3, 0, 1.0)];
363        let n = 4;
364        assert_all_pairs(n, &edges);
365        let tree = gomory_hu_tree(n, &edges).expect("tree ok");
366        for s in 0..n {
367            for t in 0..n {
368                if s != t {
369                    assert!((tree.min_cut(s, t).expect("ok") - 2.0).abs() < 1e-9);
370                }
371            }
372        }
373    }
374
375    #[test]
376    fn disconnected_components_give_zero_cut() {
377        // Two disjoint edges: {0-1} and {2-3}. Cuts across components are 0.
378        let edges = [(0, 1, 5.0), (2, 3, 7.0)];
379        let n = 4;
380        let tree = gomory_hu_tree(n, &edges).expect("tree ok");
381        // Across components → 0.
382        assert!((tree.min_cut(0, 2).expect("ok")).abs() < 1e-9);
383        assert!((tree.min_cut(1, 3).expect("ok")).abs() < 1e-9);
384        // Within a component → the single edge weight.
385        assert!((tree.min_cut(0, 1).expect("ok") - 5.0).abs() < 1e-9);
386        assert!((tree.min_cut(2, 3).expect("ok") - 7.0).abs() < 1e-9);
387        // And cross-check the whole thing against the oracle.
388        assert_all_pairs(n, &edges);
389    }
390
391    #[test]
392    fn cut_is_symmetric() {
393        let edges = [
394            (0, 1, 2.0),
395            (1, 2, 5.0),
396            (2, 3, 1.0),
397            (0, 3, 4.0),
398            (1, 3, 3.0),
399        ];
400        let n = 4;
401        let tree = gomory_hu_tree(n, &edges).expect("tree ok");
402        for s in 0..n {
403            for t in 0..n {
404                let st = tree.min_cut(s, t).expect("ok");
405                let ts = tree.min_cut(t, s).expect("ok");
406                assert!((st - ts).abs() < 1e-12, "asymmetry ({s},{t}): {st} vs {ts}");
407            }
408        }
409    }
410
411    #[test]
412    fn single_edge_graph() {
413        let edges = [(0, 1, 9.0)];
414        let tree = gomory_hu_tree(2, &edges).expect("tree ok");
415        assert_eq!(tree.tree_edges().len(), 1);
416        assert!((tree.min_cut(0, 1).expect("ok") - 9.0).abs() < 1e-9);
417        assert!((tree.min_cut(1, 0).expect("ok") - 9.0).abs() < 1e-9);
418    }
419
420    #[test]
421    fn tiny_two_node_no_edges() {
422        let tree = gomory_hu_tree(2, &[]).expect("tree ok");
423        // No edges → cut is 0.
424        assert!((tree.min_cut(0, 1).expect("ok")).abs() < 1e-9);
425        assert_eq!(tree.tree_edges().len(), 1);
426    }
427
428    #[test]
429    fn single_node_graph() {
430        let tree = gomory_hu_tree(1, &[]).expect("tree ok");
431        assert_eq!(tree.n, 1);
432        assert_eq!(tree.tree_edges().len(), 0);
433        assert!((tree.min_cut(0, 0).expect("ok")).abs() < 1e-9);
434    }
435
436    #[test]
437    fn empty_graph() {
438        let tree = gomory_hu_tree(0, &[]).expect("tree ok");
439        assert_eq!(tree.n, 0);
440        assert_eq!(tree.tree_edges().len(), 0);
441    }
442
443    #[test]
444    fn same_vertex_is_zero() {
445        let edges = [(0, 1, 3.0), (1, 2, 4.0)];
446        let tree = gomory_hu_tree(3, &edges).expect("tree ok");
447        assert!((tree.min_cut(1, 1).expect("ok")).abs() < 1e-12);
448    }
449
450    #[test]
451    fn parallel_edges_sum() {
452        // Two parallel edges 0-1 of weight 2 and 3 → combined cut should be 5.
453        let edges = [(0, 1, 2.0), (0, 1, 3.0)];
454        let tree = gomory_hu_tree(2, &edges).expect("tree ok");
455        assert!((tree.min_cut(0, 1).expect("ok") - 5.0).abs() < 1e-9);
456    }
457
458    #[test]
459    fn self_loops_ignored() {
460        let edges = [(0, 0, 100.0), (0, 1, 4.0), (1, 1, 50.0)];
461        let tree = gomory_hu_tree(2, &edges).expect("tree ok");
462        assert!((tree.min_cut(0, 1).expect("ok") - 4.0).abs() < 1e-9);
463    }
464
465    #[test]
466    fn rejects_negative_weight() {
467        let edges = [(0, 1, -1.0)];
468        assert!(matches!(
469            gomory_hu_tree(2, &edges),
470            Err(GraphalgError::NegativeWeight { .. })
471        ));
472    }
473
474    #[test]
475    fn rejects_oob_endpoint() {
476        let edges = [(0, 5, 1.0)];
477        assert!(matches!(
478            gomory_hu_tree(3, &edges),
479            Err(GraphalgError::IndexOutOfBounds { .. })
480        ));
481    }
482
483    #[test]
484    fn min_cut_rejects_oob_query() {
485        let tree = gomory_hu_tree(3, &[(0, 1, 1.0), (1, 2, 1.0)]).expect("ok");
486        assert!(tree.min_cut(0, 9).is_err());
487        assert!(tree.min_cut(9, 0).is_err());
488    }
489
490    #[test]
491    fn star_graph_all_pairs() {
492        // Star centered at 0 with leaf weights 1,2,3. min cut between two leaves is the
493        // smaller of their two spoke weights; between center and a leaf is the spoke.
494        let edges = [(0, 1, 1.0), (0, 2, 2.0), (0, 3, 3.0)];
495        let n = 4;
496        assert_all_pairs(n, &edges);
497    }
498
499    #[test]
500    fn path_graph_all_pairs() {
501        let edges = [(0, 1, 4.0), (1, 2, 2.0), (2, 3, 6.0), (3, 4, 5.0)];
502        assert_all_pairs(5, &edges);
503    }
504}