use crate::core::{Graph, IgraphError, IgraphResult};
use super::max_flow::max_flow_value;
pub fn mincut_value(graph: &Graph, capacity: Option<&[f64]>) -> IgraphResult<f64> {
let n = graph.vcount();
if let Some(c) = capacity {
let m = graph.ecount();
if c.len() != m {
return Err(IgraphError::InvalidArgument(format!(
"capacity length {} does not match edge count {}",
c.len(),
m
)));
}
for (i, &v) in c.iter().enumerate() {
if !v.is_finite() || v < 0.0 {
return Err(IgraphError::InvalidArgument(format!(
"capacity[{i}] = {v} is not a finite non-negative number"
)));
}
}
}
if n <= 1 {
return Ok(f64::INFINITY);
}
let directed = graph.is_directed();
let mut min_cut = f64::INFINITY;
for v in 1..n {
let f = max_flow_value(graph, 0, v, capacity)?;
if f < min_cut {
min_cut = f;
if min_cut == 0.0 {
return Ok(0.0);
}
}
if directed {
let f2 = max_flow_value(graph, v, 0, capacity)?;
if f2 < min_cut {
min_cut = f2;
if min_cut == 0.0 {
return Ok(0.0);
}
}
}
}
Ok(min_cut)
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-12;
fn approx_eq(a: f64, b: f64) -> bool {
if a.is_infinite() && b.is_infinite() {
a.is_sign_positive() == b.is_sign_positive()
} else {
(a - b).abs() < TOL
}
}
fn ring_n(n: u32, directed: bool) -> Graph {
let mut g = Graph::new(n, directed).expect("graph");
for i in 0..n {
let j = (i + 1) % n;
g.add_edge(i, j).expect("edge");
}
g
}
fn path_n(n: u32, directed: bool) -> Graph {
let mut g = Graph::new(n, directed).expect("graph");
for i in 0..(n - 1) {
g.add_edge(i, i + 1).expect("edge");
}
g
}
fn complete_undirected(n: u32) -> Graph {
let mut g = Graph::new(n, false).expect("graph");
for i in 0..n {
for j in (i + 1)..n {
g.add_edge(i, j).expect("edge");
}
}
g
}
fn complete_directed_mutual(n: u32) -> Graph {
let mut g = Graph::new(n, true).expect("graph");
for i in 0..n {
for j in 0..n {
if i != j {
g.add_edge(i, j).expect("edge");
}
}
}
g
}
#[test]
fn empty_graph_returns_infinity() {
let g = Graph::new(0, false).expect("graph");
let mc = mincut_value(&g, None).expect("mc");
assert!(mc.is_infinite() && mc.is_sign_positive());
}
#[test]
fn single_vertex_returns_infinity() {
let g = Graph::new(1, false).expect("graph");
assert!(mincut_value(&g, None).expect("mc").is_infinite());
let g2 = Graph::new(1, true).expect("graph");
assert!(mincut_value(&g2, None).expect("mc").is_infinite());
}
#[test]
fn two_disconnected_vertices_return_zero() {
let g = Graph::new(2, false).expect("graph");
assert!(approx_eq(mincut_value(&g, None).expect("mc"), 0.0));
}
#[test]
fn two_isolated_edges_return_zero() {
let mut g = Graph::new(4, false).expect("graph");
g.add_edge(0, 1).expect("edge");
g.add_edge(2, 3).expect("edge");
assert!(approx_eq(mincut_value(&g, None).expect("mc"), 0.0));
}
#[test]
fn unit_caps_ring_5v_undirected() {
let g = ring_n(5, false);
assert!(approx_eq(mincut_value(&g, None).expect("mc"), 2.0));
}
#[test]
fn unit_caps_path_5v_undirected() {
let g = path_n(5, false);
assert!(approx_eq(mincut_value(&g, None).expect("mc"), 1.0));
}
#[test]
fn unit_caps_k5_undirected() {
let g = complete_undirected(5);
assert!(approx_eq(mincut_value(&g, None).expect("mc"), 4.0));
}
#[test]
fn unit_caps_directed_3cycle() {
let g = ring_n(3, true);
assert!(approx_eq(mincut_value(&g, None).expect("mc"), 1.0));
}
#[test]
fn unit_caps_complete_directed_mutual_4v() {
let g = complete_directed_mutual(4);
assert!(approx_eq(mincut_value(&g, None).expect("mc"), 3.0));
}
#[test]
fn weighted_k2_returns_capacity() {
let mut g = Graph::new(2, false).expect("graph");
g.add_edge(0, 1).expect("edge");
let caps = vec![7.5];
assert!(approx_eq(mincut_value(&g, Some(&caps)).expect("mc"), 7.5));
}
#[test]
fn weighted_ring_5v_undirected_min_edge() {
let g = ring_n(5, false);
let caps = vec![0.25, 10.0, 10.0, 10.0, 10.0];
let mc = mincut_value(&g, Some(&caps)).expect("mc");
assert!(approx_eq(mc, 10.25), "got {mc}, want 10.25");
}
#[test]
fn weighted_path_directed_zero_capacity_returns_zero() {
let g = path_n(5, true);
let caps = vec![0.0, 1.0, 1.0, 1.0];
assert!(approx_eq(mincut_value(&g, Some(&caps)).expect("mc"), 0.0));
}
#[test]
fn weighted_directed_3cycle_min_arc() {
let g = ring_n(3, true);
let caps = vec![3.0, 1.0, 2.0];
assert!(approx_eq(mincut_value(&g, Some(&caps)).expect("mc"), 1.0));
}
#[test]
fn multigraph_two_parallel_edges_returns_two() {
let mut g = Graph::new(2, false).expect("graph");
g.add_edge(0, 1).expect("edge");
g.add_edge(0, 1).expect("edge");
assert!(approx_eq(mincut_value(&g, None).expect("mc"), 2.0));
}
#[test]
fn none_matches_unit_caps() {
let fixtures: Vec<Graph> = vec![
ring_n(6, false),
ring_n(6, true),
path_n(5, false),
complete_undirected(4),
complete_directed_mutual(4),
];
for g in fixtures {
let caps = vec![1.0_f64; g.ecount()];
let a = mincut_value(&g, None).expect("mc");
let b = mincut_value(&g, Some(&caps)).expect("mc");
assert!(
approx_eq(a, b),
"None={a} != Some(1.0..)={b} (n={}, dir={})",
g.vcount(),
g.is_directed()
);
}
}
#[test]
fn capacity_wrong_length_errors() {
let g = ring_n(4, false);
let caps = vec![1.0_f64; 99];
let err = mincut_value(&g, Some(&caps)).expect_err("must err");
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn capacity_negative_errors() {
let g = ring_n(4, false);
let mut caps = vec![1.0_f64; g.ecount()];
caps[1] = -0.5;
let err = mincut_value(&g, Some(&caps)).expect_err("must err");
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn capacity_nan_errors() {
let g = ring_n(4, false);
let mut caps = vec![1.0_f64; g.ecount()];
caps[0] = f64::NAN;
let err = mincut_value(&g, Some(&caps)).expect_err("must err");
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptests {
use super::*;
use crate::algorithms::flow::edge_connectivity::edge_connectivity;
use crate::core::Graph;
use proptest::prelude::*;
fn xorshift(mut r: u64) -> u64 {
r ^= r << 13;
r ^= r >> 7;
r ^= r << 17;
r
}
fn build_random(seed: u64, n: u32, m_max: u32, directed: bool) -> Graph {
let mut g = Graph::new(n, directed).expect("graph");
let mut state = seed | 1;
for _ in 0..m_max {
state = xorshift(state);
let u = u32::try_from(state % u64::from(n)).expect("modulo fits");
state = xorshift(state);
let v = u32::try_from(state % u64::from(n)).expect("modulo fits");
if u == v {
continue;
}
g.add_edge(u, v).expect("edge");
}
g
}
proptest! {
#[test]
fn nonneg_and_finite(
seed in any::<u64>(),
n in 2u32..6,
m in 0u32..10,
directed in any::<bool>(),
) {
let g = build_random(seed, n, m, directed);
let mc = mincut_value(&g, None).expect("mc");
prop_assert!(mc.is_finite(), "expected finite mc, got {mc}");
prop_assert!(mc >= 0.0, "expected mc >= 0, got {mc}");
}
#[test]
fn none_matches_unit_caps(
seed in any::<u64>(),
n in 2u32..6,
m in 0u32..10,
directed in any::<bool>(),
) {
let g = build_random(seed, n, m, directed);
let caps = vec![1.0_f64; g.ecount()];
let a = mincut_value(&g, None).expect("mc");
let b = mincut_value(&g, Some(&caps)).expect("mc");
prop_assert!((a - b).abs() < 1e-12,
"None={a} != Some(1.0..)={b} (n={n}, m={m}, dir={directed}, seed={seed})");
}
#[test]
fn unit_caps_match_edge_connectivity(
seed in any::<u64>(),
n in 2u32..6,
m in 0u32..10,
directed in any::<bool>(),
) {
let g = build_random(seed, n, m, directed);
let mc = mincut_value(&g, None).expect("mc");
let ec = edge_connectivity(&g, true).expect("ec") as f64;
prop_assert!((mc - ec).abs() < 1e-12,
"mincut={mc} != edge_conn={ec} (n={n}, m={m}, dir={directed}, seed={seed})");
}
#[test]
fn doubling_capacities_doubles_mincut(
seed in any::<u64>(),
n in 2u32..5,
m in 1u32..8,
directed in any::<bool>(),
) {
let g = build_random(seed, n, m, directed);
let caps1 = vec![1.0_f64; g.ecount()];
let caps2 = vec![2.0_f64; g.ecount()];
let mc1 = mincut_value(&g, Some(&caps1)).expect("mc1");
let mc2 = mincut_value(&g, Some(&caps2)).expect("mc2");
prop_assert!((mc2 - 2.0 * mc1).abs() < 1e-12,
"doubling caps should double mc: mc1={mc1}, mc2={mc2}");
}
}
}