Files

215 lines
8.8 KiB
HTML
Raw Permalink 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>091. 不同路径 – 图解</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:48px;min-height:48px;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:.9rem;font-weight:700;}
.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.origin{border-color:var(--purple);background:var(--purple-dim);}
.grid-cell2.dest{border-color:var(--green);background:var(--green-dim);}
.arrrow-row{display:flex;gap:2px;justify-content:center;margin-top:6px;font-size:.7rem;color:var(--text2);font-family:var(--mono);}
</style>
</head>
<body><div class="container">
<h1>🟡 091. 不同路径 <span class="badge medium">中等</span></h1>
<p class="subtitle">分类:动态规划 | LeetCode Hot 100</p>
<div class="controls" id="controls">
<label>m×n:</label><input type="text" id="inputArea" value="3,7">
<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 uniquePaths(m, n):
dp = [[1]*n for _ in range(m)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]`;
const EXAMPLES = [
{ name: '例1: 3×7', input: '3,7' },
{ name: '例2: 3×2', input: '3,2' },
{ name: '例3: 7×3', input: '7,3' },
{ name: '例4: 1×1', input: '1,1' },
{ name: '例5: 4×4', input: '4,4' },
];
let controller = null;
function genSteps(m, n) {
const steps = [];
const dp = Array.from({length: m}, () => Array(n).fill(null));
steps.push({
desc: '初始化:第一行和第一列均为 1(只有一种走法)',
hint: '机器人只能向右或向下走,第一行/列只有一条路径到达。',
detail: '<b>思路</b>:dp[i][j] = 从 (0,0) 到 (i,j) 的不同路径数<br>• dp[i][j] = dp[i-1][j] + dp[i][j-1]<br>• 第一行 dp[0][j] = 1<br>• 第一列 dp[i][0] = 1<br><br>时间 O(mn),空间 O(mn)',
hlLine: -1, curI: -1, curJ: -1,
dp: dp.map(r => [...r]),
});
// fill first row & col with 1
for (let j = 0; j < n; j++) dp[0][j] = 1;
for (let i = 0; i < m; i++) dp[i][0] = 1;
steps.push({
desc: '第一行/列初始化为 1',
hint: '边缘格子只有一种到达方式。',
detail: `dp[0][j] = 1(j=0..${n-1})<br>dp[i][0] = 1(i=0..${m-1})`,
hlLine: 2, curI: -1, curJ: -1,
dp: dp.map(r => [...r]),
});
// fill rest
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
steps.push({
desc: `dp[${i}][${j}] = dp[${i-1}][${j}] + dp[${i}][${j-1}] = ${dp[i-1][j]} + ${dp[i][j-1]} = ${dp[i][j]}`,
hint: `从上方 (${dp[i-1][j]} 条路) 和左方 (${dp[i][j-1]} 条路) 汇聚。`,
detail: `dp[${i}][${j}] = dp[${i-1}][${j}] + dp[${i}][${j-1}]<br>= <span style="color:var(--blue)">↑${dp[i-1][j]}</span> + <span style="color:var(--orange)">←${dp[i][j-1]}</span><br>= <b>${dp[i][j]}</b> 条路径`,
hlLine: 4, curI: i, curJ: j,
dp: dp.map(r => [...r]),
});
}
}
steps.push({
desc: `计算完成,不同路径数 = ${dp[m-1][n-1]}`,
hint: 'dp 表已全部填充,右下角即为答案。',
detail: `<b>结果</b>:从 (0,0) 到 (${m-1},${n-1}) 共有 <b>${dp[m-1][n-1]}</b> 条不同路径`,
hlLine: 5, curI: -1, curJ: -1,
dp: dp.map(r => [...r]), 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 { m, n } = parseMN($('inputArea').value);
const dp = step.dp;
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] && dp[i][j] !== null) cls += ' filled';
if (i === 0 && j === 0) cls += ' origin';
if (i === m - 1 && j === n - 1 && dp[m-1] && dp[m-1][n-1] !== null) cls += ' dest';
cell.className = cls;
cell.innerHTML = `<span class="gval">${dp[i] && dp[i][j] !== null ? 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);
// arrow row to show direction
if (step.curI > 0 || step.curJ > 0) {
const arrowRow = document.createElement('div');
arrowRow.className = 'arrrow-row';
arrowRow.innerHTML = '↑ 从上方贡献 ← 从左方贡献';
$('vizArea').appendChild(arrowRow);
}
const legend = document.createElement('div');
legend.className = 'legend';
legend.innerHTML = `
<span class="legend-item"><span class="legend-dot" style="background:var(--purple)"></span> 起点</span>
<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></div>`;
} else {
$('resultContent').innerHTML = `<div style="color:var(--text2);font-size:.85rem;">等待填充完成…</div>`;
}
}
function parseMN(s) {
const parts = s.split(',').map(x => parseInt(x.trim()));
const m = parts[0] > 0 ? parts[0] : 3;
const n = parts[1] > 0 ? parts[1] : 7;
return { m, n };
}
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 { m, n } = parseMN($('inputArea').value);
const steps = genSteps(m, n);
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>