<p class="hint">Vertex cover: choose the fewest dots so that <b>every</b> line touches a chosen dot. The greedy <b>2-approximation</b> repeatedly grabs <b>both</b> endpoints of an uncovered line. It is fast and provably never worse than 2× the true optimum.</p>
<svg id="graph" viewBox="0 0 320 220" class="graph"></svg>
<div class="scores">
<span class="score" id="approxScore">2-approx: —</span>
<span class="score" id="optScore">optimum: —</span>
<span class="score" id="ratio">ratio: —</span>
</div>
<div class="status" id="status">Run the greedy rule, then find the true optimum.</div>
<div class="btns">
<button id="approx" type="button">Run 2-approximation</button>
<button id="opt" type="button">Find true optimum</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%; max-width: 360px; height: auto; display: block; margin: 0 auto .4rem; }
.edge { stroke: #adb1b8; stroke-width: 3; }
.edge.covered { stroke: #0a7d33; }
.edge.active { stroke: #e63946; stroke-width: 4; }
.node { fill: #c9ccd1; stroke: #8a8f97; stroke-width: 2; }
.node.approx { fill: #1d3557; stroke: #14253f; }
.node.opt { fill: #e9b949; stroke: #b9902a; }
.nlabel { font: 700 12px ui-monospace, monospace; fill: #1d3557; pointer-events: none; }
.scores { display: flex; gap: .6rem; flex-wrap: wrap; margin: .3rem 0; }
.score { font: 600 13px ui-monospace, monospace; background: #eef2f6; border: 1px solid #d4dde6;
padding: .25rem .55rem; border-radius: 6px; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 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] ?? ''));
// Vertex cover: choose the fewest vertices so every edge has at least one chosen endpoint.
// Exact minimum is NP-hard. Greedy 2-approximation: while an edge is uncovered, take BOTH
// endpoints (the endpoints of a maximal matching). Theorem: |greedy| <= 2 * |optimum|.
const NODES = [
{id:0, x: 55, y: 45}, {id:1, x:160, y: 30}, {id:2, x:265, y: 45},
{id:3, x: 50, y:175}, {id:4, x:160, y:110}, {id:5, x:265, y:180}
];
// A small graph with a hub (node 4). Order matters for the greedy walk:
// grabbing a hub edge first lets greedy cover cheaply, beating the worst case.
const EDGES = [[4,0],[4,1],[4,2],[4,3],[4,5],[0,1],[2,5]];
let approxSet = new Set();
let optSet = new Set();
function neighborsCovered(cover){ return EDGES.every(([a,b]) => cover.has(a) || cover.has(b)); }
// Greedy 2-approximation over the edge list.
function greedyCover(){
const cover = new Set();
const picks = [];
for(const [a,b] of EDGES){
if(!cover.has(a) && !cover.has(b)){ cover.add(a); cover.add(b); picks.push([a,b]); }
}
return {cover, picks};
}
// Brute force: smallest subset of vertices that covers every edge.
function optimumCover(){
const n = NODES.length; let best = null;
for(let mask=0; mask<(1<<n); mask++){
const s = new Set();
for(let i=0;i<n;i++) if(mask&(1<<i)) s.add(i);
if(neighborsCovered(s) && (best===null || s.size<best.size)) best = s;
}
return best;
}
function render(){
const svg = document.getElementById('graph'); svg.innerHTML='';
const NS='http://www.w3.org/2000/svg';
EDGES.forEach(([a,b])=>{
const ln=document.createElementNS(NS,'line');
const A=NODES[a],B=NODES[b];
ln.setAttribute('x1',A.x);ln.setAttribute('y1',A.y);
ln.setAttribute('x2',B.x);ln.setAttribute('y2',B.y);
let cls='edge';
if(approxSet.has(a)||approxSet.has(b)) cls+=' covered';
ln.setAttribute('class',cls); svg.appendChild(ln);
});
NODES.forEach(nd=>{
const c=document.createElementNS(NS,'circle');
c.setAttribute('cx',nd.x);c.setAttribute('cy',nd.y);c.setAttribute('r',16);
let cls='node';
if(approxSet.has(nd.id)) cls+=' approx';
else if(optSet.has(nd.id)) cls+=' opt';
c.setAttribute('class',cls); svg.appendChild(c);
const t=document.createElementNS(NS,'text');
t.setAttribute('x',nd.x);t.setAttribute('y',nd.y+4);
t.setAttribute('text-anchor','middle');t.setAttribute('class','nlabel');
t.style.fill = (approxSet.has(nd.id)) ? '#fff' : '#1d3557';
t.textContent=nd.id; svg.appendChild(t);
});
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function updateScores(){
document.getElementById('approxScore').textContent = fmt('2-approx: %v%', { v: approxSet.size||'—' });
document.getElementById('optScore').textContent = fmt('optimum: %v%', { v: optSet.size||'—' });
if(approxSet.size && optSet.size){
const r=(approxSet.size/optSet.size).toFixed(2);
document.getElementById('ratio').textContent = fmt('ratio: %r%×', { r: r });
} else document.getElementById('ratio').textContent = 'ratio: —';
}
function runApprox(){
optSet=new Set();
const {cover}=greedyCover(); approxSet=cover; render(); updateScores();
setStatus(fmt('Greedy cover uses %size% vertices — guaranteed ≤ 2× optimum.', { size: cover.size }),'ok');
console.log('Greedy 2-approx cover:', [...cover].sort((a,b)=>a-b).join(','), 'size', cover.size);
}
function runOpt(){
const best=optimumCover(); optSet=best; approxSet=new Set(); render(); updateScores();
setStatus(fmt('True minimum cover uses %size% vertices (brute force over 2^%length%).', { size: best.size, length: NODES.length }),'ok');
console.log('Optimum cover:', [...best].sort((a,b)=>a-b).join(','), 'size', best.size);
}
function reset(){ approxSet=new Set(); optSet=new Set(); render(); updateScores();
setStatus('Run the greedy rule, then find the true optimum.',''); }
document.getElementById('approx').addEventListener('click',runApprox);
document.getElementById('opt').addEventListener('click',runOpt);
document.getElementById('reset').addEventListener('click',reset);
render(); updateScores();
<p class="hint">Vertex cover: choose the fewest dots so that <b>every</b> line touches a chosen dot. The greedy <b>2-approximation</b> repeatedly grabs <b>both</b> endpoints of an uncovered line. It is fast and provably never worse than 2× the true optimum.</p>
<svg id="graph" viewBox="0 0 320 220" class="graph"></svg>
<div class="scores">
<span class="score" id="approxScore">2-approx: —</span>
<span class="score" id="optScore">optimum: —</span>
<span class="score" id="ratio">ratio: —</span>
</div>
<div class="status" id="status">Run the greedy rule, then find the true optimum.</div>
<div class="btns">
<button id="approx" type="button">Run 2-approximation</button>
<button id="opt" type="button">Find true optimum</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%; max-width: 360px; height: auto; display: block; margin: 0 auto .4rem; }
.edge { stroke: #adb1b8; stroke-width: 3; }
.edge.covered { stroke: #0a7d33; }
.edge.active { stroke: #e63946; stroke-width: 4; }
.node { fill: #c9ccd1; stroke: #8a8f97; stroke-width: 2; }
.node.approx { fill: #1d3557; stroke: #14253f; }
.node.opt { fill: #e9b949; stroke: #b9902a; }
.nlabel { font: 700 12px ui-monospace, monospace; fill: #1d3557; pointer-events: none; }
.scores { display: flex; gap: .6rem; flex-wrap: wrap; margin: .3rem 0; }
.score { font: 600 13px ui-monospace, monospace; background: #eef2f6; border: 1px solid #d4dde6;
padding: .25rem .55rem; border-radius: 6px; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 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] ?? ''));
// Vertex cover: choose the fewest vertices so every edge has at least one chosen endpoint.
// Exact minimum is NP-hard. Greedy 2-approximation: while an edge is uncovered, take BOTH
// endpoints (the endpoints of a maximal matching). Theorem: |greedy| <= 2 * |optimum|.
const NODES = [
{id:0, x: 55, y: 45}, {id:1, x:160, y: 30}, {id:2, x:265, y: 45},
{id:3, x: 50, y:175}, {id:4, x:160, y:110}, {id:5, x:265, y:180}
];
// A small graph with a hub (node 4). Order matters for the greedy walk:
// grabbing a hub edge first lets greedy cover cheaply, beating the worst case.
const EDGES = [[4,0],[4,1],[4,2],[4,3],[4,5],[0,1],[2,5]];
let approxSet = new Set();
let optSet = new Set();
function neighborsCovered(cover){ return EDGES.every(([a,b]) => cover.has(a) || cover.has(b)); }
// Greedy 2-approximation over the edge list.
function greedyCover(){
const cover = new Set();
const picks = [];
for(const [a,b] of EDGES){
if(!cover.has(a) && !cover.has(b)){ cover.add(a); cover.add(b); picks.push([a,b]); }
}
return {cover, picks};
}
// Brute force: smallest subset of vertices that covers every edge.
function optimumCover(){
const n = NODES.length; let best = null;
for(let mask=0; mask<(1<<n); mask++){
const s = new Set();
for(let i=0;i<n;i++) if(mask&(1<<i)) s.add(i);
if(neighborsCovered(s) && (best===null || s.size<best.size)) best = s;
}
return best;
}
function render(){
const svg = document.getElementById('graph'); svg.innerHTML='';
const NS='http://www.w3.org/2000/svg';
EDGES.forEach(([a,b])=>{
const ln=document.createElementNS(NS,'line');
const A=NODES[a],B=NODES[b];
ln.setAttribute('x1',A.x);ln.setAttribute('y1',A.y);
ln.setAttribute('x2',B.x);ln.setAttribute('y2',B.y);
let cls='edge';
if(approxSet.has(a)||approxSet.has(b)) cls+=' covered';
ln.setAttribute('class',cls); svg.appendChild(ln);
});
NODES.forEach(nd=>{
const c=document.createElementNS(NS,'circle');
c.setAttribute('cx',nd.x);c.setAttribute('cy',nd.y);c.setAttribute('r',16);
let cls='node';
if(approxSet.has(nd.id)) cls+=' approx';
else if(optSet.has(nd.id)) cls+=' opt';
c.setAttribute('class',cls); svg.appendChild(c);
const t=document.createElementNS(NS,'text');
t.setAttribute('x',nd.x);t.setAttribute('y',nd.y+4);
t.setAttribute('text-anchor','middle');t.setAttribute('class','nlabel');
t.style.fill = (approxSet.has(nd.id)) ? '#fff' : '#1d3557';
t.textContent=nd.id; svg.appendChild(t);
});
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function updateScores(){
document.getElementById('approxScore').textContent = fmt('2-approx: %v%', { v: approxSet.size||'—' });
document.getElementById('optScore').textContent = fmt('optimum: %v%', { v: optSet.size||'—' });
if(approxSet.size && optSet.size){
const r=(approxSet.size/optSet.size).toFixed(2);
document.getElementById('ratio').textContent = fmt('ratio: %r%×', { r: r });
} else document.getElementById('ratio').textContent = 'ratio: —';
}
function runApprox(){
optSet=new Set();
const {cover}=greedyCover(); approxSet=cover; render(); updateScores();
setStatus(fmt('Greedy cover uses %size% vertices — guaranteed ≤ 2× optimum.', { size: cover.size }),'ok');
console.log('Greedy 2-approx cover:', [...cover].sort((a,b)=>a-b).join(','), 'size', cover.size);
}
function runOpt(){
const best=optimumCover(); optSet=best; approxSet=new Set(); render(); updateScores();
setStatus(fmt('True minimum cover uses %size% vertices (brute force over 2^%length%).', { size: best.size, length: NODES.length }),'ok');
console.log('Optimum cover:', [...best].sort((a,b)=>a-b).join(','), 'size', best.size);
}
function reset(){ approxSet=new Set(); optSet=new Set(); render(); updateScores();
setStatus('Run the greedy rule, then find the true optimum.',''); }
document.getElementById('approx').addEventListener('click',runApprox);
document.getElementById('opt').addEventListener('click',runOpt);
document.getElementById('reset').addEventListener('click',reset);
render(); updateScores();
Comments
Loading comments...