use crate::graph::Graph;
use crate::matching::min_weight_perfect;
use crate::planar;
use std::collections::{BTreeMap, BinaryHeap};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Params {
pub scale: f64,
}
impl Default for Params {
fn default() -> Self {
Params { scale: 1.0 }
}
}
#[derive(Clone, Debug)]
pub struct Outcome {
pub state: Vec<i8>,
pub cut: f64,
pub energy: f64,
pub faces: usize,
pub odd_faces: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Error {
HasFields { node: usize, h: f64 },
NotEmbeddable(planar::Refusal),
NotIntegral { u: usize, v: usize, w: f64 },
NoMatching,
NotACut,
Disagreement { via_join: f64, via_state: f64 },
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Error::HasFields { node, h } => write!(
f,
"node {node} carries a field of {h}, and max-cut on a graph with fields is a \
different problem: the standard reduction adds an apex vertex joined to every \
node, which is not planar"
),
Error::NotEmbeddable(r) => write!(f, "no planar embedding: {r}"),
Error::NotIntegral { u, v, w } => write!(
f,
"the coupling between {u} and {v} scales to {w}, which is not an integer. The \
matching this rests on is exact only in exact arithmetic, and rounding here moves \
the optimum rather than the last digit -- pass a scale that makes every weight \
whole"
),
Error::NoMatching => write!(
f,
"the odd-degree dual vertices admit no perfect matching, which cannot happen for a \
correct reduction -- this is a defect here, not a hard instance"
),
Error::NotACut => write!(
f,
"the edge set recovered from the dual does not two-colour, so it is not a cut. The \
reduction is wrong and no number is returned"
),
Error::Disagreement { via_join, via_state } => write!(
f,
"the T-join says the cut is {via_join} and the recovered state says {via_state}. \
Two disjoint computations of the same quantity disagree, so neither is reported"
),
}
}
}
pub fn solve(g: &Graph, p: &Params) -> Result<Outcome, Error> {
for (i, &h) in g.h.iter().enumerate() {
if h != 0.0 {
return Err(Error::HasFields { node: i, h });
}
}
if g.n == 0 {
return Ok(Outcome { state: Vec::new(), cut: 0.0, energy: 0.0, faces: 0, odd_faces: 0 });
}
let emb = planar::embed(g)
.ok_or_else(|| Error::NotEmbeddable(planar::why(g).unwrap_or(planar::Refusal::NotPlanar)))?;
let mut edges: Vec<(usize, usize, i64)> = Vec::new();
let mut seen: BTreeMap<(usize, usize), i64> = BTreeMap::new();
for u in 0..g.n {
for k in g.offset[u]..g.offset[u + 1] {
let v = g.nbr[k] as usize;
if v <= u {
continue;
}
let scaled = -g.w[k] * p.scale;
if !scaled.is_finite() || (scaled - scaled.round()).abs() > 1e-9 {
return Err(Error::NotIntegral { u, v, w: scaled });
}
seen.insert((u, v), scaled.round() as i64);
}
}
edges.extend(seen.into_iter().map(|((u, v), w)| (u, v, w)));
let total: i64 = edges.iter().map(|e| e.2).sum();
let faces = emb.faces();
let mut face_of: BTreeMap<(usize, usize), usize> = BTreeMap::new();
for (fi, face) in faces.iter().enumerate() {
for &d in face {
face_of.insert(d, fi);
}
}
let nf = faces.len();
let mut dual: Vec<(usize, usize, i64, usize)> = Vec::with_capacity(edges.len());
for (ei, &(u, v, w)) in edges.iter().enumerate() {
let a = *face_of.get(&(u, v)).ok_or(Error::NotACut)?;
let b = *face_of.get(&(v, u)).ok_or(Error::NotACut)?;
dual.push((a, b, w, ei));
}
let mut parity = vec![false; nf];
for (a, b, w, _) in &dual {
parity[*a] ^= true;
parity[*b] ^= true;
if *w < 0 {
parity[*a] ^= true;
parity[*b] ^= true;
}
}
let base: i64 = dual.iter().filter(|e| e.2 < 0).map(|e| e.2).sum();
let mut preselected = vec![false; dual.len()];
for (i, e) in dual.iter().enumerate() {
preselected[i] = e.2 < 0;
}
let odd: Vec<usize> = (0..nf).filter(|&f| parity[f]).collect();
if odd.len() % 2 == 1 {
return Err(Error::NotACut);
}
let mut adj: Vec<Vec<(usize, i64, usize)>> = vec![Vec::new(); nf];
for (i, &(a, b, w, _)) in dual.iter().enumerate() {
adj[a].push((b, w.abs(), i));
adj[b].push((a, w.abs(), i));
}
let k = odd.len();
let mut cost = vec![0i64; k * k];
let mut preds: Vec<Vec<(usize, usize)>> = Vec::with_capacity(k);
for (si, &s) in odd.iter().enumerate() {
let (dist, pred) = dijkstra(nf, &adj, s);
preds.push(pred);
for (ti, &t) in odd.iter().enumerate() {
cost[si * k + ti] = if si == ti { 0 } else { dist[t] };
}
}
let (mate, join_weight) = min_weight_perfect(k, &cost).ok_or(Error::NoMatching)?;
let mut in_f = preselected;
for si in 0..k {
let ti = mate[si];
if ti < si {
continue; }
let mut cur = odd[ti];
while cur != odd[si] {
let (prev, ei) = preds[si][cur];
if prev == usize::MAX {
return Err(Error::NoMatching);
}
in_f[ei] ^= true;
cur = prev;
}
}
let mut cut_edge = vec![false; edges.len()];
for (i, &(_, _, _, ei)) in dual.iter().enumerate() {
cut_edge[ei] = !in_f[i];
}
let state = two_colour(g.n, &edges, &cut_edge).ok_or(Error::NotACut)?;
let via_join = (total - (base + join_weight)) as f64 / p.scale;
let mut via_state = 0.0f64;
for &(u, v, w) in &edges {
if state[u] != state[v] {
via_state += w as f64 / p.scale;
}
}
if (via_join - via_state).abs() > 1e-6 {
return Err(Error::Disagreement { via_join, via_state });
}
let energy = g.energy(&state);
Ok(Outcome { state, cut: via_state, energy, faces: nf, odd_faces: k })
}
fn dijkstra(
n: usize,
adj: &[Vec<(usize, i64, usize)>],
s: usize,
) -> (Vec<i64>, Vec<(usize, usize)>) {
let mut dist = vec![i64::MAX; n];
let mut pred = vec![(usize::MAX, usize::MAX); n];
let mut heap: BinaryHeap<(i64, usize)> = BinaryHeap::new();
dist[s] = 0;
heap.push((0, s));
while let Some((nd, u)) = heap.pop() {
let d = -nd;
if d > dist[u] {
continue;
}
for &(v, w, ei) in &adj[u] {
let alt = d + w;
if alt < dist[v] {
dist[v] = alt;
pred[v] = (u, ei);
heap.push((-alt, v));
}
}
}
(dist, pred)
}
fn two_colour(n: usize, edges: &[(usize, usize, i64)], cut: &[bool]) -> Option<Vec<i8>> {
let mut adj: Vec<Vec<(usize, bool)>> = vec![Vec::new(); n];
for (i, &(u, v, _)) in edges.iter().enumerate() {
adj[u].push((v, cut[i]));
adj[v].push((u, cut[i]));
}
let mut s = vec![0i8; n];
for root in 0..n {
if s[root] != 0 {
continue;
}
s[root] = 1;
let mut stack = vec![root];
while let Some(u) = stack.pop() {
for &(v, is_cut) in &adj[u] {
let want = if is_cut { -s[u] } else { s[u] };
if s[v] == 0 {
s[v] = want;
stack.push(v);
} else if s[v] != want {
return None; }
}
}
}
Some(s)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::GraphBuilder;
use crate::ising::{grid2d, lattice2d, ring};
use crate::rng::Pcg;
use crate::{branch, bls};
fn truth(g: &Graph) -> f64 {
let o = branch::solve(g, &branch::Params { max_nodes: 50_000_000, ..Default::default() });
assert!(o.proved_optimal, "the control must actually prove its answer");
(-weight(g) - o.energy) / 2.0
}
fn weight(g: &Graph) -> f64 {
let mut s = 0.0;
for u in 0..g.n {
for k in g.offset[u]..g.offset[u + 1] {
if (g.nbr[k] as usize) > u {
s += g.w[k];
}
}
}
s
}
#[test]
fn it_agrees_with_exhaustive_proof_on_planar_instances() {
for seed in 0..60u64 {
let mut rng = Pcg::new(seed, 0x91A4_AC07);
let (w, h) = (2 + (rng.next_u32() % 3) as usize, 2 + (rng.next_u32() % 3) as usize);
if w * h < 4 {
continue;
}
let mut gb = GraphBuilder::new(w * h);
for y in 0..h {
for x in 0..w {
let i = y * w + x;
if x + 1 < w {
gb.couple(i, i + 1, if rng.f64() < 0.5 { 1.0 } else { -1.0 });
}
if y + 1 < h {
gb.couple(i, i + w, if rng.f64() < 0.5 { 1.0 } else { -1.0 });
}
}
}
let g = gb.build();
let Ok(out) = solve(&g, &Params::default()) else { continue };
let want = truth(&g);
assert!(
(out.cut - want).abs() < 1e-9,
"seed {seed} ({w}x{h}): planarcut says {}, branch and bound proves {want}",
out.cut
);
let mut got = 0.0;
for u in 0..g.n {
for k in g.offset[u]..g.offset[u + 1] {
if (g.nbr[k] as usize) > u && out.state[u] != out.state[g.nbr[k] as usize] {
got -= g.w[k];
}
}
}
assert!((got - out.cut).abs() < 1e-9, "seed {seed}: the state does not make that cut");
assert!(out.state.iter().all(|&v| v == 1 || v == -1));
}
}
#[test]
fn no_heuristic_ever_beats_it() {
for (w, h) in [(6usize, 6usize), (8, 5), (10, 4)] {
let mut rng = Pcg::new(w as u64 * 31 + h as u64, 0xB0_11);
let mut gb = GraphBuilder::new(w * h);
for y in 0..h {
for x in 0..w {
let i = y * w + x;
if x + 1 < w {
gb.couple(i, i + 1, if rng.f64() < 0.5 { 1.0 } else { -1.0 });
}
if y + 1 < h {
gb.couple(i, i + w, if rng.f64() < 0.5 { 1.0 } else { -1.0 });
}
}
}
let g = gb.build();
let exact = solve(&g, &Params::default()).expect("a grid is planar");
let heur = bls::search(&g, &bls::Params { iterations: 200_000, ..Default::default() }, 5);
let mut hcut = 0.0;
for u in 0..g.n {
for k in g.offset[u]..g.offset[u + 1] {
if (g.nbr[k] as usize) > u && heur.state[u] != heur.state[g.nbr[k] as usize] {
hcut -= g.w[k];
}
}
}
assert!(
hcut <= exact.cut + 1e-9,
"{w}x{h}: breakout local search found {hcut}, above a claimed EXACT maximum of {}",
exact.cut
);
}
}
#[test]
fn bipartite_and_odd_cycle_closed_forms() {
for (w, h) in [(2usize, 2usize), (4, 4), (7, 5), (12, 9)] {
let m = (w * (h - 1) + h * (w - 1)) as f64;
let anti = solve(&grid2d(w, h, -1.0), &Params::default()).unwrap();
assert_eq!(anti.cut, m, "{w}x{h}: a bipartite antiferromagnet cuts everything");
assert_eq!(anti.energy, -m);
assert_eq!(anti.faces, (w - 1) * (h - 1) + 1);
let ferro = solve(&grid2d(w, h, 1.0), &Params::default()).unwrap();
assert_eq!(ferro.cut, 0.0, "{w}x{h}: a ferromagnet's ground state cuts nothing");
assert_eq!(ferro.energy, -m);
}
assert_eq!(solve(&ring(7, -1.0, 0.0), &Params::default()).unwrap().cut, 6.0);
assert_eq!(solve(&ring(7, -1.0, 0.0), &Params::default()).unwrap().energy, -5.0);
}
#[test]
fn each_refusal_says_which_one_it_is() {
let mut gb = GraphBuilder::new(4);
gb.couple(0, 1, 1.0);
gb.couple(1, 2, 1.0);
gb.couple(2, 3, 1.0);
gb.couple(3, 0, 1.0);
gb.bias(2, 0.5);
assert!(matches!(solve(&gb.build(), &Params::default()), Err(Error::HasFields { node: 2, .. })));
let e = solve(&lattice2d(4, 1.0), &Params::default()).unwrap_err();
assert!(matches!(e, Error::NotEmbeddable(_)), "{e}");
assert!(e.to_string().contains("not planar"), "{e}");
let mut gb = GraphBuilder::new(4);
gb.couple(0, 1, 1.0);
gb.couple(1, 2, 0.5);
gb.couple(2, 3, 1.0);
gb.couple(3, 0, 1.0);
let g = gb.build();
assert!(matches!(solve(&g, &Params::default()), Err(Error::NotIntegral { u: 1, v: 2, .. })));
assert!(solve(&g, &Params { scale: 2.0 }).is_ok());
}
#[test]
fn all_negative_weights_are_exact_too() {
for (w, h) in [(3usize, 3usize), (4, 3), (4, 4)] {
let mut gb = GraphBuilder::new(w * h);
for y in 0..h {
for x in 0..w {
let i = y * w + x;
if x + 1 < w {
gb.couple(i, i + 1, -1.0);
}
if y + 1 < h {
gb.couple(i, i + w, -1.0);
}
}
}
let g = gb.build();
let out = solve(&g, &Params::default()).unwrap();
assert!((out.cut - truth(&g)).abs() < 1e-9, "{w}x{h}: {} vs proof", out.cut);
assert_eq!(out.cut, (w * (h - 1) + h * (w - 1)) as f64);
}
}
#[test]
fn an_empty_graph_is_answered_rather_than_attempted() {
let out = solve(&GraphBuilder::new(0).build(), &Params::default()).unwrap();
assert_eq!((out.cut, out.energy, out.faces), (0.0, 0.0, 0));
}
}