use crate::error::{GraphalgError, GraphalgResult};
#[derive(Debug, Clone)]
pub struct GlobalMinCut {
pub value: f64,
pub partition: Vec<usize>,
}
pub fn stoer_wagner_min_cut(
n: usize,
edges: &[(usize, usize, f64)],
) -> GraphalgResult<GlobalMinCut> {
if n < 2 {
return Err(GraphalgError::InvalidParameter(
"global min cut needs at least 2 vertices".to_string(),
));
}
let mut w = vec![0.0f64; n * n];
let put = |w: &mut [f64], a: usize, b: usize, x: f64| {
w[a * n + b] += x;
w[b * n + a] += x;
};
for &(u, v, weight) in edges {
if u >= n || v >= n {
return Err(GraphalgError::IndexOutOfBounds {
index: u.max(v),
len: n,
});
}
if !weight.is_finite() {
return Err(GraphalgError::InvalidEdgeWeight(format!(
"edge ({u},{v}) weight not finite: {weight}"
)));
}
if weight < 0.0 {
return Err(GraphalgError::NegativeWeight {
edge: (u, v),
weight,
});
}
if u == v {
continue;
}
put(&mut w, u, v, weight);
}
let mut groups: Vec<Vec<usize>> = (0..n).map(|v| vec![v]).collect();
let mut active = vec![true; n];
let mut remaining = n;
let mut best_value = f64::INFINITY;
let mut best_partition: Vec<usize> = Vec::new();
while remaining > 1 {
let mut weights_into = vec![0.0f64; n];
let mut added = vec![false; n];
let mut prev = usize::MAX; let mut last = usize::MAX;
for _ in 0..remaining {
let mut sel = usize::MAX;
let mut sel_w = f64::NEG_INFINITY;
for i in 0..n {
if active[i] && !added[i] && weights_into[i] > sel_w {
sel_w = weights_into[i];
sel = i;
}
}
if sel == usize::MAX {
return Err(GraphalgError::NumericalInstability(
"maximum adjacency search failed to select a vertex".to_string(),
));
}
added[sel] = true;
prev = last;
last = sel;
for j in 0..n {
if active[j] && !added[j] {
weights_into[j] += w[sel * n + j];
}
}
}
let cut_value = weights_into[last];
if cut_value < best_value {
best_value = cut_value;
best_partition = groups[last].clone();
}
if prev == usize::MAX {
return Err(GraphalgError::NumericalInstability(
"phase produced no merge pair".to_string(),
));
}
for j in 0..n {
if j != prev && j != last {
let add = w[last * n + j];
w[prev * n + j] += add;
w[j * n + prev] += add;
}
}
active[last] = false;
let absorbed = std::mem::take(&mut groups[last]);
groups[prev].extend(absorbed);
remaining -= 1;
}
if best_partition.len() * 2 > n {
let in_set: Vec<bool> = {
let mut m = vec![false; n];
for &v in &best_partition {
m[v] = true;
}
m
};
best_partition = (0..n).filter(|&v| !in_set[v]).collect();
}
best_partition.sort_unstable();
Ok(GlobalMinCut {
value: best_value,
partition: best_partition,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn brute_force_min_cut(n: usize, edges: &[(usize, usize, f64)]) -> f64 {
let mut best = f64::INFINITY;
for mask in 1u32..(1u32 << (n - 1)) {
let side = |v: usize| -> bool {
if v == 0 {
false
} else {
(mask >> (v - 1)) & 1 == 1
}
};
let mut cut = 0.0;
for &(u, v, w) in edges {
if u == v {
continue;
}
if side(u) != side(v) {
cut += w;
}
}
if cut < best {
best = cut;
}
}
best
}
fn assert_cut_value_correct(n: usize, edges: &[(usize, usize, f64)]) -> GlobalMinCut {
let gmc = stoer_wagner_min_cut(n, edges).expect("ok");
let bf = brute_force_min_cut(n, edges);
assert!(
(gmc.value - bf).abs() < 1e-6,
"stoer-wagner={} brute={}",
gmc.value,
bf
);
let mut in_set = vec![false; n];
for &v in &gmc.partition {
in_set[v] = true;
}
assert!(!gmc.partition.is_empty(), "partition empty");
assert!(gmc.partition.len() < n, "partition is everything");
let mut realised = 0.0;
for &(u, v, w) in edges {
if u != v && in_set[u] != in_set[v] {
realised += w;
}
}
assert!(
(realised - gmc.value).abs() < 1e-6,
"partition realises {realised}, reported {}",
gmc.value
);
gmc
}
#[test]
fn canonical_stoer_wagner_example() {
let edges = [
(0, 1, 2.0),
(0, 4, 3.0),
(1, 2, 3.0),
(1, 4, 2.0),
(1, 5, 2.0),
(2, 3, 4.0),
(2, 6, 2.0),
(3, 6, 2.0),
(3, 7, 2.0),
(4, 5, 3.0),
(5, 6, 1.0),
(6, 7, 3.0),
];
let gmc = assert_cut_value_correct(8, &edges);
assert!((gmc.value - 4.0).abs() < 1e-9, "value={}", gmc.value);
}
#[test]
fn triangle_unit_weights() {
let edges = [(0, 1, 1.0), (1, 2, 1.0), (0, 2, 1.0)];
let gmc = assert_cut_value_correct(3, &edges);
assert!((gmc.value - 2.0).abs() < 1e-9);
}
#[test]
fn path_graph_cut_is_lightest_edge() {
let edges = [(0, 1, 5.0), (1, 2, 1.0), (2, 3, 5.0)];
let gmc = assert_cut_value_correct(4, &edges);
assert!((gmc.value - 1.0).abs() < 1e-9);
}
#[test]
fn single_bridge_min_cut() {
let edges = [
(0, 1, 10.0),
(1, 2, 10.0),
(0, 2, 10.0),
(3, 4, 10.0),
(4, 5, 10.0),
(3, 5, 10.0),
(2, 3, 1.0), ];
let gmc = assert_cut_value_correct(6, &edges);
assert!((gmc.value - 1.0).abs() < 1e-9, "value={}", gmc.value);
let mut in_set = vec![false; 6];
for &v in &gmc.partition {
in_set[v] = true;
}
assert_eq!(in_set[0], in_set[1]);
assert_eq!(in_set[1], in_set[2]);
assert_ne!(in_set[2], in_set[3]);
}
#[test]
fn disconnected_graph_zero_cut() {
let edges = [(0, 1, 7.0), (2, 3, 9.0)];
let gmc = stoer_wagner_min_cut(4, &edges).expect("ok");
assert!(gmc.value.abs() < 1e-9, "value={}", gmc.value);
}
#[test]
fn two_vertices_single_edge() {
let edges = [(0, 1, 3.5)];
let gmc = stoer_wagner_min_cut(2, &edges).expect("ok");
assert!((gmc.value - 3.5).abs() < 1e-9);
assert_eq!(gmc.partition.len(), 1);
}
#[test]
fn two_vertices_no_edge() {
let gmc = stoer_wagner_min_cut(2, &[]).expect("ok");
assert!(gmc.value.abs() < 1e-9);
}
#[test]
fn parallel_edges_sum() {
let edges = [(0, 1, 2.0), (0, 1, 4.0)];
let gmc = stoer_wagner_min_cut(2, &edges).expect("ok");
assert!((gmc.value - 6.0).abs() < 1e-9, "value={}", gmc.value);
}
#[test]
fn self_loops_ignored() {
let edges = [(0, 0, 100.0), (0, 1, 2.0), (1, 1, 50.0)];
let gmc = stoer_wagner_min_cut(2, &edges).expect("ok");
assert!((gmc.value - 2.0).abs() < 1e-9);
}
#[test]
fn complete_graph_k4_unit() {
let edges = [
(0, 1, 1.0),
(0, 2, 1.0),
(0, 3, 1.0),
(1, 2, 1.0),
(1, 3, 1.0),
(2, 3, 1.0),
];
let gmc = assert_cut_value_correct(4, &edges);
assert!((gmc.value - 3.0).abs() < 1e-9);
}
#[test]
fn weighted_random_small_matches_brute_force() {
type Case<'a> = (usize, &'a [(usize, usize, f64)]);
let cases: &[Case<'_>] = &[
(
5,
&[
(0, 1, 2.0),
(1, 2, 3.0),
(2, 3, 1.0),
(3, 4, 4.0),
(0, 4, 2.0),
(1, 3, 5.0),
],
),
(
6,
&[
(0, 1, 3.0),
(0, 2, 1.0),
(1, 2, 2.0),
(2, 3, 4.0),
(3, 4, 1.0),
(4, 5, 3.0),
(3, 5, 2.0),
(1, 4, 1.0),
],
),
(
5,
&[
(0, 1, 6.0),
(0, 2, 6.0),
(1, 2, 6.0),
(2, 3, 1.0),
(3, 4, 6.0),
(1, 4, 1.0),
],
),
];
for &(n, edges) in cases {
assert_cut_value_correct(n, edges);
}
}
#[test]
fn star_graph_min_cut_is_lightest_spoke() {
let edges = [(0, 1, 1.0), (0, 2, 2.0), (0, 3, 3.0)];
let gmc = assert_cut_value_correct(4, &edges);
assert!((gmc.value - 1.0).abs() < 1e-9);
}
#[test]
fn rejects_too_few_vertices() {
assert!(matches!(
stoer_wagner_min_cut(1, &[]),
Err(GraphalgError::InvalidParameter(_))
));
assert!(matches!(
stoer_wagner_min_cut(0, &[]),
Err(GraphalgError::InvalidParameter(_))
));
}
#[test]
fn rejects_negative_weight() {
assert!(matches!(
stoer_wagner_min_cut(3, &[(0, 1, -1.0)]),
Err(GraphalgError::NegativeWeight { .. })
));
}
#[test]
fn rejects_oob_endpoint() {
assert!(matches!(
stoer_wagner_min_cut(3, &[(0, 9, 1.0)]),
Err(GraphalgError::IndexOutOfBounds { .. })
));
}
#[test]
fn rejects_nonfinite_weight() {
assert!(matches!(
stoer_wagner_min_cut(3, &[(0, 1, f64::INFINITY)]),
Err(GraphalgError::InvalidEdgeWeight(_))
));
}
#[test]
fn partition_is_smaller_side() {
let edges = [
(0, 1, 5.0),
(0, 2, 5.0),
(0, 3, 5.0),
(1, 2, 5.0),
(1, 3, 5.0),
(2, 3, 5.0),
(3, 4, 1.0),
];
let gmc = stoer_wagner_min_cut(5, &edges).expect("ok");
assert!((gmc.value - 1.0).abs() < 1e-9);
assert_eq!(gmc.partition, vec![4]);
}
}