<div class="controls">
<span class="label">Beam width:</span>
<button class="bw-btn active" data-k="1">1</button>
<button class="bw-btn" data-k="2">2</button>
<button class="bw-btn" data-k="3">3</button>
<button id="run-btn" type="button">Run</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
<div class="step-info" id="step-info">Press Run to start.</div>
<svg id="tree-svg" viewBox="0 0 540 320" xmlns="http://www.w3.org/2000/svg"></svg>
<div class="result" id="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; margin-bottom: .5rem; }
.label { font-size: .85rem; color: #555; }
.bw-btn { font: 600 14px system-ui; padding: .3rem .7rem; border: 1.5px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; }
.bw-btn.active { background: #1d3557; color: #fff; }
button { font: 600 14px system-ui; padding: .35rem .8rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.step-info { font-size: .88rem; color: #444; min-height: 1.5em; margin-bottom: .3rem; }
#tree-svg { width: 100%; height: auto; display: block; }
.result { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-top: .4rem; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Tree definition โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Nodes: id, label, level (0=root, 1, 2, 3=leaves), position (col)
// Edges: from -> to with cost. Total tree: 1 root -> 3 children -> 9 grandchildren -> 27 leaves
// We use a trimmed 1-3-9-27 tree but display only the first 9 leaves (3 per branch)
// for a 540px canvas. Optimal path: the one with minimum cumulative cost.
// Layout: 9 positions across width for leaves
// Level 0: root at x=270
// Level 1: 3 nodes
// Level 2: 9 nodes
// We'll show depth-3 with 3 branches at each level (factor 3)
const W = 540, H = 320;
const LEVELS = [
[270], // root
[100, 270, 440], // level 1
[40, 100, 160, 210, 270, 330, 380, 440, 500], // level 2 (leaves)
];
// Shrink to 3 levels (root + 2 more), leaves at level 2
// Costs for edges: [from-node-index][child-index] = cost
// We'll define 1 root โ 3 children (costs), each child โ 3 grandchildren (costs)
// Cumulative costs determine "optimal" path.
// Edge costs chosen so K=1 (greedy) is SUBOPTIMAL:
// root->A costs only 1 (greedy picks it) but A's leaves all cost 8-10 โ total 9-11
// root->B costs 3 but B->e costs only 1 โ total 4 (GLOBAL OPTIMUM)
// K>=2 keeps B in the beam and finds the optimum; K=1 misses it.
const EDGE_COSTS = [
// root -> level-1 (A=cheapest trap, B=optimal branch, C=mediocre)
[1, 3, 5],
// level-1[0]=A -> level-2 [a,b,c] (all expensive)
[8, 9, 10],
// level-1[1]=B -> level-2 [d,e,f] (e is the global best)
[2, 1, 3],
// level-1[2]=C -> level-2 [g,h,i] (mediocre)
[4, 5, 6],
];
// Compute cumulative costs for all root-to-leaf paths
// Path is identified by (l1-child, l2-child): 0-2, 0-2
function pathCost(l1, l2) {
return EDGE_COSTS[0][l1] + EDGE_COSTS[1 + l1][l2];
}
// Find optimal (min cost) path
let optL1 = 0, optL2 = 0, optCost = Infinity;
for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) {
const c = pathCost(i, j);
if (c < optCost) { optCost = c; optL1 = i; optL2 = j; }
}
// โโ SVG helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const svg = document.getElementById('tree-svg');
const NS = 'http://www.w3.org/2000/svg';
function el(tag, attrs) {
const e = document.createElementNS(NS, tag);
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
return e;
}
const Y = [30, 130, 250]; // y coords for levels 0,1,2
const R = 18; // node radius
// Node positions: level -> index -> {x, y}
const nodePos = [
[{ x: 270, y: Y[0] }],
[{ x: 100, y: Y[1] }, { x: 270, y: Y[1] }, { x: 440, y: Y[1] }],
[
{ x: 40, y: Y[2] }, { x: 100, y: Y[2] }, { x: 160, y: Y[2] },
{ x: 220, y: Y[2] }, { x: 270, y: Y[2] }, { x: 320, y: Y[2] },
{ x: 380, y: Y[2] }, { x: 440, y: Y[2] }, { x: 500, y: Y[2] },
],
];
// Edge list: { from: {lv,idx}, to: {lv,idx}, cost }
const edges = [];
for (let i = 0; i < 3; i++) {
edges.push({ from: { lv: 0, idx: 0 }, to: { lv: 1, idx: i }, cost: EDGE_COSTS[0][i] });
for (let j = 0; j < 3; j++) {
edges.push({ from: { lv: 1, idx: i }, to: { lv: 2, idx: i * 3 + j }, cost: EDGE_COSTS[1 + i][j] });
}
}
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let K = 1;
let stepIdx = 0;
let steps = []; // array of beam states to animate
let timer = null;
// โโ Build steps โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Each step: { beam: [{lv, idx, cumCost, path:[{lv,idx}]}], pruned: [{lv,idx}] }
function buildSteps(k) {
const result = [];
// Step 0: just root active
const root = [{ lv: 0, idx: 0, cumCost: 0, path: [{ lv: 0, idx: 0 }] }];
result.push({ beam: root, pruned: [], expanded: [] });
// Step 1: expand root -> level 1
const expanded1 = [];
for (const node of root) {
for (let i = 0; i < 3; i++) {
const cost = EDGE_COSTS[0][i];
expanded1.push({ lv: 1, idx: i, cumCost: node.cumCost + cost, path: [...node.path, { lv: 1, idx: i }] });
}
}
expanded1.sort((a, b) => a.cumCost - b.cumCost);
const beam1 = expanded1.slice(0, k);
const pruned1 = expanded1.slice(k);
result.push({ beam: beam1, pruned: pruned1, expanded: expanded1 });
// Step 2: expand beam1 -> level 2
const expanded2 = [];
for (const node of beam1) {
for (let j = 0; j < 3; j++) {
const cost = EDGE_COSTS[1 + node.idx][j];
const leafIdx = node.idx * 3 + j;
expanded2.push({ lv: 2, idx: leafIdx, cumCost: node.cumCost + cost, path: [...node.path, { lv: 2, idx: leafIdx }] });
}
}
expanded2.sort((a, b) => a.cumCost - b.cumCost);
const beam2 = expanded2.slice(0, k);
const pruned2 = expanded2.slice(k);
result.push({ beam: beam2, pruned: pruned2, expanded: expanded2 });
return result;
}
// โโ Render โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function render(stepState) {
svg.innerHTML = '';
const beamNodes = new Set(stepState.beam.map(n => n.lv + ',' + n.idx));
const prunedNodes = new Set(stepState.pruned.map(n => n.lv + ',' + n.idx));
// Collect all nodes on beam paths
const onPath = new Set();
for (const n of stepState.beam) for (const p of n.path) onPath.add(p.lv + ',' + p.idx);
// Draw edges
for (const e of edges) {
const p1 = nodePos[e.from.lv][e.from.idx];
const p2 = nodePos[e.to.lv][e.to.idx];
const toKey = e.to.lv + ',' + e.to.idx;
const fromKey = e.from.lv + ',' + e.from.idx;
const active = onPath.has(toKey) && onPath.has(fromKey);
const pruned = prunedNodes.has(toKey);
const line = el('line', {
x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y,
stroke: active ? '#e63946' : pruned ? '#ddd' : '#ccc',
'stroke-width': active ? 2.4 : 1.2,
'stroke-dasharray': pruned ? '4 3' : 'none',
});
svg.appendChild(line);
// Edge cost label
const mx = (p1.x + p2.x) / 2, my = (p1.y + p2.y) / 2;
const costLbl = el('text', { x: mx + 6, y: my, 'text-anchor': 'start',
'font-size': '11', 'font-family': 'system-ui', fill: active ? '#c92f3c' : '#888' });
costLbl.textContent = e.cost;
svg.appendChild(costLbl);
}
// Draw nodes
for (let lv = 0; lv < 3; lv++) {
for (let idx = 0; idx < nodePos[lv].length; idx++) {
const { x, y } = nodePos[lv][idx];
const key = lv + ',' + idx;
const inBeam = beamNodes.has(key);
const pruned = prunedNodes.has(key);
const path = onPath.has(key);
const fill = inBeam ? '#e63946' : pruned ? '#f0f0f0' : path ? '#ffd6d6' : '#e8eef3';
const stroke = inBeam ? '#c92f3c' : pruned ? '#ccc' : '#9ab0c6';
const textFill = inBeam ? '#fff' : pruned ? '#bbb' : '#1d3557';
const circle = el('circle', { cx: x, cy: y, r: R, fill, stroke, 'stroke-width': 1.8 });
svg.appendChild(circle);
// Label: cumulative cost for beam nodes at level 2, else node name
let label;
if (lv === 0) label = 'S';
else if (lv === 1) label = ['A','B','C'][idx];
else label = ['a','b','c','d','e','f','g','h','i'][idx];
const txt = el('text', { x, y: y + 5, 'text-anchor': 'middle',
'font-size': '13', 'font-weight': '700', 'font-family': 'system-ui, monospace', fill: textFill });
txt.textContent = label;
svg.appendChild(txt);
// Show cumulative cost for beam leaf nodes
if (lv === 2 && inBeam) {
const node = stepState.beam.find(n => n.lv === 2 && n.idx === idx);
if (node) {
const cost = el('text', { x, y: y + R + 14, 'text-anchor': 'middle',
'font-size': '11', 'font-family': 'system-ui', fill: '#c92f3c', 'font-weight': '700' });
cost.textContent = fmt('cost=%cumCost%', { cumCost: node.cumCost });
svg.appendChild(cost);
}
}
// Show cumulative cost for all expanded level-2 nodes
if (lv === 2 && !inBeam && !pruned) {
/* nothing โ unpruned expanded will be shown in next step */
}
if (lv === 2 && pruned) {
const node = stepState.pruned ? stepState.pruned.find(n => n.lv === 2 && n.idx === idx) : null;
if (node) {
const cost = el('text', { x, y: y + R + 14, 'text-anchor': 'middle',
'font-size': '11', 'font-family': 'system-ui', fill: '#bbb' });
cost.textContent = fmt('cost=%cumCost%', { cumCost: node.cumCost });
svg.appendChild(cost);
}
}
}
}
// Legend
const legend = [
{ color: '#e63946', label: 'In beam' },
{ color: '#f0f0f0', label: 'Pruned', stroke: '#ccc' },
{ color: '#e8eef3', label: 'Not yet visited', stroke: '#9ab0c6' },
];
let lx = 10, ly = H - 10;
for (const { color, label, stroke } of legend) {
const c = el('circle', { cx: lx + 7, cy: ly - 5, r: 6, fill: color, stroke: stroke || color, 'stroke-width': 1.4 });
svg.appendChild(c);
const t = el('text', { x: lx + 17, y: ly, 'font-size': '10', 'font-family': 'system-ui', fill: '#555' });
t.textContent = label;
svg.appendChild(t);
lx += 90;
}
}
function setInfo(msg) { document.getElementById('step-info').textContent = msg; }
function setResult(msg, cls) {
const r = document.getElementById('result');
r.textContent = msg; r.className = 'result ' + (cls || '');
}
function stepMessages(k) {
return [
'Start: root node S is in the beam.',
fmt('Level 1: expanded S โ A, B, C. Keeping top-%k% by cost.', { k: k }),
fmt('Level 2 (leaves): expanded beam. Keeping top-%k% final paths.', { k: k }),
];
}
// โโ Controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.querySelectorAll('.bw-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.bw-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
K = parseInt(btn.dataset.k, 10);
stopAnim();
stepIdx = 0;
steps = [];
render({ beam: [{ lv: 0, idx: 0, cumCost: 0, path: [{ lv: 0, idx: 0 }] }], pruned: [], expanded: [] });
setInfo(fmt('Beam width set to %K%. Press Run.', { K: K }));
setResult('');
});
});
function stopAnim() {
if (timer) { clearInterval(timer); timer = null; }
}
document.getElementById('run-btn').addEventListener('click', () => {
stopAnim();
steps = buildSteps(K);
stepIdx = 0;
const msgs = stepMessages(K);
render(steps[0]);
setInfo(msgs[0]);
setResult('');
timer = setInterval(() => {
stepIdx++;
if (stepIdx >= steps.length) {
stopAnim();
// Show result
const finalBeam = steps[steps.length - 1].beam;
const best = finalBeam[0];
const pathLabels = best.path.map(n => {
if (n.lv === 0) return 'S';
if (n.lv === 1) return ['A','B','C'][n.idx];
return ['a','b','c','d','e','f','g','h','i'][n.idx];
}).join(' โ ');
const isOptimal = best.cumCost === optCost;
setResult(
fmt('Best path found: %pathLabels% (cost %cumCost%)%v%', { pathLabels: pathLabels, cumCost: best.cumCost, v: isOptimal ? ' โ Optimal!' : ' โ ' + fmt('optimal is cost %optCost%', { optCost: optCost }) }),
isOptimal ? 'ok' : 'bad'
);
setInfo(msgs[stepIdx - 1] || '');
return;
}
render(steps[stepIdx]);
setInfo(msgs[stepIdx]);
}, 1100);
});
document.getElementById('reset-btn').addEventListener('click', () => {
stopAnim();
stepIdx = 0;
steps = [];
render({ beam: [{ lv: 0, idx: 0, cumCost: 0, path: [{ lv: 0, idx: 0 }] }], pruned: [], expanded: [] });
setInfo('Press Run to start.');
setResult('');
});
// Initial render
const msgs = stepMessages(K);
render({ beam: [{ lv: 0, idx: 0, cumCost: 0, path: [{ lv: 0, idx: 0 }] }], pruned: [], expanded: [] });
setInfo('Press Run to start.');
<div class="controls">
<span class="label">Beam width:</span>
<button class="bw-btn active" data-k="1">1</button>
<button class="bw-btn" data-k="2">2</button>
<button class="bw-btn" data-k="3">3</button>
<button id="run-btn" type="button">Run</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
<div class="step-info" id="step-info">Press Run to start.</div>
<svg id="tree-svg" viewBox="0 0 540 320" xmlns="http://www.w3.org/2000/svg"></svg>
<div class="result" id="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; margin-bottom: .5rem; }
.label { font-size: .85rem; color: #555; }
.bw-btn { font: 600 14px system-ui; padding: .3rem .7rem; border: 1.5px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; }
.bw-btn.active { background: #1d3557; color: #fff; }
button { font: 600 14px system-ui; padding: .35rem .8rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.step-info { font-size: .88rem; color: #444; min-height: 1.5em; margin-bottom: .3rem; }
#tree-svg { width: 100%; height: auto; display: block; }
.result { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-top: .4rem; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Tree definition โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Nodes: id, label, level (0=root, 1, 2, 3=leaves), position (col)
// Edges: from -> to with cost. Total tree: 1 root -> 3 children -> 9 grandchildren -> 27 leaves
// We use a trimmed 1-3-9-27 tree but display only the first 9 leaves (3 per branch)
// for a 540px canvas. Optimal path: the one with minimum cumulative cost.
// Layout: 9 positions across width for leaves
// Level 0: root at x=270
// Level 1: 3 nodes
// Level 2: 9 nodes
// We'll show depth-3 with 3 branches at each level (factor 3)
const W = 540, H = 320;
const LEVELS = [
[270], // root
[100, 270, 440], // level 1
[40, 100, 160, 210, 270, 330, 380, 440, 500], // level 2 (leaves)
];
// Shrink to 3 levels (root + 2 more), leaves at level 2
// Costs for edges: [from-node-index][child-index] = cost
// We'll define 1 root โ 3 children (costs), each child โ 3 grandchildren (costs)
// Cumulative costs determine "optimal" path.
// Edge costs chosen so K=1 (greedy) is SUBOPTIMAL:
// root->A costs only 1 (greedy picks it) but A's leaves all cost 8-10 โ total 9-11
// root->B costs 3 but B->e costs only 1 โ total 4 (GLOBAL OPTIMUM)
// K>=2 keeps B in the beam and finds the optimum; K=1 misses it.
const EDGE_COSTS = [
// root -> level-1 (A=cheapest trap, B=optimal branch, C=mediocre)
[1, 3, 5],
// level-1[0]=A -> level-2 [a,b,c] (all expensive)
[8, 9, 10],
// level-1[1]=B -> level-2 [d,e,f] (e is the global best)
[2, 1, 3],
// level-1[2]=C -> level-2 [g,h,i] (mediocre)
[4, 5, 6],
];
// Compute cumulative costs for all root-to-leaf paths
// Path is identified by (l1-child, l2-child): 0-2, 0-2
function pathCost(l1, l2) {
return EDGE_COSTS[0][l1] + EDGE_COSTS[1 + l1][l2];
}
// Find optimal (min cost) path
let optL1 = 0, optL2 = 0, optCost = Infinity;
for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) {
const c = pathCost(i, j);
if (c < optCost) { optCost = c; optL1 = i; optL2 = j; }
}
// โโ SVG helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const svg = document.getElementById('tree-svg');
const NS = 'http://www.w3.org/2000/svg';
function el(tag, attrs) {
const e = document.createElementNS(NS, tag);
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
return e;
}
const Y = [30, 130, 250]; // y coords for levels 0,1,2
const R = 18; // node radius
// Node positions: level -> index -> {x, y}
const nodePos = [
[{ x: 270, y: Y[0] }],
[{ x: 100, y: Y[1] }, { x: 270, y: Y[1] }, { x: 440, y: Y[1] }],
[
{ x: 40, y: Y[2] }, { x: 100, y: Y[2] }, { x: 160, y: Y[2] },
{ x: 220, y: Y[2] }, { x: 270, y: Y[2] }, { x: 320, y: Y[2] },
{ x: 380, y: Y[2] }, { x: 440, y: Y[2] }, { x: 500, y: Y[2] },
],
];
// Edge list: { from: {lv,idx}, to: {lv,idx}, cost }
const edges = [];
for (let i = 0; i < 3; i++) {
edges.push({ from: { lv: 0, idx: 0 }, to: { lv: 1, idx: i }, cost: EDGE_COSTS[0][i] });
for (let j = 0; j < 3; j++) {
edges.push({ from: { lv: 1, idx: i }, to: { lv: 2, idx: i * 3 + j }, cost: EDGE_COSTS[1 + i][j] });
}
}
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let K = 1;
let stepIdx = 0;
let steps = []; // array of beam states to animate
let timer = null;
// โโ Build steps โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Each step: { beam: [{lv, idx, cumCost, path:[{lv,idx}]}], pruned: [{lv,idx}] }
function buildSteps(k) {
const result = [];
// Step 0: just root active
const root = [{ lv: 0, idx: 0, cumCost: 0, path: [{ lv: 0, idx: 0 }] }];
result.push({ beam: root, pruned: [], expanded: [] });
// Step 1: expand root -> level 1
const expanded1 = [];
for (const node of root) {
for (let i = 0; i < 3; i++) {
const cost = EDGE_COSTS[0][i];
expanded1.push({ lv: 1, idx: i, cumCost: node.cumCost + cost, path: [...node.path, { lv: 1, idx: i }] });
}
}
expanded1.sort((a, b) => a.cumCost - b.cumCost);
const beam1 = expanded1.slice(0, k);
const pruned1 = expanded1.slice(k);
result.push({ beam: beam1, pruned: pruned1, expanded: expanded1 });
// Step 2: expand beam1 -> level 2
const expanded2 = [];
for (const node of beam1) {
for (let j = 0; j < 3; j++) {
const cost = EDGE_COSTS[1 + node.idx][j];
const leafIdx = node.idx * 3 + j;
expanded2.push({ lv: 2, idx: leafIdx, cumCost: node.cumCost + cost, path: [...node.path, { lv: 2, idx: leafIdx }] });
}
}
expanded2.sort((a, b) => a.cumCost - b.cumCost);
const beam2 = expanded2.slice(0, k);
const pruned2 = expanded2.slice(k);
result.push({ beam: beam2, pruned: pruned2, expanded: expanded2 });
return result;
}
// โโ Render โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function render(stepState) {
svg.innerHTML = '';
const beamNodes = new Set(stepState.beam.map(n => n.lv + ',' + n.idx));
const prunedNodes = new Set(stepState.pruned.map(n => n.lv + ',' + n.idx));
// Collect all nodes on beam paths
const onPath = new Set();
for (const n of stepState.beam) for (const p of n.path) onPath.add(p.lv + ',' + p.idx);
// Draw edges
for (const e of edges) {
const p1 = nodePos[e.from.lv][e.from.idx];
const p2 = nodePos[e.to.lv][e.to.idx];
const toKey = e.to.lv + ',' + e.to.idx;
const fromKey = e.from.lv + ',' + e.from.idx;
const active = onPath.has(toKey) && onPath.has(fromKey);
const pruned = prunedNodes.has(toKey);
const line = el('line', {
x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y,
stroke: active ? '#e63946' : pruned ? '#ddd' : '#ccc',
'stroke-width': active ? 2.4 : 1.2,
'stroke-dasharray': pruned ? '4 3' : 'none',
});
svg.appendChild(line);
// Edge cost label
const mx = (p1.x + p2.x) / 2, my = (p1.y + p2.y) / 2;
const costLbl = el('text', { x: mx + 6, y: my, 'text-anchor': 'start',
'font-size': '11', 'font-family': 'system-ui', fill: active ? '#c92f3c' : '#888' });
costLbl.textContent = e.cost;
svg.appendChild(costLbl);
}
// Draw nodes
for (let lv = 0; lv < 3; lv++) {
for (let idx = 0; idx < nodePos[lv].length; idx++) {
const { x, y } = nodePos[lv][idx];
const key = lv + ',' + idx;
const inBeam = beamNodes.has(key);
const pruned = prunedNodes.has(key);
const path = onPath.has(key);
const fill = inBeam ? '#e63946' : pruned ? '#f0f0f0' : path ? '#ffd6d6' : '#e8eef3';
const stroke = inBeam ? '#c92f3c' : pruned ? '#ccc' : '#9ab0c6';
const textFill = inBeam ? '#fff' : pruned ? '#bbb' : '#1d3557';
const circle = el('circle', { cx: x, cy: y, r: R, fill, stroke, 'stroke-width': 1.8 });
svg.appendChild(circle);
// Label: cumulative cost for beam nodes at level 2, else node name
let label;
if (lv === 0) label = 'S';
else if (lv === 1) label = ['A','B','C'][idx];
else label = ['a','b','c','d','e','f','g','h','i'][idx];
const txt = el('text', { x, y: y + 5, 'text-anchor': 'middle',
'font-size': '13', 'font-weight': '700', 'font-family': 'system-ui, monospace', fill: textFill });
txt.textContent = label;
svg.appendChild(txt);
// Show cumulative cost for beam leaf nodes
if (lv === 2 && inBeam) {
const node = stepState.beam.find(n => n.lv === 2 && n.idx === idx);
if (node) {
const cost = el('text', { x, y: y + R + 14, 'text-anchor': 'middle',
'font-size': '11', 'font-family': 'system-ui', fill: '#c92f3c', 'font-weight': '700' });
cost.textContent = fmt('cost=%cumCost%', { cumCost: node.cumCost });
svg.appendChild(cost);
}
}
// Show cumulative cost for all expanded level-2 nodes
if (lv === 2 && !inBeam && !pruned) {
/* nothing โ unpruned expanded will be shown in next step */
}
if (lv === 2 && pruned) {
const node = stepState.pruned ? stepState.pruned.find(n => n.lv === 2 && n.idx === idx) : null;
if (node) {
const cost = el('text', { x, y: y + R + 14, 'text-anchor': 'middle',
'font-size': '11', 'font-family': 'system-ui', fill: '#bbb' });
cost.textContent = fmt('cost=%cumCost%', { cumCost: node.cumCost });
svg.appendChild(cost);
}
}
}
}
// Legend
const legend = [
{ color: '#e63946', label: 'In beam' },
{ color: '#f0f0f0', label: 'Pruned', stroke: '#ccc' },
{ color: '#e8eef3', label: 'Not yet visited', stroke: '#9ab0c6' },
];
let lx = 10, ly = H - 10;
for (const { color, label, stroke } of legend) {
const c = el('circle', { cx: lx + 7, cy: ly - 5, r: 6, fill: color, stroke: stroke || color, 'stroke-width': 1.4 });
svg.appendChild(c);
const t = el('text', { x: lx + 17, y: ly, 'font-size': '10', 'font-family': 'system-ui', fill: '#555' });
t.textContent = label;
svg.appendChild(t);
lx += 90;
}
}
function setInfo(msg) { document.getElementById('step-info').textContent = msg; }
function setResult(msg, cls) {
const r = document.getElementById('result');
r.textContent = msg; r.className = 'result ' + (cls || '');
}
function stepMessages(k) {
return [
'Start: root node S is in the beam.',
fmt('Level 1: expanded S โ A, B, C. Keeping top-%k% by cost.', { k: k }),
fmt('Level 2 (leaves): expanded beam. Keeping top-%k% final paths.', { k: k }),
];
}
// โโ Controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.querySelectorAll('.bw-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.bw-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
K = parseInt(btn.dataset.k, 10);
stopAnim();
stepIdx = 0;
steps = [];
render({ beam: [{ lv: 0, idx: 0, cumCost: 0, path: [{ lv: 0, idx: 0 }] }], pruned: [], expanded: [] });
setInfo(fmt('Beam width set to %K%. Press Run.', { K: K }));
setResult('');
});
});
function stopAnim() {
if (timer) { clearInterval(timer); timer = null; }
}
document.getElementById('run-btn').addEventListener('click', () => {
stopAnim();
steps = buildSteps(K);
stepIdx = 0;
const msgs = stepMessages(K);
render(steps[0]);
setInfo(msgs[0]);
setResult('');
timer = setInterval(() => {
stepIdx++;
if (stepIdx >= steps.length) {
stopAnim();
// Show result
const finalBeam = steps[steps.length - 1].beam;
const best = finalBeam[0];
const pathLabels = best.path.map(n => {
if (n.lv === 0) return 'S';
if (n.lv === 1) return ['A','B','C'][n.idx];
return ['a','b','c','d','e','f','g','h','i'][n.idx];
}).join(' โ ');
const isOptimal = best.cumCost === optCost;
setResult(
fmt('Best path found: %pathLabels% (cost %cumCost%)%v%', { pathLabels: pathLabels, cumCost: best.cumCost, v: isOptimal ? ' โ Optimal!' : ' โ ' + fmt('optimal is cost %optCost%', { optCost: optCost }) }),
isOptimal ? 'ok' : 'bad'
);
setInfo(msgs[stepIdx - 1] || '');
return;
}
render(steps[stepIdx]);
setInfo(msgs[stepIdx]);
}, 1100);
});
document.getElementById('reset-btn').addEventListener('click', () => {
stopAnim();
stepIdx = 0;
steps = [];
render({ beam: [{ lv: 0, idx: 0, cumCost: 0, path: [{ lv: 0, idx: 0 }] }], pruned: [], expanded: [] });
setInfo('Press Run to start.');
setResult('');
});
// Initial render
const msgs = stepMessages(K);
render({ beam: [{ lv: 0, idx: 0, cumCost: 0, path: [{ lv: 0, idx: 0 }] }], pruned: [], expanded: [] });
setInfo('Press Run to start.');
With beam width 1 (pure greedy) the algorithm always follows the cheapest local edge and may end up at a worse final node. With width 3 it keeps all branches at this scale and always finds the optimal path. Real language models operate with K = 4โ10 over vocabularies of tens of thousands โ still an approximation, but a very effective one.
Comments
Loading comments...