Skip to main content

rust_igraph/algorithms/flow/
mincut.rs

1//! `mincut` (ALGO-FL-019) — global minimum cut with partition.
2//!
3//! Counterpart of `igraph_mincut` in
4//! `references/igraph/src/flow/flow.c:1625`. Returns not just the
5//! cut value, but also the actual cut edges and vertex partitions.
6//!
7//! ## Algorithm
8//!
9//! Same fixed-vertex strategy as [`super::mincut_value`]: pick vertex
10//! 0, compute `st_mincut(0, v)` for every other vertex v (and `st_mincut(v, 0)`
11//! for directed graphs). Track whichever pair yields the minimum cut.
12//! Return the full [`StMincut`] from that winning pair.
13//!
14//! ## Corner cases
15//!
16//! * `vcount ≤ 1` → value `f64::INFINITY`, empty cut/partitions.
17//! * Disconnected graph → value `0.0`, empty cut, partition splits
18//!   at first disconnected pair found.
19
20use crate::core::{Graph, IgraphError, IgraphResult};
21
22use super::st_mincut::{StMincut, st_mincut};
23
24/// Result of the global minimum cut computation.
25///
26/// Contains the cut value, edge IDs forming the cut, and the two
27/// vertex partitions.
28pub type Mincut = StMincut;
29
30/// Compute the global minimum cut of a (possibly weighted) graph.
31///
32/// Returns the minimum-capacity edge set whose removal maximally
33/// disconnects the graph, along with the resulting vertex partitions.
34///
35/// # Arguments
36///
37/// * `graph` — input graph (directed or undirected).
38/// * `capacity` — optional per-edge capacity vector. `None` means unit
39///   capacities. When `Some(c)`, `c.len()` must equal `graph.ecount()`
40///   and all entries must be finite non-negative.
41///
42/// # Returns
43///
44/// A [`Mincut`] containing:
45/// * `value` — total capacity of the cut (`f64::INFINITY` if `vcount ≤ 1`).
46/// * `cut` — edge IDs in the cut.
47/// * `partition` / `partition2` — the two sides after cutting.
48///
49/// # Errors
50///
51/// Propagates errors from [`st_mincut`] (capacity validation, vertex
52/// out-of-range, etc.).
53///
54/// # Examples
55///
56/// ```
57/// use rust_igraph::{Graph, mincut};
58///
59/// // Undirected path: 0-1-2. Min cut = 1 edge.
60/// let mut g = Graph::new(3, false).unwrap();
61/// g.add_edge(0, 1).unwrap();
62/// g.add_edge(1, 2).unwrap();
63/// let mc = mincut(&g, None).unwrap();
64/// assert!((mc.value - 1.0).abs() < 1e-12);
65/// assert_eq!(mc.cut.len(), 1);
66/// assert_eq!(mc.partition.len() + mc.partition2.len(), 3);
67/// ```
68pub fn mincut(graph: &Graph, capacity: Option<&[f64]>) -> IgraphResult<Mincut> {
69    let n = graph.vcount();
70
71    if let Some(c) = capacity {
72        let m = graph.ecount();
73        if c.len() != m {
74            return Err(IgraphError::InvalidArgument(format!(
75                "mincut: capacity length {} does not match edge count {}",
76                c.len(),
77                m
78            )));
79        }
80        for (i, &v) in c.iter().enumerate() {
81            if !v.is_finite() || v < 0.0 {
82                return Err(IgraphError::InvalidArgument(format!(
83                    "mincut: capacity[{i}] = {v} is not a finite non-negative number"
84                )));
85            }
86        }
87    }
88
89    if n <= 1 {
90        return Ok(Mincut {
91            value: f64::INFINITY,
92            cut: Vec::new(),
93            partition: if n == 1 { vec![0] } else { Vec::new() },
94            partition2: Vec::new(),
95        });
96    }
97
98    let directed = graph.is_directed();
99    let mut best: Option<Mincut> = None;
100
101    for v in 1..n {
102        let result = st_mincut(graph, 0, v, capacity)?;
103        let is_better = best.as_ref().is_none_or(|b| result.value < b.value);
104        if is_better {
105            if result.value == 0.0 {
106                return Ok(result);
107            }
108            best = Some(result);
109        }
110
111        if directed {
112            let result2 = st_mincut(graph, v, 0, capacity)?;
113            let is_better2 = best.as_ref().is_none_or(|b| result2.value < b.value);
114            if is_better2 {
115                if result2.value == 0.0 {
116                    return Ok(result2);
117                }
118                best = Some(result2);
119            }
120        }
121    }
122
123    best.ok_or(IgraphError::Internal(
124        "mincut: no iteration completed despite n >= 2",
125    ))
126}
127
128#[cfg(test)]
129#[allow(clippy::float_cmp)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn mincut_empty_graph() {
135        let g = Graph::new(0, false).unwrap();
136        let mc = mincut(&g, None).unwrap();
137        assert_eq!(mc.value, f64::INFINITY);
138        assert!(mc.cut.is_empty());
139    }
140
141    #[test]
142    fn mincut_single_vertex() {
143        let g = Graph::new(1, false).unwrap();
144        let mc = mincut(&g, None).unwrap();
145        assert_eq!(mc.value, f64::INFINITY);
146        assert_eq!(mc.partition, vec![0]);
147    }
148
149    #[test]
150    fn mincut_disconnected() {
151        let g = Graph::new(3, false).unwrap();
152        let mc = mincut(&g, None).unwrap();
153        assert!((mc.value - 0.0).abs() < 1e-12);
154        assert!(mc.cut.is_empty());
155    }
156
157    #[test]
158    fn mincut_path() {
159        let mut g = Graph::new(3, false).unwrap();
160        g.add_edge(0, 1).unwrap();
161        g.add_edge(1, 2).unwrap();
162        let mc = mincut(&g, None).unwrap();
163        assert!((mc.value - 1.0).abs() < 1e-12);
164        assert_eq!(mc.cut.len(), 1);
165        assert_eq!(mc.partition.len() + mc.partition2.len(), 3);
166    }
167
168    #[test]
169    fn mincut_ring() {
170        let mut g = Graph::new(5, false).unwrap();
171        for (u, v) in [(0u32, 1u32), (1, 2), (2, 3), (3, 4), (4, 0)] {
172            g.add_edge(u, v).unwrap();
173        }
174        let mc = mincut(&g, None).unwrap();
175        assert!((mc.value - 2.0).abs() < 1e-12);
176        assert_eq!(mc.cut.len(), 2);
177    }
178
179    #[test]
180    fn mincut_weighted() {
181        // Triangle with one weak edge
182        let mut g = Graph::new(3, false).unwrap();
183        g.add_edge(0, 1).unwrap();
184        g.add_edge(1, 2).unwrap();
185        g.add_edge(2, 0).unwrap();
186        let caps = vec![10.0, 1.0, 10.0];
187        let mc = mincut(&g, Some(&caps)).unwrap();
188        // Minimum cut separates vertex 1 from {0, 2} or vice versa
189        // cutting edges 0→1 (cap 10) and 1→2 (cap 1) costs 11
190        // Actually min is to cut edge 1→2 (cap 1) + ... no, need to disconnect
191        // The minimum cut of a triangle: must cut 2 edges to disconnect
192        // To isolate vertex 1: cut edges 0-1 (10) and 1-2 (1) = 11
193        // To isolate vertex 2: cut edges 1-2 (1) and 2-0 (10) = 11
194        // To isolate vertex 0: cut edges 0-1 (10) and 2-0 (10) = 20
195        // So min is 11
196        assert!((mc.value - 11.0).abs() < 1e-12);
197    }
198
199    #[test]
200    fn mincut_directed_ring() {
201        // Directed cycle: 0→1→2→0. Cutting any single edge disconnects
202        let mut g = Graph::new(3, true).unwrap();
203        g.add_edge(0, 1).unwrap();
204        g.add_edge(1, 2).unwrap();
205        g.add_edge(2, 0).unwrap();
206        let mc = mincut(&g, None).unwrap();
207        assert!((mc.value - 1.0).abs() < 1e-12);
208        assert_eq!(mc.cut.len(), 1);
209    }
210
211    #[test]
212    fn mincut_k4() {
213        // Complete graph K4 — min cut = 3 (remove 3 edges to isolate one vertex)
214        let mut g = Graph::new(4, false).unwrap();
215        g.add_edge(0, 1).unwrap();
216        g.add_edge(0, 2).unwrap();
217        g.add_edge(0, 3).unwrap();
218        g.add_edge(1, 2).unwrap();
219        g.add_edge(1, 3).unwrap();
220        g.add_edge(2, 3).unwrap();
221        let mc = mincut(&g, None).unwrap();
222        assert!((mc.value - 3.0).abs() < 1e-12);
223        assert_eq!(mc.cut.len(), 3);
224    }
225
226    #[test]
227    fn mincut_invalid_capacity_len() {
228        let mut g = Graph::new(2, false).unwrap();
229        g.add_edge(0, 1).unwrap();
230        assert!(mincut(&g, Some(&[1.0, 2.0])).is_err());
231    }
232}