This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user