Cut-Rod-Problem-with-dynami.../visualization.html
2026-04-29 00:04:10 +02:00

716 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cut Rod Problem — Live Visualizer</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d1117;
--surf: #161b22;
--surf2: #21262d;
--border: #30363d;
--text: #e6edf3;
--dim: #7d8590;
--purple: #7c6aff;
--red: #ff6b6b;
--yellow: #ffd43b;
--green: #3fb950;
--blue: #58a6ff;
--orange: #f0883e;
--pink: #f778ba;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 24px 16px;
}
/* ── HEADER ─────────────────────────────────── */
header {
text-align: center;
margin-bottom: 24px;
}
header h1 {
font-size: 1.9rem;
font-weight: 700;
background: linear-gradient(120deg, var(--purple), var(--pink), var(--red));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
header p {
color: var(--dim);
font-size: 0.88rem;
margin-top: 4px;
}
/* ── CONTROLS ────────────────────────────────── */
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
justify-content: center;
background: var(--surf);
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px 20px;
margin-bottom: 20px;
max-width: 1120px;
margin-left: auto;
margin-right: auto;
}
.ctrl-group {
display: flex;
align-items: center;
gap: 8px;
}
.ctrl-group label { color: var(--dim); font-size: 0.82rem; white-space: nowrap; }
input[type=number] {
width: 64px;
background: var(--surf2);
border: 1px solid var(--border);
color: var(--text);
padding: 6px 10px;
border-radius: 7px;
font-size: 0.9rem;
}
input[type=range] { width: 110px; accent-color: var(--purple); }
.speed-val { font-size: 0.8rem; color: var(--dim); min-width: 24px; }
.btn {
padding: 7px 18px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.88rem;
font-weight: 600;
transition: opacity .15s, transform .1s;
}
.btn:active { transform: scale(.96); }
.btn-primary { background: var(--purple); color: #fff; }
.btn-primary:hover { opacity: .85; }
.btn-secondary { background: var(--surf2); color: var(--dim); border: 1px solid var(--border); }
.btn-secondary:hover { color: var(--text); border-color: #555; }
/* ── LAYOUT ──────────────────────────────────── */
.layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 16px;
max-width: 1120px;
margin: 0 auto;
}
@media (max-width: 800px) {
.layout { grid-template-columns: 1fr; }
}
/* ── PANEL ───────────────────────────────────── */
.panel {
background: var(--surf);
border: 1px solid var(--border);
border-radius: 12px;
padding: 18px 20px;
}
.panel + .panel { margin-top: 14px; }
.panel-title {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: .09em;
color: var(--dim);
margin-bottom: 14px;
}
/* ── ROD VISUAL ──────────────────────────────── */
#rod-desc {
font-size: 0.84rem;
color: var(--dim);
margin-bottom: 10px;
min-height: 20px;
}
.rod-bar {
display: flex;
height: 64px;
border-radius: 8px;
overflow: hidden;
margin-bottom: 10px;
transition: all .25s;
}
.rod-seg {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-weight: 700;
color: rgba(0,0,0,.75);
transition: all .25s;
min-width: 12px;
overflow: hidden;
}
.rod-seg .seg-len { font-size: .95rem; line-height: 1; }
.rod-seg .seg-price{ font-size: .65rem; opacity: .8; margin-top: 2px; }
.rod-cut {
width: 3px;
background: #fff;
box-shadow: 0 0 8px rgba(255,255,255,.7);
flex-shrink: 0;
}
#rod-subtext {
font-size: 0.82rem;
color: var(--dim);
min-height: 36px;
}
/* ── DP TABLE ────────────────────────────────── */
#dp-scroll { overflow-x: auto; }
.dp-tbl {
border-collapse: collapse;
font-size: .82rem;
min-width: 100%;
}
.dp-tbl th, .dp-tbl td {
border: 1px solid var(--border);
padding: 7px 11px;
text-align: center;
transition: background .25s, color .2s, box-shadow .2s;
min-width: 38px;
}
.dp-tbl th { background: var(--surf2); color: var(--dim); font-size: .72rem; }
.dp-tbl td.empty { color: #3d444d; }
.dp-tbl td.done { background: #122620; color: var(--green); font-weight: 600; }
.dp-tbl td.active { background: #2b2400; color: var(--yellow); font-weight: 700; box-shadow: inset 0 0 0 2px var(--yellow); }
.dp-tbl td.ref { background: #0d1f36; color: var(--blue); font-weight: 600; }
.dp-tbl td.s-done { background: #1a1030; color: var(--purple); font-weight: 600; }
.dp-tbl td.s-active { background: #2b2400; color: var(--yellow); font-weight: 700; box-shadow: inset 0 0 0 2px var(--yellow); }
/* ── PROGRESS ────────────────────────────────── */
.prog-row {
display: flex;
align-items: center;
gap: 10px;
margin-top: 12px;
}
.prog-bar {
flex: 1;
height: 4px;
background: var(--surf2);
border-radius: 2px;
overflow: hidden;
}
.prog-fill {
height: 100%;
background: linear-gradient(90deg, var(--purple), var(--pink));
transition: width .3s;
}
.prog-label { font-size: .72rem; color: var(--dim); white-space: nowrap; }
/* ── STEP INFO ───────────────────────────────── */
#step-msg {
font-size: .88rem;
line-height: 1.65;
min-height: 60px;
}
.formula-box {
margin-top: 10px;
padding: 9px 13px;
background: var(--bg);
border-radius: 7px;
font-family: 'SFMono-Regular', 'Consolas', monospace;
font-size: .85rem;
border-left: 3px solid var(--purple);
color: var(--text);
}
/* ── PSEUDOCODE ──────────────────────────────── */
.pseudo {
font-family: 'SFMono-Regular', 'Consolas', monospace;
font-size: .78rem;
line-height: 1.8;
background: var(--bg);
border-radius: 8px;
padding: 12px 14px;
}
.pseudo .ln {
display: block;
padding: 1px 6px;
border-radius: 4px;
transition: background .2s, color .2s;
color: var(--dim);
white-space: pre;
}
.pseudo .ln.hl {
background: #2b2400;
color: var(--yellow);
}
.pseudo .kw { color: var(--purple); }
.pseudo .num { color: var(--orange); }
/* ── PRICE TABLE ─────────────────────────────── */
#price-scroll { overflow-x: auto; }
.price-tbl {
border-collapse: collapse;
font-size: .78rem;
}
.price-tbl th, .price-tbl td {
border: 1px solid var(--border);
padding: 5px 9px;
text-align: center;
}
.price-tbl th { background: var(--surf2); color: var(--dim); }
.price-tbl td { color: var(--green); font-weight: 600; }
.price-tbl td.hl-price { background: #122620; box-shadow: inset 0 0 0 2px var(--green); }
/* ── RESULT ──────────────────────────────────── */
#result-panel {
display: none;
background: #0a1f12;
border: 1px solid #2d6a4f;
border-radius: 12px;
padding: 18px 20px;
margin-top: 14px;
}
.result-rod {
display: flex;
height: 72px;
border-radius: 8px;
overflow: hidden;
margin: 12px 0;
}
.result-seg {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: 700;
color: rgba(0,0,0,.8);
border-right: 2px solid rgba(0,0,0,.25);
min-width: 28px;
overflow: hidden;
transition: flex .4s;
}
.result-seg:last-child { border-right: none; }
.result-seg .r-len { font-size: 1rem; }
.result-seg .r-price { font-size: .68rem; opacity: .85; margin-top: 2px; }
.stats-row { display: flex; gap: 12px; flex-wrap: wrap; }
.stat {
background: var(--surf2);
border-radius: 8px;
padding: 10px 14px;
flex: 1;
min-width: 90px;
}
.stat .s-lbl { font-size: .7rem; color: var(--dim); margin-bottom: 4px; }
.stat .s-val { font-size: 1.3rem; font-weight: 700; color: var(--green); }
/* ── COLORS ──────────────────────────────────── */
.y { color: var(--yellow); }
.g { color: var(--green); }
.b { color: var(--blue); }
.r { color: var(--red); }
.p { color: var(--purple); }
.o { color: var(--orange); }
</style>
</head>
<body>
<header>
<h1>Cut Rod Problem</h1>
<p>Bottom-Up Dynamic Programming &mdash; step-by-step live visualization</p>
</header>
<div class="controls">
<div class="ctrl-group">
<label>Rod Length</label>
<input type="number" id="inp-n" value="10" min="1" max="25">
</div>
<div class="ctrl-group">
<label>Speed</label>
<input type="range" id="inp-speed" min="1" max="10" value="5">
<span class="speed-val" id="speed-lbl">5×</span>
</div>
<button class="btn btn-primary" id="btn-play">▶ Play</button>
<button class="btn btn-secondary" id="btn-step">⏭ Step</button>
<button class="btn btn-secondary" id="btn-back">⏮ Back</button>
<button class="btn btn-secondary" id="btn-reset">↺ Reset</button>
</div>
<div class="layout">
<!-- LEFT COLUMN -->
<div>
<div class="panel">
<div class="panel-title">Rod Visualization</div>
<div id="rod-desc"></div>
<div class="rod-bar" id="rod-bar"></div>
<div id="rod-subtext"></div>
</div>
<div class="panel">
<div class="panel-title">DP Table</div>
<div id="dp-scroll"></div>
<div class="prog-row">
<div class="prog-bar"><div class="prog-fill" id="prog-fill" style="width:0%"></div></div>
<span class="prog-label" id="prog-lbl">0 / 0</span>
</div>
</div>
<div id="result-panel">
<div class="panel-title" style="color:var(--green)">Optimal Solution</div>
<div class="result-rod" id="result-rod"></div>
<div class="stats-row" id="stats-row"></div>
</div>
</div>
<!-- RIGHT COLUMN -->
<div>
<div class="panel">
<div class="panel-title">Current Step</div>
<div id="step-msg"><span style="color:var(--dim)">Press ▶ Play to begin…</span></div>
</div>
<div class="panel">
<div class="panel-title">Algorithm</div>
<div class="pseudo" id="pseudo">
<span class="ln" data-ln="1"><span class="kw">def</span> bottom_up_cut_rod(p, n):</span>
<span class="ln" data-ln="2"> r = [<span class="num">0</span>] * (n + <span class="num">1</span>) <span style="color:#444"># r[0]=0</span></span>
<span class="ln" data-ln="3"> <span class="kw">for</span> j <span class="kw">in</span> range(<span class="num">1</span>, n+<span class="num">1</span>):</span>
<span class="ln" data-ln="4"> q = <span class="num">-∞</span></span>
<span class="ln" data-ln="5"> <span class="kw">for</span> i <span class="kw">in</span> range(<span class="num">1</span>, j+<span class="num">1</span>):</span>
<span class="ln" data-ln="6"> q = max(q, p[i] + r[j-i])</span>
<span class="ln" data-ln="7"> r[j] = q</span>
<span class="ln" data-ln="8"> <span class="kw">return</span> r[n]</span>
</div>
</div>
<div class="panel">
<div class="panel-title">Price Table (p[i])</div>
<div id="price-scroll"></div>
</div>
</div>
</div>
<script>
// ── DATA ────────────────────────────────────────
const PRICE_DATA = [0,1,5,8,9,10,17,17,20,24,30,33,31,31,31,40,39,20,45,42,46,70,76,77,80,85,90,95,100,110,120,111,200,678];
const SEG_COLS = [
'#FF6B6B','#FFD43B','#69DB7C','#74C0FC','#FF922B',
'#DA77F2','#38D9A9','#F783AC','#A9E34B','#63E6BE',
'#FFA94D','#748FFC','#4DABF7','#F06595','#20C997',
'#E599F7','#FFC078','#6BCB77','#4D96FF','#CC5DE8',
'#FFD93D','#6BCBF7','#FF9F43','#A29BFE','#55EFC4',
];
// ── STEP BUILDER ─────────────────────────────────
function buildSteps(prices, n) {
const steps = [];
const r = new Array(n + 1).fill(null);
const s = new Array(n + 1).fill(null);
r[0] = 0;
steps.push({
type:'init', r:[...r], s:[...s], j:null, i:null, pseudo:2,
msg:`<span class="g">Initialize:</span> r[0] = 0 &mdash; a rod of length 0 earns nothing.`,
formula:`r[0] = 0`
});
for (let j = 1; j <= n; j++) {
let q = -Infinity;
steps.push({
type:'start_j', r:[...r], s:[...s], j, i:null, pseudo:3,
msg:`<span class="y">j = ${j}:</span> Computing best revenue for a rod of length <span class="y">${j}</span>.`,
formula:`r[${j}] = max(p[i] + r[${j}i]) for i = 1…${j}`
});
steps.push({
type:'init_q', r:[...r], s:[...s], j, i:null, pseudo:4,
msg:`Set q = −∞ &mdash; will track the best candidate for <span class="y">r[${j}]</span>.`,
formula:`q = −∞`
});
for (let i = 1; i <= j; i++) {
const rji = r[j - i] ?? 0;
const candidate = prices[i] + rji;
const isBetter = candidate > q;
steps.push({
type:'try_cut', r:[...r], s:[...s], j, i, candidate, q, isBetter, pseudo:6,
msg:`Try cut at <span class="b">i = ${i}</span>:&nbsp; p[${i}] + r[${j-i}] = <span class="b">${prices[i]}</span> + <span class="b">${rji}</span> = <span class="${isBetter?'g':'r'}">${candidate}</span>${isBetter?' &nbsp;<span class="g">✓ new best</span>':''}`,
formula:`p[${i}] + r[${j-i}] = ${prices[i]} + ${rji} = ${candidate}`
});
if (isBetter) {
q = candidate;
s[j] = i;
}
}
r[j] = q;
steps.push({
type:'set_r', r:[...r], s:[...s], j, i:s[j], pseudo:7,
msg:`<span class="g">r[${j}] = ${q}</span> &mdash; best first cut is at <span class="p">${s[j]}</span>, leaving remainder <span class="b">${j - s[j]}</span>.`,
formula:`r[${j}] = ${q} (s[${j}] = ${s[j]})`
});
}
// Reconstruct optimal cuts
const cuts = [];
let rem = n;
while (rem > 0 && s[rem] != null) { cuts.push(s[rem]); rem -= s[rem]; }
steps.push({
type:'done', r:[...r], s:[...s], j:null, i:null, cuts, pseudo:8,
msg:`<span class="g">Done!</span> Maximum revenue for length ${n} is <span class="g">${r[n]}</span>.`,
formula:`Optimal cuts: [${cuts.join(', ')}]`
});
return steps;
}
// ── STATE ─────────────────────────────────────────
let steps = [], idx = 0, playing = false, timer = null, N = 10;
// ── RENDER HELPERS ────────────────────────────────
function getDelay() {
const s = parseInt(document.getElementById('inp-speed').value);
return Math.max(40, 1300 - s * 120);
}
function renderPriceTable(prices, n, hiI) {
const lim = Math.min(n, prices.length - 1);
let h = '<table class="price-tbl"><tr><th>i</th>';
for (let i = 1; i <= lim; i++) h += `<th>${i}</th>`;
h += '</tr><tr><th>p[i]</th>';
for (let i = 1; i <= lim; i++) {
const cls = i === hiI ? ' class="hl-price"' : '';
h += `<td${cls}>${prices[i]}</td>`;
}
h += '</tr></table>';
document.getElementById('price-scroll').innerHTML = h;
}
function renderDpTable(r, s, n, activeJ, refJI) {
let h = '<table class="dp-tbl"><tr><th>j</th>';
for (let j = 0; j <= n; j++) h += `<th>${j}</th>`;
h += '</tr>';
// r row
h += '<tr><th>r[j]</th>';
for (let j = 0; j <= n; j++) {
let cls = 'empty'; let val = '—';
if (r[j] !== null) { cls = 'done'; val = r[j]; }
if (j === activeJ) cls = 'active';
if (j === refJI && j !== activeJ && r[j] !== null) cls = 'ref';
h += `<td class="${cls}">${val}</td>`;
}
h += '</tr>';
// s row
h += '<tr><th>s[j]</th>';
for (let j = 0; j <= n; j++) {
let cls = 'empty'; let val = '—';
if (s[j] !== null) { cls = 's-done'; val = s[j]; }
if (j === activeJ) cls = 's-active';
h += `<td class="${cls}">${val}</td>`;
}
h += '</tr></table>';
document.getElementById('dp-scroll').innerHTML = h;
}
function renderRod(j, i, r, prices) {
const bar = document.getElementById('rod-bar');
const desc = document.getElementById('rod-desc');
const sub = document.getElementById('rod-subtext');
bar.innerHTML = '';
if (!j) { desc.innerHTML = ''; sub.innerHTML = ''; return; }
desc.innerHTML = `Rod of length <span class="y">${j}</span>`;
if (i === null) {
// Whole rod, no cut shown
const seg = document.createElement('div');
seg.className = 'rod-seg';
seg.style.cssText = `background:${SEG_COLS[j % SEG_COLS.length]};flex:${j}`;
seg.innerHTML = `<span class="seg-len">${j}</span>`;
bar.appendChild(seg);
sub.innerHTML = '';
return;
}
// Left piece
const left = document.createElement('div');
left.className = 'rod-seg';
left.style.cssText = `background:var(--red);flex:${i}`;
left.innerHTML = `<span class="seg-len">${i}</span><span class="seg-price">$${prices[i]}</span>`;
bar.appendChild(left);
const remainder = j - i;
if (remainder > 0) {
const cut = document.createElement('div');
cut.className = 'rod-cut';
bar.appendChild(cut);
const right = document.createElement('div');
right.className = 'rod-seg';
right.style.cssText = `background:var(--blue);flex:${remainder}`;
const rVal = r[remainder] ?? 0;
right.innerHTML = `<span class="seg-len">${remainder}</span><span class="seg-price">r=${rVal}</span>`;
bar.appendChild(right);
}
const rji = r[j - i] ?? 0;
sub.innerHTML =
`<span style="color:var(--red)">■</span> Cut piece: length <b>${i}</b>, price = <b>$${prices[i]}</b>` +
`&emsp;<span style="color:var(--blue)">■</span> Remainder: length <b>${j-i}</b>, best revenue = <b>${rji}</b>`;
}
function renderResult(cuts, r, prices, n) {
const panel = document.getElementById('result-panel');
const rod = document.getElementById('result-rod');
const stats = document.getElementById('stats-row');
panel.style.display = 'block';
rod.innerHTML = '';
cuts.forEach((len, k) => {
const seg = document.createElement('div');
seg.className = 'result-seg';
seg.style.cssText = `background:${SEG_COLS[k % SEG_COLS.length]};flex:${len}`;
seg.innerHTML = `<span class="r-len">${len}"</span><span class="r-price">$${prices[len]}</span>`;
rod.appendChild(seg);
});
const revenue = r[n];
const nCuts = cuts.length - 1;
stats.innerHTML = `
<div class="stat"><div class="s-lbl">Best Revenue</div><div class="s-val">$${revenue}</div></div>
<div class="stat"><div class="s-lbl">Cuts Made</div><div class="s-val">${nCuts}</div></div>
<div class="stat"><div class="s-lbl">Pieces</div><div class="s-val" style="font-size:.95rem">${cuts.join(' + ')}</div></div>
`;
}
function highlightPseudo(ln) {
document.querySelectorAll('#pseudo .ln').forEach(el => {
el.classList.toggle('hl', parseInt(el.dataset.ln) === ln);
});
}
// ── APPLY STEP ────────────────────────────────────
function applyStep(step) {
const { type, r, s, j, i, msg, formula, pseudo, cuts } = step;
// Step info
document.getElementById('step-msg').innerHTML =
msg + (formula ? `<div class="formula-box">${formula}</div>` : '');
// Pseudocode highlight
highlightPseudo(pseudo);
// Determine referenced cell in DP table
const refCell = (type === 'try_cut') ? j - i : null;
renderDpTable(r, s, N, (type === 'done' || type === 'init') ? null : j, refCell);
// Rod
if (type === 'init' || type === 'done') {
renderRod(null, null, r, PRICE_DATA);
} else if (type === 'start_j' || type === 'set_r' || type === 'init_q') {
renderRod(j, null, r, PRICE_DATA);
} else if (type === 'try_cut') {
renderRod(j, i, r, PRICE_DATA);
}
// Price table highlight
renderPriceTable(PRICE_DATA, N, type === 'try_cut' ? i : null);
// Result
if (type === 'done') {
renderResult(cuts, r, PRICE_DATA, N);
stopPlay();
} else {
document.getElementById('result-panel').style.display = 'none';
}
// Progress
const pct = steps.length > 1 ? (idx / (steps.length - 1)) * 100 : 0;
document.getElementById('prog-fill').style.width = pct + '%';
document.getElementById('prog-lbl').textContent = `${idx + 1} / ${steps.length}`;
}
// ── PLAYBACK ──────────────────────────────────────
function stopPlay() {
playing = false;
clearTimeout(timer);
document.getElementById('btn-play').textContent = '▶ Play';
}
function advance() {
if (idx < steps.length - 1) {
idx++;
applyStep(steps[idx]);
if (playing && idx < steps.length - 1) {
timer = setTimeout(advance, getDelay());
}
}
}
function init() {
N = Math.max(1, Math.min(25, parseInt(document.getElementById('inp-n').value) || 10));
steps = buildSteps(PRICE_DATA, N);
idx = 0;
stopPlay();
document.getElementById('result-panel').style.display = 'none';
renderPriceTable(PRICE_DATA, N, null);
applyStep(steps[0]);
}
// ── EVENTS ────────────────────────────────────────
document.getElementById('btn-play').addEventListener('click', () => {
if (playing) {
stopPlay();
} else {
if (idx >= steps.length - 1) init();
playing = true;
document.getElementById('btn-play').textContent = '⏸ Pause';
timer = setTimeout(advance, getDelay());
}
});
document.getElementById('btn-step').addEventListener('click', () => {
stopPlay();
if (idx < steps.length - 1) { idx++; applyStep(steps[idx]); }
});
document.getElementById('btn-back').addEventListener('click', () => {
stopPlay();
if (idx > 0) { idx--; applyStep(steps[idx]); }
});
document.getElementById('btn-reset').addEventListener('click', () => {
stopPlay();
init();
});
document.getElementById('inp-n').addEventListener('change', () => {
stopPlay();
init();
});
document.getElementById('inp-speed').addEventListener('input', e => {
document.getElementById('speed-lbl').textContent = e.target.value + '×';
});
// ── BOOT ─────────────────────────────────────────
init();
</script>
</body>
</html>