Tutorial: MAX-CUT and optimality proofs¶
MAX-CUT asks for a partition of the vertices of a graph that maximizes the number (or weight) of cut edges. With
spins \(x_i\in\{-1,+1\}\) the cut value is \(\sum_{ij\in E}w_{ij}(1-x_ix_j)/2\), a quadratic polynomial, so it is a POP with
boolean=True. This tutorial shows how a relaxation bound becomes a proof of optimality.
Order 1 sometimes suffices¶
Order 1 is the Goemans–Williamson / Shor SDP. On the Petersen graph (maximum cut 12) it already proves optimality:
import networkx as nx
import moment_sos as po
G = nx.petersen_graph()
s = po.variables(G.number_of_nodes())
problem = po.Problem(sum((1 - s[i]*s[j]) / 2 for i, j in G.edges()), boolean=True)
res = po.solve(problem, order=1)
x, value = res.round() # best cut found: a lower bound
print(res.bound, value) # 12.5 and 12
assert int(res.bound) == value == 12 # the cut is at most floor(12.5) = 12 and a cut of 12 exists: optimal
The logic: for integer weights the optimum is an integer, so floor(bound) is also an upper bound. If a feasible point
attains it, that point is optimal, and the pair (bound, point) is the proof.
When order 1 is not enough¶
On a random graph the order-1 bound is often too weak, and the order-2 relaxation (a much bigger block, size \(1+n+\binom n2\)) is needed:
import networkx as nx
import numpy as np
import moment_sos as po
G = nx.gnp_random_graph(20, 0.3, seed=3)
s = po.variables(20)
problem = po.Problem(sum((1 - s[i]*s[j]) / 2 for i, j in G.edges()), boolean=True)
r1 = po.solve(problem, order=1)
x, value = r1.round()
print("order 1:", round(r1.bound, 3), " best cut:", value) # 39.1 vs 38: floor(39.1) = 39 != 38
assert np.floor(r1.bound + 1e-9) > value # order 1 cannot prove optimality here
r2 = po.solve(problem, order=2, etas=po.SHORT_ETAS, stop_at=value + 1 - 1e-9)
print("order 2:", round(r2.bound, 4)) # 38.876
assert np.floor(r2.bound + 1e-9) == value # proved optimal
Two options make this fast:
etas=po.SHORT_ETASstarts the smoothing continuation coarser; for 0/1 and ±1-weighted MAX-CUT this certifies integer optima about 20× faster than the default schedule (not tested on other problem classes).stop_at=value + 1 - 1e-9stops the moment the certified bound drops belowvalue + 1, i.e. as soon as optimality is proved. You do not wait for convergence.
Making the proof airtight¶
res.bound is exact up to the eigensolver’s floating-point error. For a proof that holds in exact arithmetic use the
rigorous certificate:
rig = r2.rigorous()
print(rig["ok"], rig["bound"])
assert rig["ok"] and np.floor(rig["bound"]) == value
See Certificates and primal recovery for what it certifies (the normalized floating-point problem; for unit weights that is exact).
Weighted graphs¶
Weights are ordinary coefficients: sum(w[i, j] * (1 - s[i]*s[j]) / 2 for ...). The optimum is then an integer only for
integer weights; with non-integer weights compare res.bound with value and read the gap, there is no rounding down.
On instances with weights spanning many orders of magnitude (the Biq Mac torus instances have weights up to \(10^5\)) the
smoothing solver is noticeably less accurate; see Benchmarks and limitations.