Files
illustrated-algorithm/longest-palindromic-substring/index.html
T

293 lines
12 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>093. 最长回文子串 – 图解</title>
<link rel="stylesheet" href="../shared/style.css">
<style>.vis-area { min-height: 120px; padding: 16px 0; } .code-section { margin-top: 16px; }
.expansion-line{display:flex;margin-top:4px;justify-content:center;font-family:var(--mono);font-size:.75rem;color:var(--text2);}
.bracket{padding:0 2px;}
.arrow-l,.arrow-r{color:var(--orange);font-weight:700;}
</style>
</head>
<body><div class="container">
<h1>🟡 093. 最长回文子串 <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="babad">
<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 longestPalindrome(s):
def expand(l, r):
while l >= 0 and r < len(s) and s[l] == s[r]:
l -= 1
r += 1
return l + 1, r - 1 # 回文起止
best_l, best_r = 0, 0
for i in range(len(s)):
# 奇数长度
l1, r1 = expand(i, i)
if r1 - l1 > best_r - best_l:
best_l, best_r = l1, r1
# 偶数长度
l2, r2 = expand(i, i + 1)
if r2 - l2 > best_r - best_l:
best_l, best_r = l2, r2
return s[best_l:best_r+1]`;
const EXAMPLES = [
{ name: '例1: babad', input: 'babad' },
{ name: '例2: cbbd', input: 'cbbd' },
{ name: '例3: a', input: 'a' },
{ name: '例4: racecar', input: 'racecar' },
{ name: '例5: abacdfgdcaba', input: 'abacdfgdcaba' },
];
let controller = null;
function expandAroundCenter(s, l, r) {
const steps = [];
while (l >= 0 && r < s.length && s[l] === s[r]) {
steps.push({ l, r, matched: true });
l--; r++;
}
steps.push({ l: l + 1, r: r - 1, matched: false, isFinal: true });
return steps;
}
function genSteps(s) {
const steps = [];
const n = s.length;
let bestL = 0, bestR = 0;
steps.push({
desc: '初始化:对每个位置做中心扩展',
hint: '枚举每个中心位置,向两侧扩展,找到最长回文。',
detail: '<b>思路</b>:中心扩展法<br>• 对每个下标 i,分别以 (i,i) 和 (i,i+1) 为中心<br>• 向两侧扩展直到不等<br>• 记录最长回文的起止位置<br><br>时间 O(n²),空间 O(1)',
hlLine: -1, centerIdx: -1, centerType: '',
expL: -1, expR: -1, bestL: 0, bestR: 0,
cellCls: s.split('').map(() => ''),
});
for (let i = 0; i < n; i++) {
// odd length
const oddSteps = expandAroundCenter(s, i, i);
for (const es of oddSteps) {
const cellCls = s.split('').map(() => '');
// mark palindrome range
if (es.l >= 0 && es.r < n) {
for (let j = es.l; j <= es.r; j++) cellCls[j] = 'palindrome';
}
// mark center
cellCls[i] = 'center';
// mark expansion pointers
if (es.l !== i || es.r !== i) {
if (es.l >= 0) cellCls[es.l] = es.matched ? 'expand' : 'palindrome';
if (es.r < n) cellCls[es.r] = es.matched ? 'expand' : 'palindrome';
}
// mark best
for (let j = bestL; j <= bestR; j++) cellCls[j] = 'best';
// but center takes priority visual
if (es.isFinal) {
const newBest = es.r - es.l > bestR - bestL;
if (newBest) { bestL = es.l; bestR = es.r; }
steps.push({
desc: `中心 i=${i}(奇数):扩展到 [${es.l}, ${es.r}],回文 "${s.substring(es.l, es.r + 1)}"${newBest ? ' → 更新最优!' : ''}`,
hint: newBest ? `找到更长回文 "${s.substring(es.l, es.r + 1)}"(长度 ${es.r - es.l + 1})` : `回文长度 ${es.r - es.l + 1},未超过当前最优 ${bestR - bestL + 1}`,
detail: `中心 i=<b>${i}</b>(奇数长度)<br>扩展结果:[${es.l}, ${es.r}] = "${s.substring(es.l, es.r + 1)}"<br>长度 = ${es.r - es.l + 1}${newBest ? '<br><span style="color:var(--green)">更新最优!</span>' : '<br>未超过当前最优 ' + (bestR - bestL + 1)}`,
hlLine: 10,
centerIdx: i, centerType: 'odd',
expL: es.l, expR: es.r, bestL, bestR,
cellCls,
});
} else {
steps.push({
desc: `中心 i=${i}(奇数):s[${es.l}]='${s[es.l]}' == s[${es.r}]='${s[es.r]}',扩展到 [${es.l}, ${es.r}]`,
hint: `字符匹配,继续向外扩展。`,
detail: `中心 i=<b>${i}</b>(奇数)<br>比较 s[${es.l}]='${s[es.l]}' ↔ s[${es.r}]='${s[es.r]}'<br>✅ 匹配!范围 [${es.l}, ${es.r}]`,
hlLine: 3,
centerIdx: i, centerType: 'odd',
expL: es.l, expR: es.r, bestL, bestR,
cellCls,
});
}
}
// even length
if (i + 1 < n) {
const evenSteps = expandAroundCenter(s, i, i + 1);
for (const es of evenSteps) {
const cellCls = s.split('').map(() => '');
if (es.l >= 0 && es.r < n) {
for (let j = es.l; j <= es.r; j++) cellCls[j] = 'palindrome';
}
cellCls[i] = 'center';
if (i + 1 < n) cellCls[i + 1] = 'center';
if (es.l !== i || es.r !== i + 1) {
if (es.l >= 0 && es.l !== i) cellCls[es.l] = es.matched ? 'expand' : 'palindrome';
if (es.r < n && es.r !== i + 1) cellCls[es.r] = es.matched ? 'expand' : 'palindrome';
}
for (let j = bestL; j <= bestR; j++) cellCls[j] = 'best';
if (es.isFinal) {
const newBest = es.r - es.l > bestR - bestL;
if (newBest) { bestL = es.l; bestR = es.r; }
steps.push({
desc: `中心 i=${i},${i+1}(偶数):扩展到 [${es.l}, ${es.r}],回文 "${s.substring(es.l, es.r + 1)}"${newBest ? ' → 更新最优!' : ''}`,
hint: newBest ? `找到更长回文 "${s.substring(es.l, es.r + 1)}"(长度 ${es.r - es.l + 1})` : `回文长度 ${es.r - es.l + 1},未超过当前最优 ${bestR - bestL + 1}`,
detail: `中心 i=<b>${i},${i+1}</b>(偶数长度)<br>扩展结果:[${es.l}, ${es.r}] = "${s.substring(es.l, es.r + 1)}"<br>长度 = ${es.r - es.l + 1}${newBest ? '<br><span style="color:var(--green)">更新最优!</span>' : '<br>未超过当前最优 ' + (bestR - bestL + 1)}`,
hlLine: 12,
centerIdx: i, centerType: 'even',
expL: es.l, expR: es.r, bestL, bestR,
cellCls,
});
} else {
steps.push({
desc: `中心 i=${i},${i+1}(偶数):s[${es.l}]='${s[es.l]}' == s[${es.r}]='${s[es.r]}',扩展到 [${es.l}, ${es.r}]`,
hint: `字符匹配,继续向外扩展。`,
detail: `中心 i=<b>${i},${i+1}</b>(偶数)<br>比较 s[${es.l}]='${s[es.l]}' ↔ s[${es.r}]='${s[es.r]}'<br>✅ 匹配!范围 [${es.l}, ${es.r}]`,
hlLine: 3,
centerIdx: i, centerType: 'even',
expL: es.l, expR: es.r, bestL, bestR,
cellCls,
});
}
}
}
}
// final
const finalCls = s.split('').map(() => '');
for (let j = bestL; j <= bestR; j++) finalCls[j] = 'best';
steps.push({
desc: `遍历结束,最长回文子串 = "${s.substring(bestL, bestR + 1)}"`,
hint: '中心扩展法穷举了所有可能的回文中心。',
detail: `<b>结果</b>:最长回文子串 = <b>"${s.substring(bestL, bestR + 1)}"</b><br>位置:[${bestL}, ${bestR}]<br>长度:${bestR - bestL + 1}`,
hlLine: -1, centerIdx: -1, centerType: '',
expL: bestL, expR: bestR, bestL, bestR,
cellCls: finalCls, result: s.substring(bestL, bestR + 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 s = $('inputArea').value.trim();
// string cells
const row = document.createElement('div');
row.className = 'str-row';
s.split('').forEach((c, i) => {
const cell = document.createElement('div');
const cls = step.cellCls[i] || '';
cell.className = 'str-cell ' + cls;
cell.innerHTML = `${c}<span class="str-idx">${i}</span>`;
row.appendChild(cell);
});
$('vizArea').innerHTML = '';
$('vizArea').appendChild(row);
// expansion arrows
if (step.expL >= 0 && step.expR >= 0 && step.centerIdx >= 0) {
const arrow = document.createElement('div');
arrow.className = 'expansion-line';
const ptrs = s.split('').map((_, i) => {
if (i === step.expL && i === step.expR) return '<span style="color:var(--orange)">▲</span>';
if (i === step.expL) return '<span class="arrow-l">◀</span>';
if (i === step.expR) return '<span class="arrow-r">▶</span>';
return ' ';
});
arrow.innerHTML = ptrs.join(' ');
$('vizArea').appendChild(arrow);
}
// stats
const stats = document.createElement('div');
stats.style.cssText = 'text-align:center;margin-top:8px;font-family:var(--mono);font-size:.82rem;';
const best = step.bestR - step.bestL + 1;
stats.innerHTML = `当前最优 = <span style="color:var(--green)">"${s.substring(step.bestL, step.bestR + 1)}"</span>(长度 ${best})`;
$('vizArea').appendChild(stats);
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(--purple)"></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.bestL}, ${step.bestR}],长度 ${step.bestR - step.bestL + 1}</small></div>`;
} else {
$('resultContent').innerHTML = `<div style="color:var(--text2);font-size:.85rem;">等待遍历完成…</div>`;
}
}
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 s = $('inputArea').value.trim();
const steps = genSteps(s);
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>