Files
illustrated-algorithm/minimum-path-sum/index.html
T

259 lines
10 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>092. 最小路径和 – 图解</title>
<link rel="stylesheet" href="../shared/style.css">
<style>.vis-area { min-height: 120px; padding: 16px 0; } .code-section { margin-top: 16px; }
.grid-vis{display:inline-grid;gap:3px;margin:8px auto;}
.grid-cell2{min-width:44px;min-height:44px;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:var(--mono);border-radius:4px;background:var(--surface2);border:2px solid var(--border);transition:all .25s;padding:2px;}
.grid-cell2 .gval{font-size:.8rem;font-weight:600;}
.grid-cell2 .gdp{font-size:.65rem;color:var(--text2);}
.grid-cell2.computing{border-color:var(--orange);background:var(--orange-dim);}
.grid-cell2.filled{border-color:var(--accent);background:rgba(108,126,255,.08);}
.grid-cell2.path{border-color:var(--green);background:var(--green-dim);}
.grid-cell2.origin{border-color:var(--purple);background:var(--purple-dim);}
</style>
</head>
<body><div class="container">
<h1>🟡 092. 最小路径和 <span class="badge medium">中等</span></h1>
<p class="subtitle">分类:动态规划 | LeetCode Hot 100</p>
<div class="controls" id="controls">
<label>输入:</label><input type="text" id="inputArea" value="[[1,3,1],[1,5,1],[4,2,1]]">
<button id="applyBtn" class="primary">生成图解</button>
<select id="exampleSelect"></select>
<span style="flex:1"></span>
<button id="prevBtn">◀ 上一步</button>
<button id="nextBtn">下一步 ▶</button>
<button id="jumpBtn">⏭ 跳到结果</button>
<button id="autoBtn">自动播放</button>
<button id="resetBtn">重置</button>
</div>
<div class="pipeline" id="pipeline"></div>
<div class="hint info" id="hintBox"><span id="stepInfo"></span><br><span id="hintText"></span></div>
<div class="panels">
<div class="panel" id="mainPanel"><h3>📊 可视化</h3><div class="vis-area" id="vizArea"></div></div>
<div class="panel-grid">
<div class="panel" id="detailPanel"><h3>📝 当前步骤详情</h3><div id="detailContent"></div></div>
<div class="panel" id="resultPanel"><h3>✅ 结果</h3><div id="resultContent"></div></div>
</div>
</div>
<div class="panel code-section"><h3>💻 参考代码(Python)</h3><div id="codeArea"></div></div>
<footer>Powered by QwenPaw · 图解算法 · LeetCode Hot 100</footer>
</div>
<script src="../shared/algo-viz.js"></script>
<script>"use strict";(function(){
const CODE = `def minPathSum(grid):
m, n = len(grid), len(grid[0])
dp = [[0]*n for _ in range(m)]
dp[0][0] = grid[0][0]
# 第一行
for j in range(1, n):
dp[0][j] = dp[0][j-1] + grid[0][j]
# 第一列
for i in range(1, m):
dp[i][0] = dp[i-1][0] + grid[i][0]
# 其余
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
return dp[m-1][n-1]`;
const EXAMPLES = [
{ name: '例1: [[1,3,1],[1,5,1],[4,2,1]]', input: '[[1,3,1],[1,5,1],[4,2,1]]' },
{ name: '例2: [[1,2,3],[4,5,6]]', input: '[[1,2,3],[4,5,6]]' },
{ name: '例3: [[1]]', input: '[[1]]' },
{ name: '例4: [[1,2],[1,1]]', input: '[[1,2],[1,1]]' },
];
let controller = null;
function genSteps(grid) {
const steps = [];
const m = grid.length, n = grid[0].length;
const dp = Array.from({length: m}, () => Array(n).fill(null));
// init step
steps.push({
desc: '初始化 DP 表:dp[0][0] = grid[0][0]',
hint: '从左上角出发,只能向右或向下走,求到右下角的最小路径和。',
detail: '<b>思路</b>:dp[i][j] = 从 (0,0) 到 (i,j) 的最小路径和<br>• dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]<br>• 第一行、第一列只有一条路径<br><br>时间 O(mn),空间 O(mn)',
hlLine: -1, curI: -1, curJ: -1,
dp: dp.map(r => [...r]), pathCells: [],
});
// (0,0)
dp[0][0] = grid[0][0];
steps.push({
desc: `dp[0][0] = ${grid[0][0]}(起点)`,
hint: '左上角只有一个值,路径和就是自身。',
detail: `dp[0][0] = grid[0][0] = <b>${grid[0][0]}</b>`,
hlLine: 3, curI: 0, curJ: 0,
dp: dp.map(r => [...r]), pathCells: [[0,0]],
});
// first row
for (let j = 1; j < n; j++) {
dp[0][j] = dp[0][j-1] + grid[0][j];
steps.push({
desc: `第一行 dp[0][${j}] = dp[0][${j-1}] + grid[0][${j}] = ${dp[0][j-1]} + ${grid[0][j]} = ${dp[0][j]}`,
hint: '第一行只能从左边来。',
detail: `dp[0][${j}] = dp[0][${j-1}] + grid[0][${j}]<br>= ${dp[0][j-1]} + ${grid[0][j]} = <b>${dp[0][j]}</b>`,
hlLine: 6, curI: 0, curJ: j,
dp: dp.map(r => [...r]), pathCells: [],
});
}
// first col
for (let i = 1; i < m; i++) {
dp[i][0] = dp[i-1][0] + grid[i][0];
steps.push({
desc: `第一列 dp[${i}][0] = dp[${i-1}][0] + grid[${i}][0] = ${dp[i-1][0]} + ${grid[i][0]} = ${dp[i][0]}`,
hint: '第一列只能从上边来。',
detail: `dp[${i}][0] = dp[${i-1}][0] + grid[${i}][0]<br>= ${dp[i-1][0]} + ${grid[i][0]} = <b>${dp[i][0]}</b>`,
hlLine: 9, curI: i, curJ: 0,
dp: dp.map(r => [...r]), pathCells: [],
});
}
// rest
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
const fromTop = dp[i-1][j];
const fromLeft = dp[i][j-1];
const choice = fromTop <= fromLeft ? 'top' : 'left';
dp[i][j] = Math.min(fromTop, fromLeft) + grid[i][j];
steps.push({
desc: `dp[${i}][${j}] = min(dp[${i-1}][${j}]=${fromTop}, dp[${i}][${j-1}]=${fromLeft}) + grid[${i}][${j}]=${grid[i][j]} = ${dp[i][j]}`,
hint: `从${choice === 'top' ? '上方' : '左方'}来更优,dp[${i}][${j}] = ${dp[i][j]}。`,
detail: `dp[${i}][${j}] = min(${fromTop}, ${fromLeft}) + ${grid[i][j]}<br>= min(←${fromLeft}, ↑${fromTop}) + ${grid[i][j]}<br>= <b>${dp[i][j]}</b><br>选择:${choice === 'top' ? '↑ 上方' : '← 左方'}`,
hlLine: 12, curI: i, curJ: j,
dp: dp.map(r => [...r]), pathCells: [],
});
}
}
// backtrack path
const pathCells = [];
let pi = m - 1, pj = n - 1;
pathCells.push([pi, pj]);
while (pi > 0 || pj > 0) {
if (pi === 0) pj--;
else if (pj === 0) pi--;
else if (dp[pi-1][pj] <= dp[pi][pj-1]) pi--;
else pj--;
pathCells.push([pi, pj]);
}
pathCells.reverse();
steps.push({
desc: `遍历完成,最小路径和 = ${dp[m-1][n-1]}`,
hint: '回溯找到最优路径。',
detail: `<b>结果</b>:最小路径和 = <b>${dp[m-1][n-1]}</b><br>路径:${pathCells.map(([i,j]) => `(${i},${j})`).join(' → ')}`,
hlLine: 13, curI: -1, curJ: -1,
dp: dp.map(r => [...r]), pathCells, result: dp[m-1][n-1], isFinal: true,
});
return steps;
}
function renderStep(step) {
if (!step) {
$('vizArea').innerHTML = '<div style="color:var(--text2);text-align:center;padding:40px;">点击「生成图解」开始</div>';
$('detailContent').innerHTML = $('resultContent').innerHTML = '';
$('stepInfo').textContent = $('hintText').textContent = '';
return;
}
$('stepInfo').textContent = step.desc;
$('hintText').textContent = step.hint;
$('detailContent').innerHTML = step.detail;
const grid = parseGrid($('inputArea').value);
const m = grid.length, n = grid[0].length;
const dp = step.dp;
const pathSet = new Set((step.pathCells || []).map(([i,j]) => i+','+j));
const g = document.createElement('div');
g.className = 'grid-vis';
g.style.gridTemplateColumns = `repeat(${n}, auto)`;
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
const cell = document.createElement('div');
let cls = 'grid-cell2';
if (i === step.curI && j === step.curJ) cls += ' computing';
else if (dp[i][j] !== null) cls += ' filled';
if (pathSet.has(i+','+j)) cls += ' path';
if (i === 0 && j === 0) cls += ' origin';
cell.className = cls;
cell.innerHTML = `<span class="gval">${grid[i][j]}</span><span class="gdp">${dp[i][j] !== null ? 'dp=' + dp[i][j] : ''}</span>`;
g.appendChild(cell);
}
}
$('vizArea').innerHTML = '';
const wrapper = document.createElement('div');
wrapper.style.cssText = 'display:flex;justify-content:center;';
wrapper.appendChild(g);
$('vizArea').appendChild(wrapper);
const legend = document.createElement('div');
legend.className = 'legend';
legend.innerHTML = `
<span class="legend-item"><span class="legend-dot" style="background:var(--orange)"></span> 正在计算</span>
<span class="legend-item"><span class="legend-dot" style="background:var(--accent)"></span> 已填充</span>
<span class="legend-item"><span class="legend-dot" style="background:var(--green)"></span> 最优路径</span>
`;
$('vizArea').appendChild(legend);
renderCode($('codeArea'), CODE, step.hlLine);
if (step.isFinal) {
$('resultContent').innerHTML = `<div class="result-box">最小路径和 = <span class="val">${step.result}</span><br><small>路径:${step.pathCells.map(([i,j]) => `(${i},${j})`).join(' → ')}</small></div>`;
} else {
$('resultContent').innerHTML = `<div style="color:var(--text2);font-size:.85rem;">等待填充完成…</div>`;
}
}
function parseGrid(s) {
try {
const a = JSON.parse(s.replace(/'/g, '"'));
if (Array.isArray(a) && a.every(r => Array.isArray(r) && r.every(x => typeof x === 'number'))) return a;
} catch(e) {}
return [[1,3,1],[1,5,1],[4,2,1]];
}
function init() {
const sel = $('exampleSelect');
EXAMPLES.forEach((ex, i) => {
const o = document.createElement('option');
o.value = i; o.textContent = ex.name;
sel.appendChild(o);
});
sel.onchange = () => { $('inputArea').value = EXAMPLES[sel.value].input; build(); };
$('applyBtn').onclick = build;
$('inputArea').onkeydown = e => { if (e.key === 'Enter') build(); };
build();
}
function build() {
const grid = parseGrid($('inputArea').value);
const steps = genSteps(grid);
if (controller) controller.stopAuto();
controller = new StepController(steps, { onRender: renderStep });
$('nextBtn').onclick = () => controller.next();
$('prevBtn').onclick = () => controller.prev();
$('jumpBtn').onclick = () => controller.jumpEnd();
$('resetBtn').onclick = () => controller.reset();
$('autoBtn').onclick = () => { if (controller.autoTimer) controller.stopAuto(); else controller.startAuto(); };
controller.next();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})()</script>
</body></html>