Below is a small graph. Each node sits on the left or right side; click a node to flip it across. Edges that cross the divide turn red — those are the ones you cut. The counter shows how many edges you're cutting and whether the two sides are balanced.
<p class="hint">Click a node to flip it to the other side. Edges that cross the divide turn <b>red</b> — those are the ones you <b>cut</b>. Keep the two sides balanced while making the cut as small as you can.</p>
<svg id="graph" viewBox="0 0 360 220" class="graph"></svg>
<div class="status" id="status"></div>
<div class="btns">
<button id="best" type="button">Best split</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.graph { width: 100%; height: auto; background: #f4f7fa; border-radius: 10px;
border: 1px solid #dde5ec; display: block; touch-action: manipulation; }
.divider { stroke: #9aa7b4; stroke-width: 1.5; stroke-dasharray: 5 5; }
.edge { stroke: #9fb0bf; stroke-width: 2.5; }
.edge.cut { stroke: #e63946; stroke-width: 3; }
.node { cursor: pointer; }
.node circle { stroke: #fff; stroke-width: 2; transition: fill .12s; }
.node.left circle { fill: #1d3557; }
.node.right circle { fill: #2a9d8f; }
.node text { fill: #fff; font: 700 13px ui-monospace, monospace;
text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.label { font: 700 11px system-ui, sans-serif; text-anchor: middle; fill: #5a6b7b; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; }
.status .ok { color: #0a7d33; }
.status .bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Balanced graph partitioning ("minimum bisection"): split the nodes into two
// equal halves while cutting as few edges as possible. Counting the cut of one
// split is instant; finding the optimal balanced split is NP-hard, so "Best
// split" here brute-forces every balanced division (fine for 6 nodes, hopeless
// as the graph grows since the number of splits roughly doubles per node).
const NS = 'http://www.w3.org/2000/svg';
// 6 nodes laid out in two loose clusters; side = 0 (left) or 1 (right).
const NODES = [
{ id: 'A', x: 90, y: 55 },
{ id: 'B', x: 70, y: 130 },
{ id: 'C', x: 130, y: 95 },
{ id: 'D', x: 230, y: 95 },
{ id: 'E', x: 290, y: 55 },
{ id: 'F', x: 270, y: 130 },
];
// Two tight triangles (A-B-C, D-E-F) joined by a single bridge C-D.
const EDGES = [ ['A','B'],['A','C'],['B','C'],['C','D'],['D','E'],['D','F'],['E','F'] ];
const idx = {}; NODES.forEach((n,i)=>idx[n.id]=i);
let side = [0,0,0,1,1,1]; // initial split: ABC | DEF (the natural minimum)
function cutCount(s){ let c=0; for(const [u,v] of EDGES) if(s[idx[u]]!==s[idx[v]]) c++; return c; }
function balance(s){ const l=s.filter(x=>x===0).length; return { l, r:s.length-l }; }
function render(){
const svg=document.getElementById('graph'); svg.innerHTML='';
const div=document.createElementNS(NS,'line');
div.setAttribute('x1',180); div.setAttribute('y1',8);
div.setAttribute('x2',180); div.setAttribute('y2',212); div.setAttribute('class','divider');
svg.appendChild(div);
const lab=(x,t)=>{ const e=document.createElementNS(NS,'text'); e.setAttribute('x',x);
e.setAttribute('y',204); e.setAttribute('class','label'); e.textContent=t; svg.appendChild(e); };
lab(60,'LEFT'); lab(300,'RIGHT');
// edges first so nodes draw on top
for(const [u,v] of EDGES){ const a=NODES[idx[u]], b=NODES[idx[v]];
const ln=document.createElementNS(NS,'line');
ln.setAttribute('x1',a.x); ln.setAttribute('y1',a.y);
ln.setAttribute('x2',b.x); ln.setAttribute('y2',b.y);
ln.setAttribute('class','edge'+(side[idx[u]]!==side[idx[v]]?' cut':''));
svg.appendChild(ln); }
NODES.forEach((n,i)=>{ const g=document.createElementNS(NS,'g');
g.setAttribute('class','node '+(side[i]===0?'left':'right'));
const c=document.createElementNS(NS,'circle');
c.setAttribute('cx',n.x); c.setAttribute('cy',n.y); c.setAttribute('r',18);
const t=document.createElementNS(NS,'text');
t.setAttribute('x',n.x); t.setAttribute('y',n.y); t.textContent=n.id;
g.appendChild(c); g.appendChild(t);
g.addEventListener('click',()=>{ side[i]=1-side[i]; render(); });
svg.appendChild(g); });
status();
}
function status(){
const b=balance(side), cut=cutCount(side);
const ok = b.l===b.r;
const balTxt = ok
? fmt('<span class="ok">balanced (%l% vs %r%)</span>', { l: b.l, r: b.r })
: fmt('<span class="bad">unbalanced (%l% vs %r%)</span>', { l: b.l, r: b.r });
document.getElementById('status').innerHTML =
fmt('Cut size: <b>%cut%</b> %v% · %balTxt%', { cut: cut, v: cut===1?'edge':'edges', balTxt: balTxt });
}
// Brute force every balanced split (exactly n/2 on each side) and keep the min cut.
function best(){
const n=NODES.length, half=n/2; let bestCut=Infinity, bestS=null, checked=0;
for(let mask=0; mask<(1<<n); mask++){
let ones=0; for(let i=0;i<n;i++) if(mask&(1<<i)) ones++;
if(ones!==half) continue; // only balanced splits
checked++;
const s=[]; for(let i=0;i<n;i++) s.push((mask>>i)&1);
const c=cutCount(s);
if(c<bestCut){ bestCut=c; bestS=s.slice(); }
}
side=bestS; render();
document.getElementById('status').innerHTML +=
" " + fmt('— <span class="ok">optimum = %bestCut% (checked %checked% balanced splits)</span>', { bestCut: bestCut, checked: checked });
console.log('Best balanced cut =', bestCut, 'over', checked, 'balanced splits');
}
document.getElementById('best').addEventListener('click',best);
document.getElementById('reset').addEventListener('click',()=>{ side=[0,1,0,1,0,1]; render(); });
render();
<p class="hint">Click a node to flip it to the other side. Edges that cross the divide turn <b>red</b> — those are the ones you <b>cut</b>. Keep the two sides balanced while making the cut as small as you can.</p>
<svg id="graph" viewBox="0 0 360 220" class="graph"></svg>
<div class="status" id="status"></div>
<div class="btns">
<button id="best" type="button">Best split</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.graph { width: 100%; height: auto; background: #f4f7fa; border-radius: 10px;
border: 1px solid #dde5ec; display: block; touch-action: manipulation; }
.divider { stroke: #9aa7b4; stroke-width: 1.5; stroke-dasharray: 5 5; }
.edge { stroke: #9fb0bf; stroke-width: 2.5; }
.edge.cut { stroke: #e63946; stroke-width: 3; }
.node { cursor: pointer; }
.node circle { stroke: #fff; stroke-width: 2; transition: fill .12s; }
.node.left circle { fill: #1d3557; }
.node.right circle { fill: #2a9d8f; }
.node text { fill: #fff; font: 700 13px ui-monospace, monospace;
text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.label { font: 700 11px system-ui, sans-serif; text-anchor: middle; fill: #5a6b7b; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; }
.status .ok { color: #0a7d33; }
.status .bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Balanced graph partitioning ("minimum bisection"): split the nodes into two
// equal halves while cutting as few edges as possible. Counting the cut of one
// split is instant; finding the optimal balanced split is NP-hard, so "Best
// split" here brute-forces every balanced division (fine for 6 nodes, hopeless
// as the graph grows since the number of splits roughly doubles per node).
const NS = 'http://www.w3.org/2000/svg';
// 6 nodes laid out in two loose clusters; side = 0 (left) or 1 (right).
const NODES = [
{ id: 'A', x: 90, y: 55 },
{ id: 'B', x: 70, y: 130 },
{ id: 'C', x: 130, y: 95 },
{ id: 'D', x: 230, y: 95 },
{ id: 'E', x: 290, y: 55 },
{ id: 'F', x: 270, y: 130 },
];
// Two tight triangles (A-B-C, D-E-F) joined by a single bridge C-D.
const EDGES = [ ['A','B'],['A','C'],['B','C'],['C','D'],['D','E'],['D','F'],['E','F'] ];
const idx = {}; NODES.forEach((n,i)=>idx[n.id]=i);
let side = [0,0,0,1,1,1]; // initial split: ABC | DEF (the natural minimum)
function cutCount(s){ let c=0; for(const [u,v] of EDGES) if(s[idx[u]]!==s[idx[v]]) c++; return c; }
function balance(s){ const l=s.filter(x=>x===0).length; return { l, r:s.length-l }; }
function render(){
const svg=document.getElementById('graph'); svg.innerHTML='';
const div=document.createElementNS(NS,'line');
div.setAttribute('x1',180); div.setAttribute('y1',8);
div.setAttribute('x2',180); div.setAttribute('y2',212); div.setAttribute('class','divider');
svg.appendChild(div);
const lab=(x,t)=>{ const e=document.createElementNS(NS,'text'); e.setAttribute('x',x);
e.setAttribute('y',204); e.setAttribute('class','label'); e.textContent=t; svg.appendChild(e); };
lab(60,'LEFT'); lab(300,'RIGHT');
// edges first so nodes draw on top
for(const [u,v] of EDGES){ const a=NODES[idx[u]], b=NODES[idx[v]];
const ln=document.createElementNS(NS,'line');
ln.setAttribute('x1',a.x); ln.setAttribute('y1',a.y);
ln.setAttribute('x2',b.x); ln.setAttribute('y2',b.y);
ln.setAttribute('class','edge'+(side[idx[u]]!==side[idx[v]]?' cut':''));
svg.appendChild(ln); }
NODES.forEach((n,i)=>{ const g=document.createElementNS(NS,'g');
g.setAttribute('class','node '+(side[i]===0?'left':'right'));
const c=document.createElementNS(NS,'circle');
c.setAttribute('cx',n.x); c.setAttribute('cy',n.y); c.setAttribute('r',18);
const t=document.createElementNS(NS,'text');
t.setAttribute('x',n.x); t.setAttribute('y',n.y); t.textContent=n.id;
g.appendChild(c); g.appendChild(t);
g.addEventListener('click',()=>{ side[i]=1-side[i]; render(); });
svg.appendChild(g); });
status();
}
function status(){
const b=balance(side), cut=cutCount(side);
const ok = b.l===b.r;
const balTxt = ok
? fmt('<span class="ok">balanced (%l% vs %r%)</span>', { l: b.l, r: b.r })
: fmt('<span class="bad">unbalanced (%l% vs %r%)</span>', { l: b.l, r: b.r });
document.getElementById('status').innerHTML =
fmt('Cut size: <b>%cut%</b> %v% · %balTxt%', { cut: cut, v: cut===1?'edge':'edges', balTxt: balTxt });
}
// Brute force every balanced split (exactly n/2 on each side) and keep the min cut.
function best(){
const n=NODES.length, half=n/2; let bestCut=Infinity, bestS=null, checked=0;
for(let mask=0; mask<(1<<n); mask++){
let ones=0; for(let i=0;i<n;i++) if(mask&(1<<i)) ones++;
if(ones!==half) continue; // only balanced splits
checked++;
const s=[]; for(let i=0;i<n;i++) s.push((mask>>i)&1);
const c=cutCount(s);
if(c<bestCut){ bestCut=c; bestS=s.slice(); }
}
side=bestS; render();
document.getElementById('status').innerHTML +=
" " + fmt('— <span class="ok">optimum = %bestCut% (checked %checked% balanced splits)</span>', { bestCut: bestCut, checked: checked });
console.log('Best balanced cut =', bestCut, 'over', checked, 'balanced splits');
}
document.getElementById('best').addEventListener('click',best);
document.getElementById('reset').addEventListener('click',()=>{ side=[0,1,0,1,0,1]; render(); });
render();
Comments
Loading comments...