Files
2026-08-24 04:35:13 +00:00

293 lines
12 KiB
HTML
Raw Permalink 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="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>