Here is a tree whose vertices carry weights. Your goal is the classic NP-hard problem maximum-weight independent set: pick a set of vertices with the largest total weight such that no two picked vertices share an edge.
Click vertices to select them (illegal neighbor pairs flash red). Then press Solve optimally to watch a single bottom-up dynamic-programming sweep compute the true optimum — the same algorithm that runs on any low-treewidth graph via its tree decomposition.
<p class="hint">Pick a set of vertices with the largest total <b>weight</b> so that <b>no two picked vertices share an edge</b> (maximum-weight independent set). On a general graph this is NP-hard — but this is a <b>tree</b>, so one bottom-up dynamic-programming sweep finds the true optimum.</p>
<svg id="tree" viewBox="0 0 360 250" role="img" aria-label="weighted tree"></svg>
<div class="status" id="status">Your selection: weight 0</div>
<div class="btns">
<button id="solve" type="button">Solve optimally</button>
<button id="reset" type="button" class="ghost">Clear</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 { width: 100%; max-width: 360px; height: auto; display: block; margin: 0 auto; }
.edge { stroke: #adb1b8; stroke-width: 2; }
.edge.bad { stroke: #e63946; stroke-width: 3.5; animation: flash .5s ease-in-out 3; }
@keyframes flash { 50% { stroke: #ffb3ba; } }
.node { cursor: pointer; }
.node circle { fill: #e8eef3; stroke: #cdd9e3; stroke-width: 2; transition: all .12s; }
.node.sel circle { fill: #0a7d33; stroke: #075c25; }
.node.opt circle { fill: #1d3557; stroke: #14233b; }
.node text { font: 700 13px ui-monospace, monospace; fill: #1d3557; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.node.sel text, .node.opt text { fill: #fff; }
.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] ?? ''));
// Maximum-weight independent set on a TREE.
// General graphs: NP-hard. Trees (treewidth 1): solvable by a single
// bottom-up dynamic program. For each vertex v we keep two values:
// IN[v] = best weight in v's subtree WITH v chosen = w(v) + sum over children c of OUT[c]
// OUT[v] = best weight in v's subtree WITHOUT v chosen = sum over children c of max(IN[c],OUT[c])
// Answer = max(IN[root], OUT[root]). Linear time, exact optimum.
const W = { 0:4, 1:8, 2:3, 3:7, 4:2, 5:9, 6:5 }; // vertex weights
const POS = { 0:[180,30], 1:[90,100], 2:[270,100], 3:[40,180], 4:[140,180], 5:[230,180], 6:[310,180] };
const CHILDREN = { 0:[1,2], 1:[3,4], 2:[5,6], 3:[], 4:[], 5:[], 6:[] };
const PARENT = {}; for (const p in CHILDREN) CHILDREN[p].forEach(c => PARENT[c] = +p);
const EDGES = []; for (const p in CHILDREN) CHILDREN[p].forEach(c => EDGES.push([+p, c]));
const ROOT = 0;
const sel = new Set();
function buildSvg() {
const svg = document.getElementById('tree'); svg.innerHTML = '';
const ns = 'http://www.w3.org/2000/svg';
EDGES.forEach(([a,b],i) => {
const l = document.createElementNS(ns,'line');
l.setAttribute('x1',POS[a][0]); l.setAttribute('y1',POS[a][1]);
l.setAttribute('x2',POS[b][0]); l.setAttribute('y2',POS[b][1]);
l.setAttribute('class','edge'); l.dataset.edge = i; svg.appendChild(l);
});
for (const v in POS) {
const g = document.createElementNS(ns,'g');
g.setAttribute('class','node'); g.dataset.v = v;
const c = document.createElementNS(ns,'circle');
c.setAttribute('cx',POS[v][0]); c.setAttribute('cy',POS[v][1]); c.setAttribute('r',18);
const t = document.createElementNS(ns,'text');
t.setAttribute('x',POS[v][0]); t.setAttribute('y',POS[v][1]); t.textContent = W[v];
g.appendChild(c); g.appendChild(t);
g.addEventListener('click',()=>toggle(+v));
svg.appendChild(g);
}
render();
}
function toggle(v){ sel.has(v) ? sel.delete(v) : sel.add(v); render(); }
function violations(){ return EDGES.filter(([a,b]) => sel.has(a) && sel.has(b)); }
function weight(set){ let s=0; set.forEach(v => s += W[v]); return s; }
function render(opt){
document.querySelectorAll('.node').forEach(g => {
const v = +g.dataset.v;
g.classList.toggle('sel', sel.has(v) && !opt);
g.classList.toggle('opt', !!opt && opt.has(v));
});
const bad = violations();
document.querySelectorAll('.edge').forEach(l => l.classList.remove('bad'));
bad.forEach(([a,b]) => { const i = EDGES.findIndex(e=>e[0]===a&&e[1]===b);
const l = document.querySelector('.edge[data-edge="'+i+'"]'); if(l) l.classList.add('bad'); });
const s = document.getElementById('status');
if (opt) { s.textContent = fmt('Optimal set highlighted: weight %weight%.', { weight: weight(opt) }); s.className='status ok'; }
else if (bad.length) { s.textContent = fmt('✗ %length% edge(s) connect two picked vertices — not independent.', { length: bad.length }); s.className='status bad'; }
else { s.textContent = fmt('Your selection: weight %weight% %v%', { weight: weight(sel), v: sel.size?'(valid independent set)':'' }); s.className='status'+(sel.size?' ok':''); }
}
// Bottom-up DP. Returns {IN, OUT, takeIn} per vertex; reconstruct the chosen set.
function solve(){
const IN = {}, OUT = {};
// post-order via recursion (tree is small and acyclic)
function dfs(v){
let inSum = W[v], outSum = 0;
CHILDREN[v].forEach(c => { dfs(c); inSum += OUT[c]; outSum += Math.max(IN[c], OUT[c]); });
IN[v] = inSum; OUT[v] = outSum;
}
dfs(ROOT);
const chosen = new Set();
// reconstruct: top-down, choose v iff it helps and parent not chosen
function pick(v, parentChosen){
const takeV = !parentChosen && IN[v] >= OUT[v];
if (takeV) chosen.add(v);
CHILDREN[v].forEach(c => pick(c, takeV));
}
pick(ROOT, false);
return { chosen, best: Math.max(IN[ROOT], OUT[ROOT]) };
}
document.getElementById('solve').addEventListener('click',()=>{
const { chosen, best } = solve();
sel.clear(); chosen.forEach(v => sel.add(v));
render(chosen);
console.log('Optimal independent set:', [...chosen].sort((a,b)=>a-b), '| weight', best);
});
document.getElementById('reset').addEventListener('click',()=>{ sel.clear(); render(); });
buildSvg();
<p class="hint">Pick a set of vertices with the largest total <b>weight</b> so that <b>no two picked vertices share an edge</b> (maximum-weight independent set). On a general graph this is NP-hard — but this is a <b>tree</b>, so one bottom-up dynamic-programming sweep finds the true optimum.</p>
<svg id="tree" viewBox="0 0 360 250" role="img" aria-label="weighted tree"></svg>
<div class="status" id="status">Your selection: weight 0</div>
<div class="btns">
<button id="solve" type="button">Solve optimally</button>
<button id="reset" type="button" class="ghost">Clear</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 { width: 100%; max-width: 360px; height: auto; display: block; margin: 0 auto; }
.edge { stroke: #adb1b8; stroke-width: 2; }
.edge.bad { stroke: #e63946; stroke-width: 3.5; animation: flash .5s ease-in-out 3; }
@keyframes flash { 50% { stroke: #ffb3ba; } }
.node { cursor: pointer; }
.node circle { fill: #e8eef3; stroke: #cdd9e3; stroke-width: 2; transition: all .12s; }
.node.sel circle { fill: #0a7d33; stroke: #075c25; }
.node.opt circle { fill: #1d3557; stroke: #14233b; }
.node text { font: 700 13px ui-monospace, monospace; fill: #1d3557; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.node.sel text, .node.opt text { fill: #fff; }
.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] ?? ''));
// Maximum-weight independent set on a TREE.
// General graphs: NP-hard. Trees (treewidth 1): solvable by a single
// bottom-up dynamic program. For each vertex v we keep two values:
// IN[v] = best weight in v's subtree WITH v chosen = w(v) + sum over children c of OUT[c]
// OUT[v] = best weight in v's subtree WITHOUT v chosen = sum over children c of max(IN[c],OUT[c])
// Answer = max(IN[root], OUT[root]). Linear time, exact optimum.
const W = { 0:4, 1:8, 2:3, 3:7, 4:2, 5:9, 6:5 }; // vertex weights
const POS = { 0:[180,30], 1:[90,100], 2:[270,100], 3:[40,180], 4:[140,180], 5:[230,180], 6:[310,180] };
const CHILDREN = { 0:[1,2], 1:[3,4], 2:[5,6], 3:[], 4:[], 5:[], 6:[] };
const PARENT = {}; for (const p in CHILDREN) CHILDREN[p].forEach(c => PARENT[c] = +p);
const EDGES = []; for (const p in CHILDREN) CHILDREN[p].forEach(c => EDGES.push([+p, c]));
const ROOT = 0;
const sel = new Set();
function buildSvg() {
const svg = document.getElementById('tree'); svg.innerHTML = '';
const ns = 'http://www.w3.org/2000/svg';
EDGES.forEach(([a,b],i) => {
const l = document.createElementNS(ns,'line');
l.setAttribute('x1',POS[a][0]); l.setAttribute('y1',POS[a][1]);
l.setAttribute('x2',POS[b][0]); l.setAttribute('y2',POS[b][1]);
l.setAttribute('class','edge'); l.dataset.edge = i; svg.appendChild(l);
});
for (const v in POS) {
const g = document.createElementNS(ns,'g');
g.setAttribute('class','node'); g.dataset.v = v;
const c = document.createElementNS(ns,'circle');
c.setAttribute('cx',POS[v][0]); c.setAttribute('cy',POS[v][1]); c.setAttribute('r',18);
const t = document.createElementNS(ns,'text');
t.setAttribute('x',POS[v][0]); t.setAttribute('y',POS[v][1]); t.textContent = W[v];
g.appendChild(c); g.appendChild(t);
g.addEventListener('click',()=>toggle(+v));
svg.appendChild(g);
}
render();
}
function toggle(v){ sel.has(v) ? sel.delete(v) : sel.add(v); render(); }
function violations(){ return EDGES.filter(([a,b]) => sel.has(a) && sel.has(b)); }
function weight(set){ let s=0; set.forEach(v => s += W[v]); return s; }
function render(opt){
document.querySelectorAll('.node').forEach(g => {
const v = +g.dataset.v;
g.classList.toggle('sel', sel.has(v) && !opt);
g.classList.toggle('opt', !!opt && opt.has(v));
});
const bad = violations();
document.querySelectorAll('.edge').forEach(l => l.classList.remove('bad'));
bad.forEach(([a,b]) => { const i = EDGES.findIndex(e=>e[0]===a&&e[1]===b);
const l = document.querySelector('.edge[data-edge="'+i+'"]'); if(l) l.classList.add('bad'); });
const s = document.getElementById('status');
if (opt) { s.textContent = fmt('Optimal set highlighted: weight %weight%.', { weight: weight(opt) }); s.className='status ok'; }
else if (bad.length) { s.textContent = fmt('✗ %length% edge(s) connect two picked vertices — not independent.', { length: bad.length }); s.className='status bad'; }
else { s.textContent = fmt('Your selection: weight %weight% %v%', { weight: weight(sel), v: sel.size?'(valid independent set)':'' }); s.className='status'+(sel.size?' ok':''); }
}
// Bottom-up DP. Returns {IN, OUT, takeIn} per vertex; reconstruct the chosen set.
function solve(){
const IN = {}, OUT = {};
// post-order via recursion (tree is small and acyclic)
function dfs(v){
let inSum = W[v], outSum = 0;
CHILDREN[v].forEach(c => { dfs(c); inSum += OUT[c]; outSum += Math.max(IN[c], OUT[c]); });
IN[v] = inSum; OUT[v] = outSum;
}
dfs(ROOT);
const chosen = new Set();
// reconstruct: top-down, choose v iff it helps and parent not chosen
function pick(v, parentChosen){
const takeV = !parentChosen && IN[v] >= OUT[v];
if (takeV) chosen.add(v);
CHILDREN[v].forEach(c => pick(c, takeV));
}
pick(ROOT, false);
return { chosen, best: Math.max(IN[ROOT], OUT[ROOT]) };
}
document.getElementById('solve').addEventListener('click',()=>{
const { chosen, best } = solve();
sel.clear(); chosen.forEach(v => sel.add(v));
render(chosen);
console.log('Optimal independent set:', [...chosen].sort((a,b)=>a-b), '| weight', best);
});
document.getElementById('reset').addEventListener('click',()=>{ sel.clear(); render(); });
buildSvg();
On a general graph this problem is intractable. But on a tree (treewidth 1) the dynamic program keeps just two numbers per vertex — best with it in the set, best with it out — and combines children in linear time. Raise the treewidth to k and the same idea works, but each "bag" of the decomposition now tracks 2k combinations. Small k: fast. Large k: back to exponential.
Comments
Loading comments...