<p class="hint">Five towns, each cable shows its cost. Click an edge to add it,
click again to remove it. Connect <b>all</b> towns for the <b>lowest total cost</b>,
with no redundant loops. Checking a network is instant — finding the cheapest is the
interesting part.</p>
<svg id="map" viewBox="0 0 320 240" class="map" aria-label="town network"></svg>
<div class="status" id="status">Pick cables to connect the towns.</div>
<div class="btns">
<button id="check" type="button">Check network</button>
<button id="solve" type="button">Solve (greedy)</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 .7rem; line-height: 1.45; }
.map { width: 100%; max-width: 420px; height: auto; display: block;
background: #f4f7fa; border: 1px solid #cdd9e3; border-radius: 10px; }
.edge { stroke: #b9c4cf; stroke-width: 5; cursor: pointer; }
.edge:hover { stroke: #8fa3b5; }
.edge.on { stroke: #2a9d8f; }
.edge.mst { stroke: #2a9d8f; }
.ehit { stroke: transparent; stroke-width: 16; cursor: pointer; }
.wlabel { font: 700 11px ui-monospace, monospace; fill: #1d3557; pointer-events: none; }
.wbg { fill: #fff; opacity: .85; pointer-events: none; }
.node { fill: #1d3557; }
.nlabel { font: 700 12px system-ui, sans-serif; fill: #fff; text-anchor: middle;
dominant-baseline: central; pointer-events: none; }
.status { font-size: 1rem; font-weight: 600; margin: .55rem 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] ?? ''));
// Minimum Spanning Tree: connect all towns at the lowest total cable cost.
// Checking a candidate network (connected? total cost?) is a quick scan.
// Finding the cheapest is solved EXACTLY by a greedy rule (Kruskal, 1956):
// sort edges cheap->expensive, add each unless it closes a loop. The cut
// property proves this greedy choice is globally optimal. Runs in P.
const NODES = [
{ id: 0, name: 'A', x: 60, y: 50 },
{ id: 1, name: 'B', x: 250, y: 55 },
{ id: 2, name: 'C', x: 285, y: 185 },
{ id: 3, name: 'D', x: 120, y: 200 },
{ id: 4, name: 'E', x: 165, y: 120 },
];
// Weighted edges (the optimal MST cost is 24: A-E, E-D, E-B, B-C = 6+7+5+6).
const EDGES = [
{ a: 0, b: 4, w: 6 }, // A-E
{ a: 0, b: 1, w: 9 }, // A-B
{ a: 0, b: 3, w: 11 }, // A-D
{ a: 1, b: 4, w: 5 }, // B-E
{ a: 1, b: 2, w: 6 }, // B-C
{ a: 2, b: 4, w: 8 }, // C-E
{ a: 2, b: 3, w: 12 }, // C-D
{ a: 3, b: 4, w: 7 }, // D-E
];
const SVGNS = 'http://www.w3.org/2000/svg';
const picked = new Set(); // indices into EDGES chosen by the player
function el(tag, attrs) {
const e = document.createElementNS(SVGNS, tag);
for (const k in attrs) e.setAttribute(k, attrs[k]);
return e;
}
function setStatus(t, cls) {
const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls || '');
}
function render() {
const svg = document.getElementById('map'); svg.innerHTML = '';
EDGES.forEach((e, i) => {
const A = NODES[e.a], B = NODES[e.b];
const line = el('line', { x1: A.x, y1: A.y, x2: B.x, y2: B.y,
class: 'edge' + (picked.has(i) ? ' on' : '') });
const hit = el('line', { x1: A.x, y1: A.y, x2: B.x, y2: B.y, class: 'ehit' });
hit.addEventListener('click', () => {
picked.has(i) ? picked.delete(i) : picked.add(i);
render(); setStatus('Pick cables to connect the towns.', '');
});
svg.appendChild(line); svg.appendChild(hit);
const mx = (A.x + B.x) / 2, my = (A.y + B.y) / 2;
svg.appendChild(el('rect', { x: mx - 9, y: my - 8, width: 18, height: 14, rx: 3, class: 'wbg' }));
const t = el('text', { x: mx, y: my + 2, class: 'wlabel', 'text-anchor': 'middle' });
t.textContent = e.w; svg.appendChild(t);
});
NODES.forEach(n => {
svg.appendChild(el('circle', { cx: n.x, cy: n.y, r: 13, class: 'node' }));
const t = el('text', { x: n.x, y: n.y, class: 'nlabel' });
t.textContent = n.name; svg.appendChild(t);
});
const total = [...picked].reduce((s, i) => s + EDGES[i].w, 0);
if (picked.size) setStatus(fmt('Selected %size% cables, total cost %total%.', { size: picked.size, total: total }), '');
}
// Union-find over a set of chosen edges -> components.
function components(edgeIdxs) {
const parent = NODES.map(n => n.id);
const find = x => { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; };
let hadLoop = false;
for (const i of edgeIdxs) {
const ra = find(EDGES[i].a), rb = find(EDGES[i].b);
if (ra === rb) hadLoop = true; else parent[ra] = rb;
}
const roots = new Set(NODES.map(n => find(n.id)));
return { count: roots.size, hadLoop };
}
function totalCost(edgeIdxs) { return [...edgeIdxs].reduce((s, i) => s + EDGES[i].w, 0); }
// Greedy MST (Kruskal): sort edges, add each unless it closes a loop.
function kruskal() {
const order = EDGES.map((_, i) => i).sort((p, q) => EDGES[p].w - EDGES[q].w);
const parent = NODES.map(n => n.id);
const find = x => { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; };
const tree = [];
for (const i of order) {
const ra = find(EDGES[i].a), rb = find(EDGES[i].b);
if (ra !== rb) { parent[ra] = rb; tree.push(i); if (tree.length === NODES.length - 1) break; }
}
return tree;
}
const OPT = totalCost(kruskal());
function check() {
if (picked.size === 0) { setStatus('No cables yet — pick some edges.', 'bad'); return; }
const { count, hadLoop } = components(picked);
const total = totalCost(picked);
if (count > 1) { setStatus(fmt('✗ Not all towns are connected yet (cost %total%).', { total: total }), 'bad'); return; }
if (hadLoop) { setStatus(fmt('✗ Connected, but there is a redundant loop (cost %total%). Trim it.', { total: total }), 'bad'); return; }
if (total === OPT) setStatus(fmt('✓ Connected with no loops — and it is the optimum, cost %total%!', { total: total }), 'ok');
else setStatus(fmt('✓ Valid spanning tree, cost %total%. The optimum is %OPT% — try Solve.', { total: total, OPT: OPT }), 'ok');
}
function solve() {
const tree = kruskal();
picked.clear(); tree.forEach(i => picked.add(i)); render();
setStatus(fmt('Greedy MST found: cost %OPT% (cheapest possible).', { OPT: OPT }), 'ok');
console.log('MST edges:', tree.map(i => NODES[EDGES[i].a].name + '-' + NODES[EDGES[i].b].name + '(' + EDGES[i].w + ')').join(' '), '| cost', OPT);
}
document.getElementById('check').addEventListener('click', check);
document.getElementById('solve').addEventListener('click', solve);
document.getElementById('reset').addEventListener('click', () => {
picked.clear(); render(); setStatus('Pick cables to connect the towns.', '');
});
render();
<p class="hint">Five towns, each cable shows its cost. Click an edge to add it,
click again to remove it. Connect <b>all</b> towns for the <b>lowest total cost</b>,
with no redundant loops. Checking a network is instant — finding the cheapest is the
interesting part.</p>
<svg id="map" viewBox="0 0 320 240" class="map" aria-label="town network"></svg>
<div class="status" id="status">Pick cables to connect the towns.</div>
<div class="btns">
<button id="check" type="button">Check network</button>
<button id="solve" type="button">Solve (greedy)</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 .7rem; line-height: 1.45; }
.map { width: 100%; max-width: 420px; height: auto; display: block;
background: #f4f7fa; border: 1px solid #cdd9e3; border-radius: 10px; }
.edge { stroke: #b9c4cf; stroke-width: 5; cursor: pointer; }
.edge:hover { stroke: #8fa3b5; }
.edge.on { stroke: #2a9d8f; }
.edge.mst { stroke: #2a9d8f; }
.ehit { stroke: transparent; stroke-width: 16; cursor: pointer; }
.wlabel { font: 700 11px ui-monospace, monospace; fill: #1d3557; pointer-events: none; }
.wbg { fill: #fff; opacity: .85; pointer-events: none; }
.node { fill: #1d3557; }
.nlabel { font: 700 12px system-ui, sans-serif; fill: #fff; text-anchor: middle;
dominant-baseline: central; pointer-events: none; }
.status { font-size: 1rem; font-weight: 600; margin: .55rem 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] ?? ''));
// Minimum Spanning Tree: connect all towns at the lowest total cable cost.
// Checking a candidate network (connected? total cost?) is a quick scan.
// Finding the cheapest is solved EXACTLY by a greedy rule (Kruskal, 1956):
// sort edges cheap->expensive, add each unless it closes a loop. The cut
// property proves this greedy choice is globally optimal. Runs in P.
const NODES = [
{ id: 0, name: 'A', x: 60, y: 50 },
{ id: 1, name: 'B', x: 250, y: 55 },
{ id: 2, name: 'C', x: 285, y: 185 },
{ id: 3, name: 'D', x: 120, y: 200 },
{ id: 4, name: 'E', x: 165, y: 120 },
];
// Weighted edges (the optimal MST cost is 24: A-E, E-D, E-B, B-C = 6+7+5+6).
const EDGES = [
{ a: 0, b: 4, w: 6 }, // A-E
{ a: 0, b: 1, w: 9 }, // A-B
{ a: 0, b: 3, w: 11 }, // A-D
{ a: 1, b: 4, w: 5 }, // B-E
{ a: 1, b: 2, w: 6 }, // B-C
{ a: 2, b: 4, w: 8 }, // C-E
{ a: 2, b: 3, w: 12 }, // C-D
{ a: 3, b: 4, w: 7 }, // D-E
];
const SVGNS = 'http://www.w3.org/2000/svg';
const picked = new Set(); // indices into EDGES chosen by the player
function el(tag, attrs) {
const e = document.createElementNS(SVGNS, tag);
for (const k in attrs) e.setAttribute(k, attrs[k]);
return e;
}
function setStatus(t, cls) {
const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls || '');
}
function render() {
const svg = document.getElementById('map'); svg.innerHTML = '';
EDGES.forEach((e, i) => {
const A = NODES[e.a], B = NODES[e.b];
const line = el('line', { x1: A.x, y1: A.y, x2: B.x, y2: B.y,
class: 'edge' + (picked.has(i) ? ' on' : '') });
const hit = el('line', { x1: A.x, y1: A.y, x2: B.x, y2: B.y, class: 'ehit' });
hit.addEventListener('click', () => {
picked.has(i) ? picked.delete(i) : picked.add(i);
render(); setStatus('Pick cables to connect the towns.', '');
});
svg.appendChild(line); svg.appendChild(hit);
const mx = (A.x + B.x) / 2, my = (A.y + B.y) / 2;
svg.appendChild(el('rect', { x: mx - 9, y: my - 8, width: 18, height: 14, rx: 3, class: 'wbg' }));
const t = el('text', { x: mx, y: my + 2, class: 'wlabel', 'text-anchor': 'middle' });
t.textContent = e.w; svg.appendChild(t);
});
NODES.forEach(n => {
svg.appendChild(el('circle', { cx: n.x, cy: n.y, r: 13, class: 'node' }));
const t = el('text', { x: n.x, y: n.y, class: 'nlabel' });
t.textContent = n.name; svg.appendChild(t);
});
const total = [...picked].reduce((s, i) => s + EDGES[i].w, 0);
if (picked.size) setStatus(fmt('Selected %size% cables, total cost %total%.', { size: picked.size, total: total }), '');
}
// Union-find over a set of chosen edges -> components.
function components(edgeIdxs) {
const parent = NODES.map(n => n.id);
const find = x => { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; };
let hadLoop = false;
for (const i of edgeIdxs) {
const ra = find(EDGES[i].a), rb = find(EDGES[i].b);
if (ra === rb) hadLoop = true; else parent[ra] = rb;
}
const roots = new Set(NODES.map(n => find(n.id)));
return { count: roots.size, hadLoop };
}
function totalCost(edgeIdxs) { return [...edgeIdxs].reduce((s, i) => s + EDGES[i].w, 0); }
// Greedy MST (Kruskal): sort edges, add each unless it closes a loop.
function kruskal() {
const order = EDGES.map((_, i) => i).sort((p, q) => EDGES[p].w - EDGES[q].w);
const parent = NODES.map(n => n.id);
const find = x => { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; };
const tree = [];
for (const i of order) {
const ra = find(EDGES[i].a), rb = find(EDGES[i].b);
if (ra !== rb) { parent[ra] = rb; tree.push(i); if (tree.length === NODES.length - 1) break; }
}
return tree;
}
const OPT = totalCost(kruskal());
function check() {
if (picked.size === 0) { setStatus('No cables yet — pick some edges.', 'bad'); return; }
const { count, hadLoop } = components(picked);
const total = totalCost(picked);
if (count > 1) { setStatus(fmt('✗ Not all towns are connected yet (cost %total%).', { total: total }), 'bad'); return; }
if (hadLoop) { setStatus(fmt('✗ Connected, but there is a redundant loop (cost %total%). Trim it.', { total: total }), 'bad'); return; }
if (total === OPT) setStatus(fmt('✓ Connected with no loops — and it is the optimum, cost %total%!', { total: total }), 'ok');
else setStatus(fmt('✓ Valid spanning tree, cost %total%. The optimum is %OPT% — try Solve.', { total: total, OPT: OPT }), 'ok');
}
function solve() {
const tree = kruskal();
picked.clear(); tree.forEach(i => picked.add(i)); render();
setStatus(fmt('Greedy MST found: cost %OPT% (cheapest possible).', { OPT: OPT }), 'ok');
console.log('MST edges:', tree.map(i => NODES[EDGES[i].a].name + '-' + NODES[EDGES[i].b].name + '(' + EDGES[i].w + ')').join(' '), '| cost', OPT);
}
document.getElementById('check').addEventListener('click', check);
document.getElementById('solve').addEventListener('click', solve);
document.getElementById('reset').addEventListener('click', () => {
picked.clear(); render(); setStatus('Pick cables to connect the towns.', '');
});
render();
Notice the asymmetry. Checking a network is easy: add up the cable costs and confirm everything is connected. Finding the cheapest one by hand means weighing many trade-offs at once. Press Solve (greedy) and watch the algorithm do something almost suspiciously simple: sort the cables from cheapest to most expensive and add each one unless it would create a loop. That's Kruskal's algorithm, and the tree it builds is guaranteed to be the cheapest possible.
Comments
Loading comments...