Here is a small graph. Each vertex is blue or orange; an edge is cut (highlighted) when its two endpoints have different colors. Click any vertex to flip its color and try to maximize the number of cut edges.
<p class="hint">Each vertex is <b style="color:#2b6cd4">blue</b> or <b style="color:#e07a1f">orange</b>. An edge is <b>cut</b> (bright) when its ends have different colors. Click a vertex to flip it and try to maximize the cut. Counting a cut is instant; finding the largest is the hard part.</p>
<svg id="g" viewBox="0 0 320 240" width="100%" height="240"></svg>
<div class="status" id="status">Cut: 0 edges</div>
<div class="btns">
<button id="greedy" type="button">Greedy flip</button>
<button id="brute" type="button">Brute force (best)</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; }
svg { background: #f4f6f9; border-radius: 10px; display: block; touch-action: manipulation; }
.edge { stroke: #c2cad3; stroke-width: 3; }
.edge.cut { stroke: #0a7d33; stroke-width: 4; }
.node { cursor: pointer; stroke: #14304f; stroke-width: 2; }
.node.A { fill: #2b6cd4; }
.node.B { fill: #e07a1f; }
.nlabel { font: 700 13px system-ui, sans-serif; fill: #fff; pointer-events: none; }
.status { font-size: 1.05rem; font-weight: 700; margin: .6rem 0; min-height: 1.4em; color: #14304f; }
.status.ok { color: #0a7d33; }
.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] ?? ''));
// MAX-CUT: color each vertex 0 or 1; an edge is "cut" when its endpoints differ.
// Counting the cut of a coloring is O(edges) — trivial. Finding the coloring that
// MAXIMIZES the cut is NP-hard (Karp, 1972). Here, greedy single-vertex flips can
// stall at a LOCAL maximum, while brute force checks all 2^n colorings for the true best.
const POS = [ [160,40],[60,110],[260,110],[110,200],[210,200] ];
const EDGES = [ [0,1],[0,2],[0,4],[1,2],[1,3],[2,3],[2,4] ];
const N = POS.length;
let color = new Array(N).fill(0); // start: all blue
function cutOf(col){ let c=0; for(const [a,b] of EDGES) if(col[a]!==col[b]) c++; return c; }
const svg = document.getElementById('g');
const SVGNS = 'http://www.w3.org/2000/svg';
function el(tag, attrs){ const e=document.createElementNS(SVGNS,tag);
for(const k in attrs) e.setAttribute(k, attrs[k]); return e; }
function render(){
svg.innerHTML='';
for(const [a,b] of EDGES){
const e=el('line',{ x1:POS[a][0], y1:POS[a][1], x2:POS[b][0], y2:POS[b][1] });
e.setAttribute('class','edge'+(color[a]!==color[b]?' cut':''));
svg.appendChild(e);
}
for(let i=0;i<N;i++){
const c=el('circle',{ cx:POS[i][0], cy:POS[i][1], r:18 });
c.setAttribute('class','node '+(color[i]?'B':'A'));
c.addEventListener('click',()=>{ color[i]^=1; render(); });
svg.appendChild(c);
const t=el('text',{ x:POS[i][0], y:POS[i][1]+5, 'text-anchor':'middle' });
t.setAttribute('class','nlabel'); t.textContent=i+1; svg.appendChild(t);
}
const cut=cutOf(color), max=EDGES.length;
const s=document.getElementById('status');
s.textContent=fmt('Cut: %cut% of %max% edges', { cut: cut, max: max });
s.className='status'+(cut===bestCut?' ok':'');
}
// Precompute the true optimum once (so we can tell when the user/greedy reaches it).
let bestCut=0;
(function(){ for(let m=0;m<(1<<N);m++){ const col=[]; for(let i=0;i<N;i++) col.push((m>>i)&1);
const c=cutOf(col); if(c>bestCut) bestCut=c; } })();
// Greedy: repeatedly flip any single vertex that strictly increases the cut. Stops
// at a local maximum — which may be below the global best.
function greedy(){
let improved=true;
while(improved){ improved=false;
for(let i=0;i<N;i++){ const before=cutOf(color); color[i]^=1;
if(cutOf(color)>before){ improved=true; } else { color[i]^=1; } }
}
render();
const cur=cutOf(color);
const s=document.getElementById('status');
if(cur<bestCut) s.textContent=fmt('Greedy stuck at %cur% (a local max). Best possible is %bestCut%.', { cur: cur, bestCut: bestCut });
}
// Brute force: check all 2^n colorings, keep the best. Always finds the true optimum.
function brute(){
let best=null, bc=-1, steps=0;
for(let m=0;m<(1<<N);m++){ steps++; const col=[]; for(let i=0;i<N;i++) col.push((m>>i)&1);
const c=cutOf(col); if(c>bc){ bc=c; best=col; } }
color=best.slice(); render();
const s=document.getElementById('status');
s.textContent=fmt('Best cut %bc% of %length% found after %steps% colorings.', { bc: bc, length: EDGES.length, steps: steps });
s.className='status ok';
}
document.getElementById('greedy').addEventListener('click',greedy);
document.getElementById('brute').addEventListener('click',brute);
document.getElementById('reset').addEventListener('click',()=>{ color=new Array(N).fill(0); render(); });
render();
<p class="hint">Each vertex is <b style="color:#2b6cd4">blue</b> or <b style="color:#e07a1f">orange</b>. An edge is <b>cut</b> (bright) when its ends have different colors. Click a vertex to flip it and try to maximize the cut. Counting a cut is instant; finding the largest is the hard part.</p>
<svg id="g" viewBox="0 0 320 240" width="100%" height="240"></svg>
<div class="status" id="status">Cut: 0 edges</div>
<div class="btns">
<button id="greedy" type="button">Greedy flip</button>
<button id="brute" type="button">Brute force (best)</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; }
svg { background: #f4f6f9; border-radius: 10px; display: block; touch-action: manipulation; }
.edge { stroke: #c2cad3; stroke-width: 3; }
.edge.cut { stroke: #0a7d33; stroke-width: 4; }
.node { cursor: pointer; stroke: #14304f; stroke-width: 2; }
.node.A { fill: #2b6cd4; }
.node.B { fill: #e07a1f; }
.nlabel { font: 700 13px system-ui, sans-serif; fill: #fff; pointer-events: none; }
.status { font-size: 1.05rem; font-weight: 700; margin: .6rem 0; min-height: 1.4em; color: #14304f; }
.status.ok { color: #0a7d33; }
.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] ?? ''));
// MAX-CUT: color each vertex 0 or 1; an edge is "cut" when its endpoints differ.
// Counting the cut of a coloring is O(edges) — trivial. Finding the coloring that
// MAXIMIZES the cut is NP-hard (Karp, 1972). Here, greedy single-vertex flips can
// stall at a LOCAL maximum, while brute force checks all 2^n colorings for the true best.
const POS = [ [160,40],[60,110],[260,110],[110,200],[210,200] ];
const EDGES = [ [0,1],[0,2],[0,4],[1,2],[1,3],[2,3],[2,4] ];
const N = POS.length;
let color = new Array(N).fill(0); // start: all blue
function cutOf(col){ let c=0; for(const [a,b] of EDGES) if(col[a]!==col[b]) c++; return c; }
const svg = document.getElementById('g');
const SVGNS = 'http://www.w3.org/2000/svg';
function el(tag, attrs){ const e=document.createElementNS(SVGNS,tag);
for(const k in attrs) e.setAttribute(k, attrs[k]); return e; }
function render(){
svg.innerHTML='';
for(const [a,b] of EDGES){
const e=el('line',{ x1:POS[a][0], y1:POS[a][1], x2:POS[b][0], y2:POS[b][1] });
e.setAttribute('class','edge'+(color[a]!==color[b]?' cut':''));
svg.appendChild(e);
}
for(let i=0;i<N;i++){
const c=el('circle',{ cx:POS[i][0], cy:POS[i][1], r:18 });
c.setAttribute('class','node '+(color[i]?'B':'A'));
c.addEventListener('click',()=>{ color[i]^=1; render(); });
svg.appendChild(c);
const t=el('text',{ x:POS[i][0], y:POS[i][1]+5, 'text-anchor':'middle' });
t.setAttribute('class','nlabel'); t.textContent=i+1; svg.appendChild(t);
}
const cut=cutOf(color), max=EDGES.length;
const s=document.getElementById('status');
s.textContent=fmt('Cut: %cut% of %max% edges', { cut: cut, max: max });
s.className='status'+(cut===bestCut?' ok':'');
}
// Precompute the true optimum once (so we can tell when the user/greedy reaches it).
let bestCut=0;
(function(){ for(let m=0;m<(1<<N);m++){ const col=[]; for(let i=0;i<N;i++) col.push((m>>i)&1);
const c=cutOf(col); if(c>bestCut) bestCut=c; } })();
// Greedy: repeatedly flip any single vertex that strictly increases the cut. Stops
// at a local maximum — which may be below the global best.
function greedy(){
let improved=true;
while(improved){ improved=false;
for(let i=0;i<N;i++){ const before=cutOf(color); color[i]^=1;
if(cutOf(color)>before){ improved=true; } else { color[i]^=1; } }
}
render();
const cur=cutOf(color);
const s=document.getElementById('status');
if(cur<bestCut) s.textContent=fmt('Greedy stuck at %cur% (a local max). Best possible is %bestCut%.', { cur: cur, bestCut: bestCut });
}
// Brute force: check all 2^n colorings, keep the best. Always finds the true optimum.
function brute(){
let best=null, bc=-1, steps=0;
for(let m=0;m<(1<<N);m++){ steps++; const col=[]; for(let i=0;i<N;i++) col.push((m>>i)&1);
const c=cutOf(col); if(c>bc){ bc=c; best=col; } }
color=best.slice(); render();
const s=document.getElementById('status');
s.textContent=fmt('Best cut %bc% of %length% found after %steps% colorings.', { bc: bc, length: EDGES.length, steps: steps });
s.className='status ok';
}
document.getElementById('greedy').addEventListener('click',greedy);
document.getElementById('brute').addEventListener('click',brute);
document.getElementById('reset').addEventListener('click',()=>{ color=new Array(N).fill(0); render(); });
render();
Notice the asymmetry. Counting the cut for a given coloring is instant — just walk every edge once. Finding the coloring with the largest cut is the hard part. Use Greedy flip to repeatedly move any single vertex that improves the cut: it climbs fast but can get stuck in a local maximum. Only Brute force guarantees the true best, and it checks all 2n colorings to do it.
Comments
Loading comments...