<p class="hint">Pick a budget <b>k</b>, then press <b>Solve</b>. The solver looks for a <b>vertex cover</b> of size at most k — a set of k nodes touching every edge. It branches: for one uncovered edge (u,v), every cover must take u <i>or</i> v. The recursion is at most k deep, so it explores at most <b>2<sup>k</sup></b> branches — no matter how big the graph is.</p>
<div class="controls">
<label>Budget k:
<select id="k">
<option value="1">1</option><option value="2" selected>2</option>
<option value="3">3</option><option value="4">4</option>
<option value="5">5</option><option value="6">6</option>
</select>
</label>
<button id="solve" type="button">Solve</button>
<button id="addiso" type="button" class="ghost">Add isolated node</button>
<button id="reset" type="button" class="ghost">Reset graph</button>
</div>
<canvas id="cv" width="380" height="240"></canvas>
<div class="status" id="status">Budget k = 2. Press Solve.</div>
<div class="meter">Branches explored: <b id="branches">0</b>
| ceiling 2<sup>k</sup> = <b id="cap">4</b>
| nodes in graph: <b id="nn">7</b></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 .7rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .3rem 0 .5rem; }
label { font: 600 14px system-ui, sans-serif; display: flex; align-items: center; gap: .35rem; }
select { font: 600 14px system-ui; padding: .3rem .4rem; border-radius: 6px; border: 1px solid #adb1b8; }
canvas { background: #f3f6f9; border: 1px solid #cdd9e3; border-radius: 10px; display: block; max-width: 100%; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0 .2rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.meter { font-size: .85rem; color: #1d3557; }
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] ?? ''));
// FIXED-PARAMETER TRACTABILITY — Vertex Cover via a bounded search tree.
// Vertex Cover is NP-hard, but parameterized by the cover size k it is FPT:
// branch on an uncovered edge (u,v) — every cover must contain u OR v — and
// decrement k. Depth <= k, so at most 2^k branches, INDEPENDENT of graph size n.
// (Downey & Fellows, early 1990s.)
let nodes = 7;
// Base graph edges (a small instance with a cover of size 2: {1,4}).
let EDGES = [[0,1],[1,2],[1,3],[4,5],[4,6],[3,4]];
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
function layout(){
const pos = []; const cx = 190, cy = 120, R = 90;
for(let i=0;i<nodes;i++){ const a = -Math.PI/2 + i*2*Math.PI/nodes;
pos.push([cx + R*Math.cos(a), cy + R*Math.sin(a)]); }
return pos;
}
function draw(cover){
cover = cover || new Set();
const pos = layout(); ctx.clearRect(0,0,cv.width,cv.height);
ctx.lineWidth = 2;
for(const [u,v] of EDGES){
const covered = cover.has(u) || cover.has(v);
ctx.strokeStyle = covered ? '#0a7d33' : '#c92f3c';
ctx.beginPath(); ctx.moveTo(pos[u][0],pos[u][1]); ctx.lineTo(pos[v][0],pos[v][1]); ctx.stroke();
}
for(let i=0;i<nodes;i++){
ctx.beginPath(); ctx.arc(pos[i][0],pos[i][1],13,0,2*Math.PI);
ctx.fillStyle = cover.has(i) ? '#1d3557' : '#c9ccd1';
ctx.fill(); ctx.lineWidth = 2; ctx.strokeStyle = '#1d3557'; ctx.stroke();
ctx.fillStyle = cover.has(i) ? '#fff' : '#222';
ctx.font = '700 13px ui-monospace, monospace'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(i, pos[i][0], pos[i][1]);
}
}
let branches = 0;
// Bounded search tree. Returns a cover (array of vertices) of size <= k, or null.
function vc(edges, k, chosen){
branches++; // count every recursive call (a tree node)
if(edges.length === 0) return chosen.slice(); // all edges covered
if(k === 0) return null; // budget exhausted, still uncovered edges
const [u,v] = edges[0]; // branch on the first uncovered edge
// Branch 1: put u in the cover.
let e1 = edges.filter(([a,b]) => a!==u && b!==u);
let r = vc(e1, k-1, chosen.concat(u));
if(r) return r;
// Branch 2: put v in the cover.
let e2 = edges.filter(([a,b]) => a!==v && b!==v);
return vc(e2, k-1, chosen.concat(v));
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function solve(){
const k = +document.getElementById('k').value;
branches = 0;
const cover = vc(EDGES.slice(), k, []);
document.getElementById('branches').textContent = branches;
document.getElementById('cap').textContent = Math.pow(2,k);
document.getElementById('nn').textContent = nodes;
if(cover){
const cs = new Set(cover); draw(cs);
setStatus(fmt('Cover of size %length% found: {%cover%}', { length: cover.length, cover: cover.join(', ') }),'ok');
console.log('k='+k, fmt('cover={%cover%}', { cover: cover.join(',') }), fmt('branches=%branches%', { branches: branches }), fmt('cap=2^k=%pow%', { pow: Math.pow(2,k) }));
} else {
draw(new Set());
setStatus(fmt('No cover of size %k% exists. Try a bigger budget.', { k: k }),'bad');
console.log('k='+k, 'no cover', fmt('branches=%branches%', { branches: branches }));
}
}
document.getElementById('k').addEventListener('change',()=>{
const k=+document.getElementById('k').value;
document.getElementById('cap').textContent = Math.pow(2,k);
setStatus(fmt('Budget k = %k%. Press Solve.', { k: k }),'');
});
document.getElementById('solve').addEventListener('click',solve);
document.getElementById('addiso').addEventListener('click',()=>{
nodes++; document.getElementById('nn').textContent = nodes; draw(new Set());
setStatus(fmt('Added an isolated node — n grew to %nodes%. Solve: branches stay the same.', { nodes: nodes }),'');
});
document.getElementById('reset').addEventListener('click',()=>{
nodes = 7; EDGES = [[0,1],[1,2],[1,3],[4,5],[4,6],[3,4]];
document.getElementById('nn').textContent = nodes; document.getElementById('branches').textContent='0';
draw(new Set()); setStatus('Graph reset. Press Solve.','');
});
draw(new Set());
<p class="hint">Pick a budget <b>k</b>, then press <b>Solve</b>. The solver looks for a <b>vertex cover</b> of size at most k — a set of k nodes touching every edge. It branches: for one uncovered edge (u,v), every cover must take u <i>or</i> v. The recursion is at most k deep, so it explores at most <b>2<sup>k</sup></b> branches — no matter how big the graph is.</p>
<div class="controls">
<label>Budget k:
<select id="k">
<option value="1">1</option><option value="2" selected>2</option>
<option value="3">3</option><option value="4">4</option>
<option value="5">5</option><option value="6">6</option>
</select>
</label>
<button id="solve" type="button">Solve</button>
<button id="addiso" type="button" class="ghost">Add isolated node</button>
<button id="reset" type="button" class="ghost">Reset graph</button>
</div>
<canvas id="cv" width="380" height="240"></canvas>
<div class="status" id="status">Budget k = 2. Press Solve.</div>
<div class="meter">Branches explored: <b id="branches">0</b>
| ceiling 2<sup>k</sup> = <b id="cap">4</b>
| nodes in graph: <b id="nn">7</b></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 .7rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .3rem 0 .5rem; }
label { font: 600 14px system-ui, sans-serif; display: flex; align-items: center; gap: .35rem; }
select { font: 600 14px system-ui; padding: .3rem .4rem; border-radius: 6px; border: 1px solid #adb1b8; }
canvas { background: #f3f6f9; border: 1px solid #cdd9e3; border-radius: 10px; display: block; max-width: 100%; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0 .2rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.meter { font-size: .85rem; color: #1d3557; }
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] ?? ''));
// FIXED-PARAMETER TRACTABILITY — Vertex Cover via a bounded search tree.
// Vertex Cover is NP-hard, but parameterized by the cover size k it is FPT:
// branch on an uncovered edge (u,v) — every cover must contain u OR v — and
// decrement k. Depth <= k, so at most 2^k branches, INDEPENDENT of graph size n.
// (Downey & Fellows, early 1990s.)
let nodes = 7;
// Base graph edges (a small instance with a cover of size 2: {1,4}).
let EDGES = [[0,1],[1,2],[1,3],[4,5],[4,6],[3,4]];
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
function layout(){
const pos = []; const cx = 190, cy = 120, R = 90;
for(let i=0;i<nodes;i++){ const a = -Math.PI/2 + i*2*Math.PI/nodes;
pos.push([cx + R*Math.cos(a), cy + R*Math.sin(a)]); }
return pos;
}
function draw(cover){
cover = cover || new Set();
const pos = layout(); ctx.clearRect(0,0,cv.width,cv.height);
ctx.lineWidth = 2;
for(const [u,v] of EDGES){
const covered = cover.has(u) || cover.has(v);
ctx.strokeStyle = covered ? '#0a7d33' : '#c92f3c';
ctx.beginPath(); ctx.moveTo(pos[u][0],pos[u][1]); ctx.lineTo(pos[v][0],pos[v][1]); ctx.stroke();
}
for(let i=0;i<nodes;i++){
ctx.beginPath(); ctx.arc(pos[i][0],pos[i][1],13,0,2*Math.PI);
ctx.fillStyle = cover.has(i) ? '#1d3557' : '#c9ccd1';
ctx.fill(); ctx.lineWidth = 2; ctx.strokeStyle = '#1d3557'; ctx.stroke();
ctx.fillStyle = cover.has(i) ? '#fff' : '#222';
ctx.font = '700 13px ui-monospace, monospace'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(i, pos[i][0], pos[i][1]);
}
}
let branches = 0;
// Bounded search tree. Returns a cover (array of vertices) of size <= k, or null.
function vc(edges, k, chosen){
branches++; // count every recursive call (a tree node)
if(edges.length === 0) return chosen.slice(); // all edges covered
if(k === 0) return null; // budget exhausted, still uncovered edges
const [u,v] = edges[0]; // branch on the first uncovered edge
// Branch 1: put u in the cover.
let e1 = edges.filter(([a,b]) => a!==u && b!==u);
let r = vc(e1, k-1, chosen.concat(u));
if(r) return r;
// Branch 2: put v in the cover.
let e2 = edges.filter(([a,b]) => a!==v && b!==v);
return vc(e2, k-1, chosen.concat(v));
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function solve(){
const k = +document.getElementById('k').value;
branches = 0;
const cover = vc(EDGES.slice(), k, []);
document.getElementById('branches').textContent = branches;
document.getElementById('cap').textContent = Math.pow(2,k);
document.getElementById('nn').textContent = nodes;
if(cover){
const cs = new Set(cover); draw(cs);
setStatus(fmt('Cover of size %length% found: {%cover%}', { length: cover.length, cover: cover.join(', ') }),'ok');
console.log('k='+k, fmt('cover={%cover%}', { cover: cover.join(',') }), fmt('branches=%branches%', { branches: branches }), fmt('cap=2^k=%pow%', { pow: Math.pow(2,k) }));
} else {
draw(new Set());
setStatus(fmt('No cover of size %k% exists. Try a bigger budget.', { k: k }),'bad');
console.log('k='+k, 'no cover', fmt('branches=%branches%', { branches: branches }));
}
}
document.getElementById('k').addEventListener('change',()=>{
const k=+document.getElementById('k').value;
document.getElementById('cap').textContent = Math.pow(2,k);
setStatus(fmt('Budget k = %k%. Press Solve.', { k: k }),'');
});
document.getElementById('solve').addEventListener('click',solve);
document.getElementById('addiso').addEventListener('click',()=>{
nodes++; document.getElementById('nn').textContent = nodes; draw(new Set());
setStatus(fmt('Added an isolated node — n grew to %nodes%. Solve: branches stay the same.', { nodes: nodes }),'');
});
document.getElementById('reset').addEventListener('click',()=>{
nodes = 7; EDGES = [[0,1],[1,2],[1,3],[4,5],[4,6],[3,4]];
document.getElementById('nn').textContent = nodes; document.getElementById('branches').textContent='0';
draw(new Set()); setStatus('Graph reset. Press Solve.','');
});
draw(new Set());
Comments
Loading comments...