Skip to main content

rust_igraph/algorithms/flow/
mincut_value.rs

1//! `mincut_value` (ALGO-FL-017) — global minimum-cut value: the
2//! weighted generalisation of [`super::edge_connectivity`].
3//!
4//! Counterpart of `igraph_mincut_value` in
5//! `references/igraph/src/flow/flow.c:1692`. Equivalent to
6//! python-igraph's `Graph.mincut_value()` and R-igraph's `min_cut()`
7//! (called with no `source`/`target`).
8//!
9//! ## Algorithm
10//!
11//! Definition: the minimum total capacity of an edge set whose
12//! removal makes the graph not strongly connected (for undirected
13//! input, strong and weak connectedness coincide). With unit
14//! capacities this collapses to the edge connectivity from FL-016.
15//!
16//! Same fixed-vertex strategy as FL-016: pick vertex `0`, and for
17//! every other vertex `v` compute the maximum flow `0 → v`
18//! (undirected) or both `0 → v` and `v → 0` (directed). The minimum
19//! over all such pairs equals the global minimum cut, because every
20//! global min-cut separates `0` from at least one vertex on the
21//! other side.
22//!
23//! Unlike igraph C, this port does **not** branch on directedness:
24//! the same fixed-vertex iteration is run for both undirected and
25//! directed graphs. igraph's undirected branch is a
26//! Stoer–Wagner-style algorithm with better asymptotics on dense
27//! weighted inputs; porting it is left for a future AWU.
28//!
29//! ## Corner cases
30//!
31//! * `vcount ≤ 1` → `f64::INFINITY`. Mirrors `IGRAPH_INFINITY` init
32//!   at `flow.c:1699` — there are no pairs to separate, so the
33//!   minimum over an empty set is `+∞`.
34//! * Disconnected graph (some pair has no `0 → v` path) → `0.0`.
35//!
36//! ## Complexity
37//!
38//! `V - 1` (undirected) or `2 (V - 1)` (directed) calls into FL-002
39//! `max_flow_value`. Each call is `O(V·E²)` on Dinic (the FL-002
40//! backend), so the total is `O(V²·E²)` worst case. The igraph C
41//! docstring reports `O(log V · V²)` for the (not-ported)
42//! Stoer-Wagner branch and `O(V⁴)` for the directed branch.
43
44use crate::core::{Graph, IgraphError, IgraphResult};
45
46use super::max_flow::max_flow_value;
47
48/// Global minimum-cut value of a (possibly weighted) graph.
49///
50/// Returns the minimum total capacity of an edge set whose removal
51/// disconnects some pair of distinct vertices. For undirected input,
52/// equivalent to the global min-cut over weak components; for
53/// directed input, over strongly connected components.
54///
55/// Mirrors `igraph_mincut_value`
56/// (`references/igraph/src/flow/flow.c:1692`).
57///
58/// # Arguments
59///
60/// * `graph` — input graph (directed or undirected).
61/// * `capacity` — optional per-edge capacity vector. `None` means
62///   every edge has unit capacity (in which case the result equals
63///   `edge_connectivity(graph, true) as f64`). When `Some(c)`,
64///   `c.len()` must equal `graph.ecount()` and each entry must be a
65///   finite non-negative number — these checks are delegated to
66///   [`max_flow_value`].
67///
68/// # Returns
69///
70/// * `f64::INFINITY` if `vcount() ≤ 1` (no pair to separate).
71/// * `0.0` if the graph is not strongly connected (directed) or not
72///   connected (undirected).
73/// * Otherwise the minimum value of `max_flow_value(graph, 0, v,
74///   capacity)` (plus `max_flow_value(graph, v, 0, capacity)` for
75///   directed) over `v ∈ 1..vcount()`.
76///
77/// # Errors
78///
79/// Propagates errors from [`max_flow_value`] — chiefly
80/// [`IgraphError::InvalidArgument`] for bad capacity input (length
81/// mismatch, NaN, negative).
82///
83/// # Examples
84///
85/// ```
86/// use rust_igraph::{Graph, mincut_value};
87///
88/// // Undirected ring on 5 vertices, unit caps — global min cut = 2.
89/// let mut g = Graph::new(5, false).unwrap();
90/// for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 4), (4, 0)] {
91///     g.add_edge(u, v).unwrap();
92/// }
93/// assert!((mincut_value(&g, None).unwrap() - 2.0).abs() < 1e-12);
94///
95/// // Same ring, weighted: each edge weight 0.5 → min cut = 1.0.
96/// let caps = vec![0.5_f64; 5];
97/// let mc = mincut_value(&g, Some(&caps)).unwrap();
98/// assert!((mc - 1.0).abs() < 1e-12);
99/// ```
100pub fn mincut_value(graph: &Graph, capacity: Option<&[f64]>) -> IgraphResult<f64> {
101    let n = graph.vcount();
102
103    // Mirrors `IGRAPH_INFINITY` init at flow.c:1699 — for n ≤ 1 the
104    // minimum over an empty pair-set is +∞. Validate capacity length
105    // up front so callers always get the same error for n=0 and n=2.
106    if let Some(c) = capacity {
107        let m = graph.ecount();
108        if c.len() != m {
109            return Err(IgraphError::InvalidArgument(format!(
110                "capacity length {} does not match edge count {}",
111                c.len(),
112                m
113            )));
114        }
115        for (i, &v) in c.iter().enumerate() {
116            if !v.is_finite() || v < 0.0 {
117                return Err(IgraphError::InvalidArgument(format!(
118                    "capacity[{i}] = {v} is not a finite non-negative number"
119                )));
120            }
121        }
122    }
123    if n <= 1 {
124        return Ok(f64::INFINITY);
125    }
126
127    // Fixed-vertex iteration — see module doc and flow.c:1706-1723.
128    let directed = graph.is_directed();
129    let mut min_cut = f64::INFINITY;
130    for v in 1..n {
131        let f = max_flow_value(graph, 0, v, capacity)?;
132        if f < min_cut {
133            min_cut = f;
134            if min_cut == 0.0 {
135                return Ok(0.0);
136            }
137        }
138        if directed {
139            let f2 = max_flow_value(graph, v, 0, capacity)?;
140            if f2 < min_cut {
141                min_cut = f2;
142                if min_cut == 0.0 {
143                    return Ok(0.0);
144                }
145            }
146        }
147    }
148
149    Ok(min_cut)
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    const TOL: f64 = 1e-12;
157
158    fn approx_eq(a: f64, b: f64) -> bool {
159        if a.is_infinite() && b.is_infinite() {
160            a.is_sign_positive() == b.is_sign_positive()
161        } else {
162            (a - b).abs() < TOL
163        }
164    }
165
166    fn ring_n(n: u32, directed: bool) -> Graph {
167        let mut g = Graph::new(n, directed).expect("graph");
168        for i in 0..n {
169            let j = (i + 1) % n;
170            g.add_edge(i, j).expect("edge");
171        }
172        g
173    }
174
175    fn path_n(n: u32, directed: bool) -> Graph {
176        let mut g = Graph::new(n, directed).expect("graph");
177        for i in 0..(n - 1) {
178            g.add_edge(i, i + 1).expect("edge");
179        }
180        g
181    }
182
183    fn complete_undirected(n: u32) -> Graph {
184        let mut g = Graph::new(n, false).expect("graph");
185        for i in 0..n {
186            for j in (i + 1)..n {
187                g.add_edge(i, j).expect("edge");
188            }
189        }
190        g
191    }
192
193    fn complete_directed_mutual(n: u32) -> Graph {
194        let mut g = Graph::new(n, true).expect("graph");
195        for i in 0..n {
196            for j in 0..n {
197                if i != j {
198                    g.add_edge(i, j).expect("edge");
199                }
200            }
201        }
202        g
203    }
204
205    // --- IGRAPH_INFINITY init parity (flow.c:1699) ---
206
207    #[test]
208    fn empty_graph_returns_infinity() {
209        let g = Graph::new(0, false).expect("graph");
210        let mc = mincut_value(&g, None).expect("mc");
211        assert!(mc.is_infinite() && mc.is_sign_positive());
212    }
213
214    #[test]
215    fn single_vertex_returns_infinity() {
216        let g = Graph::new(1, false).expect("graph");
217        assert!(mincut_value(&g, None).expect("mc").is_infinite());
218        let g2 = Graph::new(1, true).expect("graph");
219        assert!(mincut_value(&g2, None).expect("mc").is_infinite());
220    }
221
222    // --- Disconnected ---
223
224    #[test]
225    fn two_disconnected_vertices_return_zero() {
226        let g = Graph::new(2, false).expect("graph");
227        assert!(approx_eq(mincut_value(&g, None).expect("mc"), 0.0));
228    }
229
230    #[test]
231    fn two_isolated_edges_return_zero() {
232        let mut g = Graph::new(4, false).expect("graph");
233        g.add_edge(0, 1).expect("edge");
234        g.add_edge(2, 3).expect("edge");
235        assert!(approx_eq(mincut_value(&g, None).expect("mc"), 0.0));
236    }
237
238    // --- Unit-capacity parity with edge_connectivity (FL-016) ---
239
240    #[test]
241    fn unit_caps_ring_5v_undirected() {
242        let g = ring_n(5, false);
243        assert!(approx_eq(mincut_value(&g, None).expect("mc"), 2.0));
244    }
245
246    #[test]
247    fn unit_caps_path_5v_undirected() {
248        let g = path_n(5, false);
249        assert!(approx_eq(mincut_value(&g, None).expect("mc"), 1.0));
250    }
251
252    #[test]
253    fn unit_caps_k5_undirected() {
254        let g = complete_undirected(5);
255        assert!(approx_eq(mincut_value(&g, None).expect("mc"), 4.0));
256    }
257
258    #[test]
259    fn unit_caps_directed_3cycle() {
260        let g = ring_n(3, true);
261        assert!(approx_eq(mincut_value(&g, None).expect("mc"), 1.0));
262    }
263
264    #[test]
265    fn unit_caps_complete_directed_mutual_4v() {
266        let g = complete_directed_mutual(4);
267        assert!(approx_eq(mincut_value(&g, None).expect("mc"), 3.0));
268    }
269
270    // --- Weighted fixtures ---
271
272    #[test]
273    fn weighted_k2_returns_capacity() {
274        let mut g = Graph::new(2, false).expect("graph");
275        g.add_edge(0, 1).expect("edge");
276        let caps = vec![7.5];
277        assert!(approx_eq(mincut_value(&g, Some(&caps)).expect("mc"), 7.5));
278    }
279
280    #[test]
281    fn weighted_ring_5v_undirected_min_edge() {
282        // Bridge edge with weight 0.25; rest 10.0. The cheapest
283        // 2-edge cut is the bridge + its predecessor: 0.25 + 10 = 10.25,
284        // which is the minimum (any cut without the bridge requires
285        // weight ≥ 20).
286        let g = ring_n(5, false);
287        let caps = vec![0.25, 10.0, 10.0, 10.0, 10.0];
288        let mc = mincut_value(&g, Some(&caps)).expect("mc");
289        assert!(approx_eq(mc, 10.25), "got {mc}, want 10.25");
290    }
291
292    #[test]
293    fn weighted_path_directed_zero_capacity_returns_zero() {
294        // Path with the first capacity zero — flow 0 → 4 = 0, so
295        // min cut = 0.
296        let g = path_n(5, true);
297        let caps = vec![0.0, 1.0, 1.0, 1.0];
298        assert!(approx_eq(mincut_value(&g, Some(&caps)).expect("mc"), 0.0));
299    }
300
301    #[test]
302    fn weighted_directed_3cycle_min_arc() {
303        // Directed 3-cycle 0→1→2→0 with caps [3, 1, 2]. The min cut
304        // 0→2 path-wise: bottleneck arc 1→2 weight 1. Also need to
305        // check v→0 directions: 1→0 not present; 2→0 weight 2.
306        // min_{v} min(maxflow(0→v), maxflow(v→0)) = min(3, 1, 2, 2) = 1.
307        let g = ring_n(3, true);
308        let caps = vec![3.0, 1.0, 2.0];
309        assert!(approx_eq(mincut_value(&g, Some(&caps)).expect("mc"), 1.0));
310    }
311
312    // --- Multigraph: parallel edges raise the min cut ---
313
314    #[test]
315    fn multigraph_two_parallel_edges_returns_two() {
316        let mut g = Graph::new(2, false).expect("graph");
317        g.add_edge(0, 1).expect("edge");
318        g.add_edge(0, 1).expect("edge");
319        assert!(approx_eq(mincut_value(&g, None).expect("mc"), 2.0));
320    }
321
322    // --- Capacity = None must match capacity = Some(all ones) ---
323
324    #[test]
325    fn none_matches_unit_caps() {
326        let fixtures: Vec<Graph> = vec![
327            ring_n(6, false),
328            ring_n(6, true),
329            path_n(5, false),
330            complete_undirected(4),
331            complete_directed_mutual(4),
332        ];
333        for g in fixtures {
334            let caps = vec![1.0_f64; g.ecount()];
335            let a = mincut_value(&g, None).expect("mc");
336            let b = mincut_value(&g, Some(&caps)).expect("mc");
337            assert!(
338                approx_eq(a, b),
339                "None={a} != Some(1.0..)={b} (n={}, dir={})",
340                g.vcount(),
341                g.is_directed()
342            );
343        }
344    }
345
346    // --- Error paths ---
347
348    #[test]
349    fn capacity_wrong_length_errors() {
350        let g = ring_n(4, false);
351        let caps = vec![1.0_f64; 99];
352        let err = mincut_value(&g, Some(&caps)).expect_err("must err");
353        assert!(matches!(err, IgraphError::InvalidArgument(_)));
354    }
355
356    #[test]
357    fn capacity_negative_errors() {
358        let g = ring_n(4, false);
359        let mut caps = vec![1.0_f64; g.ecount()];
360        caps[1] = -0.5;
361        let err = mincut_value(&g, Some(&caps)).expect_err("must err");
362        assert!(matches!(err, IgraphError::InvalidArgument(_)));
363    }
364
365    #[test]
366    fn capacity_nan_errors() {
367        let g = ring_n(4, false);
368        let mut caps = vec![1.0_f64; g.ecount()];
369        caps[0] = f64::NAN;
370        let err = mincut_value(&g, Some(&caps)).expect_err("must err");
371        assert!(matches!(err, IgraphError::InvalidArgument(_)));
372    }
373}
374
375#[cfg(all(test, feature = "proptest-harness"))]
376mod proptests {
377    //! Proptest invariants:
378    //! * `mincut_value >= 0` for any valid input with `vcount ≥ 2`.
379    //! * `mincut_value` is finite for `vcount ≥ 2`.
380    //! * `mincut_value(g, None) == mincut_value(g, Some(unit caps))`
381    //!   to within tolerance.
382    //! * On unit capacities, `mincut_value == edge_connectivity` as
383    //!   `f64`.
384
385    use super::*;
386    use crate::algorithms::flow::edge_connectivity::edge_connectivity;
387    use crate::core::Graph;
388    use proptest::prelude::*;
389
390    fn xorshift(mut r: u64) -> u64 {
391        r ^= r << 13;
392        r ^= r >> 7;
393        r ^= r << 17;
394        r
395    }
396
397    fn build_random(seed: u64, n: u32, m_max: u32, directed: bool) -> Graph {
398        let mut g = Graph::new(n, directed).expect("graph");
399        let mut state = seed | 1;
400        for _ in 0..m_max {
401            state = xorshift(state);
402            let u = u32::try_from(state % u64::from(n)).expect("modulo fits");
403            state = xorshift(state);
404            let v = u32::try_from(state % u64::from(n)).expect("modulo fits");
405            if u == v {
406                continue;
407            }
408            g.add_edge(u, v).expect("edge");
409        }
410        g
411    }
412
413    proptest! {
414        #[test]
415        fn nonneg_and_finite(
416            seed in any::<u64>(),
417            n in 2u32..6,
418            m in 0u32..10,
419            directed in any::<bool>(),
420        ) {
421            let g = build_random(seed, n, m, directed);
422            let mc = mincut_value(&g, None).expect("mc");
423            prop_assert!(mc.is_finite(), "expected finite mc, got {mc}");
424            prop_assert!(mc >= 0.0, "expected mc >= 0, got {mc}");
425        }
426
427        #[test]
428        fn none_matches_unit_caps(
429            seed in any::<u64>(),
430            n in 2u32..6,
431            m in 0u32..10,
432            directed in any::<bool>(),
433        ) {
434            let g = build_random(seed, n, m, directed);
435            let caps = vec![1.0_f64; g.ecount()];
436            let a = mincut_value(&g, None).expect("mc");
437            let b = mincut_value(&g, Some(&caps)).expect("mc");
438            prop_assert!((a - b).abs() < 1e-12,
439                "None={a} != Some(1.0..)={b} (n={n}, m={m}, dir={directed}, seed={seed})");
440        }
441
442        #[test]
443        fn unit_caps_match_edge_connectivity(
444            seed in any::<u64>(),
445            n in 2u32..6,
446            m in 0u32..10,
447            directed in any::<bool>(),
448        ) {
449            let g = build_random(seed, n, m, directed);
450            let mc = mincut_value(&g, None).expect("mc");
451            let ec = edge_connectivity(&g, true).expect("ec") as f64;
452            prop_assert!((mc - ec).abs() < 1e-12,
453                "mincut={mc} != edge_conn={ec} (n={n}, m={m}, dir={directed}, seed={seed})");
454        }
455
456        #[test]
457        fn doubling_capacities_doubles_mincut(
458            seed in any::<u64>(),
459            n in 2u32..5,
460            m in 1u32..8,
461            directed in any::<bool>(),
462        ) {
463            let g = build_random(seed, n, m, directed);
464            let caps1 = vec![1.0_f64; g.ecount()];
465            let caps2 = vec![2.0_f64; g.ecount()];
466            let mc1 = mincut_value(&g, Some(&caps1)).expect("mc1");
467            let mc2 = mincut_value(&g, Some(&caps2)).expect("mc2");
468            prop_assert!((mc2 - 2.0 * mc1).abs() < 1e-12,
469                "doubling caps should double mc: mc1={mc1}, mc2={mc2}");
470        }
471    }
472}