<p class="hint">A <b>graphic matroid</b>: edges are independent when they form no cycle. Kruskal's greedy algorithm sorts edges by weight and adds each one that keeps the set independent — always reaching the minimum spanning tree.</p>
<canvas id="graph" width="480" height="280"></canvas>
<div class="info-row">
<span id="status" class="status">Press <b>Run greedy</b> to start.</span>
<span id="total" class="total"></span>
</div>
<div class="btns">
<button id="run" type="button">Run greedy</button>
<button id="shuffle" type="button" class="ghost">Shuffle weights</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="edge-list"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #d0d8e0; border-radius: 10px;
background: #f7f9fb; max-width: 100%; }
.info-row { display: flex; align-items: center; gap: 1rem; margin: .5rem 0 .3rem; min-height: 1.6em; }
.status { font-size: .92rem; font-weight: 600; }
.status.done { color: #0a7d33; }
.status.step { color: #1d3557; }
.total { font-size: .92rem; color: #555; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
#edge-list { font-size: .82rem; color: #444; line-height: 1.7; max-height: 110px; overflow-y: auto; }
#edge-list span { margin-right: .5rem; }
.el-added { color: #0a7d33; font-weight: 700; }
.el-skipped { color: #c92f3c; text-decoration: line-through; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Kruskal's algorithm on a fixed graph (graphic matroid).
// Independent sets = acyclic edge sets (no cycle). The greedy rule:
// sort edges by weight, add an edge if it does not create a cycle.
// By the Rado-Edmonds theorem this always yields a minimum spanning tree.
const NODES = [
{ id: 0, x: 80, y: 60 },
{ id: 1, x: 240, y: 40 },
{ id: 2, x: 400, y: 70 },
{ id: 3, x: 60, y: 180 },
{ id: 4, x: 200, y: 200 },
{ id: 5, x: 360, y: 190 },
{ id: 6, x: 110, y: 255 },
{ id: 7, x: 300, y: 258 },
];
// Base edge connectivity (weights randomized later)
const BASE_EDGES = [
[0,1],[0,3],[1,2],[1,4],[2,5],[3,4],[3,6],[4,5],[4,7],[5,7],[6,7],[1,5]
];
let edges = [];
let stepIndex = 0;
let steps = []; // { edge, accepted }
let mstSet = new Set();
let running = false;
let animTimer = null;
const canvas = document.getElementById('graph');
const ctx = canvas.getContext('2d');
const DPR = window.devicePixelRatio || 1;
function initCanvas() {
const w = canvas.clientWidth || 480;
canvas.width = w * DPR;
canvas.height = 280 * DPR;
ctx.scale(DPR, DPR);
canvas.style.width = w + 'px';
canvas.style.height = '280px';
}
function randomWeights() {
edges = BASE_EDGES.map(([u, v]) => ({
u, v,
w: Math.floor(Math.random() * 18) + 1
}));
}
// Union-Find
function makeUF(n) {
const p = Array.from({length: n}, (_, i) => i);
const r = new Array(n).fill(0);
function find(x) { return p[x] === x ? x : (p[x] = find(p[x])); }
function union(a, b) {
a = find(a); b = find(b);
if (a === b) return false;
if (r[a] < r[b]) [a, b] = [b, a];
p[b] = a;
if (r[a] === r[b]) r[a]++;
return true;
}
return { find, union };
}
function computeKruskal() {
const sorted = [...edges].sort((a, b) => a.w - b.w);
const uf = makeUF(NODES.length);
steps = [];
for (const e of sorted) {
const accepted = uf.union(e.u, e.v);
steps.push({ edge: e, accepted });
}
}
// --- Drawing ---
function nodePos(id) {
const s = canvas.clientWidth / 480;
return { x: NODES[id].x * s, y: NODES[id].y };
}
function drawEdge(u, v, color, width, dashed) {
const a = nodePos(u), b = nodePos(v);
ctx.save();
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = color;
ctx.lineWidth = width;
if (dashed) ctx.setLineDash([5, 4]);
ctx.stroke();
ctx.restore();
}
function drawLabel(u, v, w, color) {
const a = nodePos(u), b = nodePos(v);
const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
ctx.save();
ctx.font = '600 11px system-ui';
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(mx, my, 9, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(w, mx, my);
ctx.restore();
}
function drawNode(id) {
const p = nodePos(id);
ctx.save();
ctx.beginPath();
ctx.arc(p.x, p.y, 13, 0, 2 * Math.PI);
ctx.fillStyle = '#1d3557';
ctx.fill();
ctx.font = '700 11px system-ui';
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(id, p.x, p.y);
ctx.restore();
}
function render() {
const cw = canvas.clientWidth, ch = 280;
ctx.clearRect(0, 0, cw, ch);
// Draw all edges (gray)
for (const e of edges) {
drawEdge(e.u, e.v, '#c8d2dc', 1.5, false);
drawLabel(e.u, e.v, e.w, '#667');
}
// Draw accepted MST edges so far
const accepted = steps.slice(0, stepIndex).filter(s => s.accepted);
for (const s of accepted) {
drawEdge(s.edge.u, s.edge.v, '#0a7d33', 3, false);
drawLabel(s.edge.u, s.edge.v, s.edge.w, '#0a7d33');
}
// Draw rejected edge (if current step is a rejection)
if (stepIndex > 0 && !steps[stepIndex - 1].accepted) {
const rej = steps[stepIndex - 1].edge;
drawEdge(rej.u, rej.v, '#c92f3c', 2, true);
drawLabel(rej.u, rej.v, rej.w, '#c92f3c');
}
// Nodes on top
for (const n of NODES) drawNode(n.id);
}
function updateEdgeList() {
const el = document.getElementById('edge-list');
const done = steps.slice(0, stepIndex);
el.innerHTML = done.map(s => {
const cls = s.accepted ? 'el-added' : 'el-skipped';
const sym = s.accepted ? '✓' : '✗';
return `<span class="${cls}">${sym} (${s.edge.u}–${s.edge.v}) w=${s.edge.w}</span>`;
}).join('');
}
function mstWeight() {
return steps.filter(s => s.accepted).reduce((a, s) => a + s.edge.w, 0);
}
function setStatus(txt, cls) {
const el = document.getElementById('status');
el.innerHTML = txt;
el.className = 'status ' + (cls || '');
}
function setTotal(txt) { document.getElementById('total').textContent = txt; }
function doStep() {
if (stepIndex >= steps.length) { finishAnim(); return; }
stepIndex++;
render();
updateEdgeList();
const s = steps[stepIndex - 1];
if (s.accepted) {
setStatus(fmt('Added edge (%u%–%v%) weight %w% — no cycle', { u: s.edge.u, v: s.edge.v, w: s.edge.w }), 'step');
} else {
setStatus(fmt('Skipped edge (%u%–%v%) weight %w% — would form a cycle', { u: s.edge.u, v: s.edge.v, w: s.edge.w }), 'step');
}
}
function finishAnim() {
clearInterval(animTimer);
running = false;
document.getElementById('run').disabled = false;
document.getElementById('shuffle').disabled = false;
const w = mstWeight();
setStatus(`Done! Minimum spanning tree found — greedy is optimal.`, 'done');
setTotal(fmt('MST total weight: %w%', { w: w }));
}
function startAnim() {
if (running) return;
running = true;
document.getElementById('run').disabled = true;
document.getElementById('shuffle').disabled = true;
setTotal('');
animTimer = setInterval(doStep, 700);
}
function fullReset() {
clearInterval(animTimer);
running = false;
stepIndex = 0;
steps = [];
computeKruskal();
render();
updateEdgeList();
document.getElementById('run').disabled = false;
document.getElementById('shuffle').disabled = false;
setStatus('Press <b>Run greedy</b> to start.', '');
setTotal('');
}
document.getElementById('run').addEventListener('click', () => {
if (stepIndex === 0) computeKruskal();
startAnim();
});
document.getElementById('shuffle').addEventListener('click', () => {
clearInterval(animTimer);
running = false;
stepIndex = 0;
steps = [];
randomWeights();
computeKruskal();
render();
updateEdgeList();
document.getElementById('run').disabled = false;
setStatus('Weights shuffled. Press <b>Run greedy</b>.', '');
setTotal('');
});
document.getElementById('reset').addEventListener('click', fullReset);
// Init
initCanvas();
randomWeights();
computeKruskal();
render();
console.log('Kruskal steps:', steps.map(s => `(${s.edge.u}-${s.edge.v}) w=${s.edge.w} ${s.accepted?'ADDED':'SKIP'}`).join(', '));
console.log('MST weight:', mstWeight());
<p class="hint">A <b>graphic matroid</b>: edges are independent when they form no cycle. Kruskal's greedy algorithm sorts edges by weight and adds each one that keeps the set independent — always reaching the minimum spanning tree.</p>
<canvas id="graph" width="480" height="280"></canvas>
<div class="info-row">
<span id="status" class="status">Press <b>Run greedy</b> to start.</span>
<span id="total" class="total"></span>
</div>
<div class="btns">
<button id="run" type="button">Run greedy</button>
<button id="shuffle" type="button" class="ghost">Shuffle weights</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="edge-list"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #d0d8e0; border-radius: 10px;
background: #f7f9fb; max-width: 100%; }
.info-row { display: flex; align-items: center; gap: 1rem; margin: .5rem 0 .3rem; min-height: 1.6em; }
.status { font-size: .92rem; font-weight: 600; }
.status.done { color: #0a7d33; }
.status.step { color: #1d3557; }
.total { font-size: .92rem; color: #555; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
#edge-list { font-size: .82rem; color: #444; line-height: 1.7; max-height: 110px; overflow-y: auto; }
#edge-list span { margin-right: .5rem; }
.el-added { color: #0a7d33; font-weight: 700; }
.el-skipped { color: #c92f3c; text-decoration: line-through; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Kruskal's algorithm on a fixed graph (graphic matroid).
// Independent sets = acyclic edge sets (no cycle). The greedy rule:
// sort edges by weight, add an edge if it does not create a cycle.
// By the Rado-Edmonds theorem this always yields a minimum spanning tree.
const NODES = [
{ id: 0, x: 80, y: 60 },
{ id: 1, x: 240, y: 40 },
{ id: 2, x: 400, y: 70 },
{ id: 3, x: 60, y: 180 },
{ id: 4, x: 200, y: 200 },
{ id: 5, x: 360, y: 190 },
{ id: 6, x: 110, y: 255 },
{ id: 7, x: 300, y: 258 },
];
// Base edge connectivity (weights randomized later)
const BASE_EDGES = [
[0,1],[0,3],[1,2],[1,4],[2,5],[3,4],[3,6],[4,5],[4,7],[5,7],[6,7],[1,5]
];
let edges = [];
let stepIndex = 0;
let steps = []; // { edge, accepted }
let mstSet = new Set();
let running = false;
let animTimer = null;
const canvas = document.getElementById('graph');
const ctx = canvas.getContext('2d');
const DPR = window.devicePixelRatio || 1;
function initCanvas() {
const w = canvas.clientWidth || 480;
canvas.width = w * DPR;
canvas.height = 280 * DPR;
ctx.scale(DPR, DPR);
canvas.style.width = w + 'px';
canvas.style.height = '280px';
}
function randomWeights() {
edges = BASE_EDGES.map(([u, v]) => ({
u, v,
w: Math.floor(Math.random() * 18) + 1
}));
}
// Union-Find
function makeUF(n) {
const p = Array.from({length: n}, (_, i) => i);
const r = new Array(n).fill(0);
function find(x) { return p[x] === x ? x : (p[x] = find(p[x])); }
function union(a, b) {
a = find(a); b = find(b);
if (a === b) return false;
if (r[a] < r[b]) [a, b] = [b, a];
p[b] = a;
if (r[a] === r[b]) r[a]++;
return true;
}
return { find, union };
}
function computeKruskal() {
const sorted = [...edges].sort((a, b) => a.w - b.w);
const uf = makeUF(NODES.length);
steps = [];
for (const e of sorted) {
const accepted = uf.union(e.u, e.v);
steps.push({ edge: e, accepted });
}
}
// --- Drawing ---
function nodePos(id) {
const s = canvas.clientWidth / 480;
return { x: NODES[id].x * s, y: NODES[id].y };
}
function drawEdge(u, v, color, width, dashed) {
const a = nodePos(u), b = nodePos(v);
ctx.save();
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = color;
ctx.lineWidth = width;
if (dashed) ctx.setLineDash([5, 4]);
ctx.stroke();
ctx.restore();
}
function drawLabel(u, v, w, color) {
const a = nodePos(u), b = nodePos(v);
const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
ctx.save();
ctx.font = '600 11px system-ui';
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(mx, my, 9, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(w, mx, my);
ctx.restore();
}
function drawNode(id) {
const p = nodePos(id);
ctx.save();
ctx.beginPath();
ctx.arc(p.x, p.y, 13, 0, 2 * Math.PI);
ctx.fillStyle = '#1d3557';
ctx.fill();
ctx.font = '700 11px system-ui';
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(id, p.x, p.y);
ctx.restore();
}
function render() {
const cw = canvas.clientWidth, ch = 280;
ctx.clearRect(0, 0, cw, ch);
// Draw all edges (gray)
for (const e of edges) {
drawEdge(e.u, e.v, '#c8d2dc', 1.5, false);
drawLabel(e.u, e.v, e.w, '#667');
}
// Draw accepted MST edges so far
const accepted = steps.slice(0, stepIndex).filter(s => s.accepted);
for (const s of accepted) {
drawEdge(s.edge.u, s.edge.v, '#0a7d33', 3, false);
drawLabel(s.edge.u, s.edge.v, s.edge.w, '#0a7d33');
}
// Draw rejected edge (if current step is a rejection)
if (stepIndex > 0 && !steps[stepIndex - 1].accepted) {
const rej = steps[stepIndex - 1].edge;
drawEdge(rej.u, rej.v, '#c92f3c', 2, true);
drawLabel(rej.u, rej.v, rej.w, '#c92f3c');
}
// Nodes on top
for (const n of NODES) drawNode(n.id);
}
function updateEdgeList() {
const el = document.getElementById('edge-list');
const done = steps.slice(0, stepIndex);
el.innerHTML = done.map(s => {
const cls = s.accepted ? 'el-added' : 'el-skipped';
const sym = s.accepted ? '✓' : '✗';
return `<span class="${cls}">${sym} (${s.edge.u}–${s.edge.v}) w=${s.edge.w}</span>`;
}).join('');
}
function mstWeight() {
return steps.filter(s => s.accepted).reduce((a, s) => a + s.edge.w, 0);
}
function setStatus(txt, cls) {
const el = document.getElementById('status');
el.innerHTML = txt;
el.className = 'status ' + (cls || '');
}
function setTotal(txt) { document.getElementById('total').textContent = txt; }
function doStep() {
if (stepIndex >= steps.length) { finishAnim(); return; }
stepIndex++;
render();
updateEdgeList();
const s = steps[stepIndex - 1];
if (s.accepted) {
setStatus(fmt('Added edge (%u%–%v%) weight %w% — no cycle', { u: s.edge.u, v: s.edge.v, w: s.edge.w }), 'step');
} else {
setStatus(fmt('Skipped edge (%u%–%v%) weight %w% — would form a cycle', { u: s.edge.u, v: s.edge.v, w: s.edge.w }), 'step');
}
}
function finishAnim() {
clearInterval(animTimer);
running = false;
document.getElementById('run').disabled = false;
document.getElementById('shuffle').disabled = false;
const w = mstWeight();
setStatus(`Done! Minimum spanning tree found — greedy is optimal.`, 'done');
setTotal(fmt('MST total weight: %w%', { w: w }));
}
function startAnim() {
if (running) return;
running = true;
document.getElementById('run').disabled = true;
document.getElementById('shuffle').disabled = true;
setTotal('');
animTimer = setInterval(doStep, 700);
}
function fullReset() {
clearInterval(animTimer);
running = false;
stepIndex = 0;
steps = [];
computeKruskal();
render();
updateEdgeList();
document.getElementById('run').disabled = false;
document.getElementById('shuffle').disabled = false;
setStatus('Press <b>Run greedy</b> to start.', '');
setTotal('');
}
document.getElementById('run').addEventListener('click', () => {
if (stepIndex === 0) computeKruskal();
startAnim();
});
document.getElementById('shuffle').addEventListener('click', () => {
clearInterval(animTimer);
running = false;
stepIndex = 0;
steps = [];
randomWeights();
computeKruskal();
render();
updateEdgeList();
document.getElementById('run').disabled = false;
setStatus('Weights shuffled. Press <b>Run greedy</b>.', '');
setTotal('');
});
document.getElementById('reset').addEventListener('click', fullReset);
// Init
initCanvas();
randomWeights();
computeKruskal();
render();
console.log('Kruskal steps:', steps.map(s => `(${s.edge.u}-${s.edge.v}) w=${s.edge.w} ${s.accepted?'ADDED':'SKIP'}`).join(', '));
console.log('MST weight:', mstWeight());
Comments
Loading comments...