The demo below runs a simplified Goemans-Williamson-style rounding on a small graph. Each node starts with a random unit vector in 2-D (the SDP assigns unit vectors in n-D; 2-D is enough to visualize the idea). A random hyperplane through the origin splits the plane, assigning every node to side A or B depending on which half-plane its vector lands in.
<!-- Max-Cut with SDP-style hyperplane rounding (Goemans-Williamson) -->
<p class="hint">Each numbered node holds a unit vector (yellow arrow). A random hyperplane (dashed line) splits nodes into <b>side A (red)</b> and <b>side B (blue)</b>. Red edges cross the cut. Try many random hyperplanes to find a near-optimal partition.</p>
<div class="canvas-wrap">
<canvas id="cv" width="320" height="220"></canvas>
</div>
<div class="status" id="status">Press a button to start.</div>
<div class="btns">
<button id="btn-random" type="button">New random cut</button>
<button id="btn-best" type="button">Best of 20</button>
<button id="btn-reset" type="button" class="ghost">Reset vectors</button>
</div>
<div class="legend" id="legend"></div>
/* Layout and colour styles for the SDP rounding demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.canvas-wrap { display: flex; justify-content: center; margin: 0 0 .5rem; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; background: #f8fafc; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.legend { font-size: .82rem; color: #555; line-height: 1.6; }
.legend span { display: inline-block; width: 10px; height: 10px;
border-radius: 50%; margin-right: 4px; vertical-align: middle; }
.dot-a { background: #e63946; }
.dot-b { background: #457b9d; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 5-node graph: nodes 0-4, 8 edges.
// Optimal max-cut = 6 (e.g. partition {0,4} vs {1,2,3}).
const NODES = [
{ x: 80, y: 60 },
{ x: 240, y: 60 },
{ x: 160, y: 130 },
{ x: 60, y: 170 },
{ x: 260, y: 170 }
];
const EDGES = [[0,1],[0,2],[0,3],[1,2],[1,4],[2,3],[2,4],[3,4]];
// Optimal cut value for this graph: 6 edges.
// Partition A={0,4}, B={1,2,3} achieves it (0-indexed).
const OPTIMAL = 6;
// Each node gets a random 2-D unit vector (simulates the SDP solution).
// In full Goemans-Williamson the vectors live in n-D and are SDP-optimal.
let vecs = NODES.map(() => randomUnit());
function randomUnit() {
const a = Math.random() * 2 * Math.PI;
return [Math.cos(a), Math.sin(a)];
}
// Hyperplane normal vector from angle; classify nodes by which side they fall on.
function cutFromAngle(angle) {
const nx = -Math.sin(angle), ny = Math.cos(angle); // normal to the hyperplane
return NODES.map((_, i) => {
const dot = vecs[i][0] * nx + vecs[i][1] * ny;
return dot >= 0 ? 0 : 1; // side 0 = A, side 1 = B
});
}
function countCut(sides) {
return EDGES.filter(([u, v]) => sides[u] !== sides[v]).length;
}
let currentSides = NODES.map(() => 0);
let currentAngle = 0;
function draw(angle, sides) {
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
ctx.clearRect(0, 0, W, H);
// Draw the random hyperplane (dashed line through canvas centre).
const nx = -Math.sin(angle), ny = Math.cos(angle);
const cx = W / 2, cy = H / 2, len = Math.max(W, H);
ctx.save();
ctx.setLineDash([6, 4]);
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(cx - ny * len, cy + nx * len);
ctx.lineTo(cx + ny * len, cy - nx * len);
ctx.stroke();
ctx.restore();
// Draw edges — red if cut, grey otherwise.
for (const [u, v] of EDGES) {
const cut = sides[u] !== sides[v];
ctx.beginPath();
ctx.moveTo(NODES[u].x, NODES[u].y);
ctx.lineTo(NODES[v].x, NODES[v].y);
ctx.strokeStyle = cut ? '#e63946' : '#c9ccd1';
ctx.lineWidth = cut ? 2.2 : 1.4;
ctx.stroke();
}
// Draw nodes coloured by side.
const COLORS = ['#e63946', '#457b9d'];
for (let i = 0; i < NODES.length; i++) {
const { x, y } = NODES[i];
ctx.beginPath();
ctx.arc(x, y, 14, 0, 2 * Math.PI);
ctx.fillStyle = COLORS[sides[i]];
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 11px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(i + 1, x, y);
// Draw the unit vector as a short yellow arrow.
const vx = vecs[i][0] * 18, vy = vecs[i][1] * 18;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + vx, y + vy);
ctx.strokeStyle = '#ffd166';
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
function randomCut() {
currentAngle = Math.random() * 2 * Math.PI;
currentSides = cutFromAngle(currentAngle);
const n = countCut(currentSides);
draw(currentAngle, currentSides);
const pct = Math.round(100 * n / OPTIMAL);
const el = document.getElementById('status');
el.textContent = i18nFmt('Cut: %n% / %opt% edges (%pct%%)', { n: n, opt: OPTIMAL, pct: pct });
el.className = 'status ok';
updateLegend(currentSides);
}
function bestOf20() {
let bestSides = null, bestN = -1, bestAngle = 0;
for (let t = 0; t < 20; t++) {
const a = Math.random() * 2 * Math.PI;
const s = cutFromAngle(a);
const n = countCut(s);
if (n > bestN) { bestN = n; bestSides = s; bestAngle = a; }
}
currentAngle = bestAngle;
currentSides = bestSides;
draw(currentAngle, currentSides);
const pct = Math.round(100 * bestN / OPTIMAL);
const el = document.getElementById('status');
el.textContent = i18nFmt('Best of 20 cuts: %n% / %opt% edges (%pct%%)', { n: bestN, opt: OPTIMAL, pct: pct });
el.className = 'status ok';
updateLegend(currentSides);
}
function updateLegend(sides) {
const aNodes = sides.map((s, i) => s === 0 ? i + 1 : null).filter(Boolean).join(', ');
const bNodes = sides.map((s, i) => s === 1 ? i + 1 : null).filter(Boolean).join(', ');
document.getElementById('legend').innerHTML =
'<span class="dot-a"></span> Side A: nodes ' + aNodes +
' <span class="dot-b"></span> Side B: nodes ' + bNodes +
'<br>Optimal cut: ' + OPTIMAL + ' edges';
}
function resetDemo() {
vecs = NODES.map(() => randomUnit());
currentSides = NODES.map(() => 0);
currentAngle = 0;
draw(currentAngle, currentSides);
const el = document.getElementById('status');
el.textContent = 'Press a button to start.';
el.className = 'status info';
document.getElementById('legend').innerHTML =
'Optimal cut: ' + OPTIMAL + ' edges';
}
document.getElementById('btn-random').addEventListener('click', randomCut);
document.getElementById('btn-best').addEventListener('click', bestOf20);
document.getElementById('btn-reset').addEventListener('click', resetDemo);
// Initial draw with all nodes in side A (no cut yet).
draw(currentAngle, currentSides);
document.getElementById('legend').innerHTML = 'Optimal cut: ' + OPTIMAL + ' edges';
<!-- Max-Cut with SDP-style hyperplane rounding (Goemans-Williamson) -->
<p class="hint">Each numbered node holds a unit vector (yellow arrow). A random hyperplane (dashed line) splits nodes into <b>side A (red)</b> and <b>side B (blue)</b>. Red edges cross the cut. Try many random hyperplanes to find a near-optimal partition.</p>
<div class="canvas-wrap">
<canvas id="cv" width="320" height="220"></canvas>
</div>
<div class="status" id="status">Press a button to start.</div>
<div class="btns">
<button id="btn-random" type="button">New random cut</button>
<button id="btn-best" type="button">Best of 20</button>
<button id="btn-reset" type="button" class="ghost">Reset vectors</button>
</div>
<div class="legend" id="legend"></div>
/* Layout and colour styles for the SDP rounding demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.canvas-wrap { display: flex; justify-content: center; margin: 0 0 .5rem; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; background: #f8fafc; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.legend { font-size: .82rem; color: #555; line-height: 1.6; }
.legend span { display: inline-block; width: 10px; height: 10px;
border-radius: 50%; margin-right: 4px; vertical-align: middle; }
.dot-a { background: #e63946; }
.dot-b { background: #457b9d; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 5-node graph: nodes 0-4, 8 edges.
// Optimal max-cut = 6 (e.g. partition {0,4} vs {1,2,3}).
const NODES = [
{ x: 80, y: 60 },
{ x: 240, y: 60 },
{ x: 160, y: 130 },
{ x: 60, y: 170 },
{ x: 260, y: 170 }
];
const EDGES = [[0,1],[0,2],[0,3],[1,2],[1,4],[2,3],[2,4],[3,4]];
// Optimal cut value for this graph: 6 edges.
// Partition A={0,4}, B={1,2,3} achieves it (0-indexed).
const OPTIMAL = 6;
// Each node gets a random 2-D unit vector (simulates the SDP solution).
// In full Goemans-Williamson the vectors live in n-D and are SDP-optimal.
let vecs = NODES.map(() => randomUnit());
function randomUnit() {
const a = Math.random() * 2 * Math.PI;
return [Math.cos(a), Math.sin(a)];
}
// Hyperplane normal vector from angle; classify nodes by which side they fall on.
function cutFromAngle(angle) {
const nx = -Math.sin(angle), ny = Math.cos(angle); // normal to the hyperplane
return NODES.map((_, i) => {
const dot = vecs[i][0] * nx + vecs[i][1] * ny;
return dot >= 0 ? 0 : 1; // side 0 = A, side 1 = B
});
}
function countCut(sides) {
return EDGES.filter(([u, v]) => sides[u] !== sides[v]).length;
}
let currentSides = NODES.map(() => 0);
let currentAngle = 0;
function draw(angle, sides) {
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
ctx.clearRect(0, 0, W, H);
// Draw the random hyperplane (dashed line through canvas centre).
const nx = -Math.sin(angle), ny = Math.cos(angle);
const cx = W / 2, cy = H / 2, len = Math.max(W, H);
ctx.save();
ctx.setLineDash([6, 4]);
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(cx - ny * len, cy + nx * len);
ctx.lineTo(cx + ny * len, cy - nx * len);
ctx.stroke();
ctx.restore();
// Draw edges — red if cut, grey otherwise.
for (const [u, v] of EDGES) {
const cut = sides[u] !== sides[v];
ctx.beginPath();
ctx.moveTo(NODES[u].x, NODES[u].y);
ctx.lineTo(NODES[v].x, NODES[v].y);
ctx.strokeStyle = cut ? '#e63946' : '#c9ccd1';
ctx.lineWidth = cut ? 2.2 : 1.4;
ctx.stroke();
}
// Draw nodes coloured by side.
const COLORS = ['#e63946', '#457b9d'];
for (let i = 0; i < NODES.length; i++) {
const { x, y } = NODES[i];
ctx.beginPath();
ctx.arc(x, y, 14, 0, 2 * Math.PI);
ctx.fillStyle = COLORS[sides[i]];
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 11px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(i + 1, x, y);
// Draw the unit vector as a short yellow arrow.
const vx = vecs[i][0] * 18, vy = vecs[i][1] * 18;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + vx, y + vy);
ctx.strokeStyle = '#ffd166';
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
function randomCut() {
currentAngle = Math.random() * 2 * Math.PI;
currentSides = cutFromAngle(currentAngle);
const n = countCut(currentSides);
draw(currentAngle, currentSides);
const pct = Math.round(100 * n / OPTIMAL);
const el = document.getElementById('status');
el.textContent = i18nFmt('Cut: %n% / %opt% edges (%pct%%)', { n: n, opt: OPTIMAL, pct: pct });
el.className = 'status ok';
updateLegend(currentSides);
}
function bestOf20() {
let bestSides = null, bestN = -1, bestAngle = 0;
for (let t = 0; t < 20; t++) {
const a = Math.random() * 2 * Math.PI;
const s = cutFromAngle(a);
const n = countCut(s);
if (n > bestN) { bestN = n; bestSides = s; bestAngle = a; }
}
currentAngle = bestAngle;
currentSides = bestSides;
draw(currentAngle, currentSides);
const pct = Math.round(100 * bestN / OPTIMAL);
const el = document.getElementById('status');
el.textContent = i18nFmt('Best of 20 cuts: %n% / %opt% edges (%pct%%)', { n: bestN, opt: OPTIMAL, pct: pct });
el.className = 'status ok';
updateLegend(currentSides);
}
function updateLegend(sides) {
const aNodes = sides.map((s, i) => s === 0 ? i + 1 : null).filter(Boolean).join(', ');
const bNodes = sides.map((s, i) => s === 1 ? i + 1 : null).filter(Boolean).join(', ');
document.getElementById('legend').innerHTML =
'<span class="dot-a"></span> Side A: nodes ' + aNodes +
' <span class="dot-b"></span> Side B: nodes ' + bNodes +
'<br>Optimal cut: ' + OPTIMAL + ' edges';
}
function resetDemo() {
vecs = NODES.map(() => randomUnit());
currentSides = NODES.map(() => 0);
currentAngle = 0;
draw(currentAngle, currentSides);
const el = document.getElementById('status');
el.textContent = 'Press a button to start.';
el.className = 'status info';
document.getElementById('legend').innerHTML =
'Optimal cut: ' + OPTIMAL + ' edges';
}
document.getElementById('btn-random').addEventListener('click', randomCut);
document.getElementById('btn-best').addEventListener('click', bestOf20);
document.getElementById('btn-reset').addEventListener('click', resetDemo);
// Initial draw with all nodes in side A (no cut yet).
draw(currentAngle, currentSides);
document.getElementById('legend').innerHTML = 'Optimal cut: ' + OPTIMAL + ' edges';
Comments
Loading comments...