235 lines
9.5 KiB
HTML
235 lines
9.5 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-Hans">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>073. 柱状图中最大的矩形 – 图解</title>
|
||
<link rel="stylesheet" href="../shared/style.css">
|
||
<style>
|
||
.vis-area { min-height: 120px; padding: 16px 0; }
|
||
.code-section { margin-top: 16px; }
|
||
.histo-container { display:flex; align-items:flex-end; gap:2px; height:180px; padding:8px 4px; background:#f8fafc; border:1px solid var(--border); border-radius:8px; margin:8px 0; position:relative; }
|
||
.histo-bar-wrap { display:flex; flex-direction:column; align-items:center; flex:1; max-width:45px; position:relative; }
|
||
.histo-bar { width:100%; border-radius:3px 3px 0 0; transition:all 0.3s; min-height:2px; }
|
||
.histo-val { font-size:11px; font-weight:600; margin-bottom:2px; }
|
||
.histo-idx { font-size:10px; color:var(--text-muted); margin-top:2px; }
|
||
.area-overlay { position:absolute; border:2px dashed var(--green); background:rgba(34,197,94,0.1); border-radius:4px; pointer-events:none; }
|
||
.area-label { text-align:center; font-size:12px; font-weight:700; color:var(--green); margin-top:4px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>🔴 073. 柱状图中最大的矩形 <span class="badge hard">困难</span></h1>
|
||
<p class="subtitle">分类:栈 | LeetCode Hot 100</p>
|
||
|
||
<div class="controls" id="controls">
|
||
<label for="inputArea">输入:</label>
|
||
<input type="text" id="inputArea" placeholder="heights=[2,1,5,6,2,3]">
|
||
<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 examples = [
|
||
{heights:[2,1,5,6,2,3], label:'示例1: [2,1,5,6,2,3]'},
|
||
{heights:[2,4], label:'示例2: [2,4]'},
|
||
{heights:[1,2,3,4,5], label:'示例3: 递增'},
|
||
{heights:[5,4,3,2,1], label:'示例4: 递减'},
|
||
];
|
||
let heightsArr, steps, stepCtrl;
|
||
|
||
function buildSteps(heights) {
|
||
heightsArr = heights;
|
||
steps = [];
|
||
const n = heights.length;
|
||
const h = [...heights, 0]; // sentinel
|
||
const stack = []; // indices
|
||
let maxArea = 0;
|
||
let maxInfo = null;
|
||
|
||
steps.push({stage:'init', msg:'初始化:末尾添加哨兵 0,单调栈为空', currentIdx:-1, stack:[], maxArea:0, areaCalc:null, barColors:{}});
|
||
|
||
for (let i = 0; i < h.length; i++) {
|
||
steps.push({stage:'visit', msg:`访问 i=${i},height=${h[i]}`, currentIdx:i, stack:[...stack], maxArea, areaCalc:null, barColors:{}});
|
||
|
||
while (stack.length > 0 && h[i] < h[stack[stack.length - 1]]) {
|
||
const poppedIdx = stack.pop();
|
||
const poppedH = h[poppedIdx];
|
||
const left = stack.length > 0 ? stack[stack.length - 1] : -1;
|
||
const width = i - left - 1;
|
||
const area = poppedH * width;
|
||
const colors = {};
|
||
colors[poppedIdx] = '#ef4444';
|
||
for (let j = left + 1; j < i; j++) colors[j] = '#fbbf24';
|
||
|
||
if (area > maxArea) {
|
||
maxArea = area;
|
||
maxInfo = {idx: poppedIdx, h: poppedH, left: left+1, right: i-1, width, area};
|
||
}
|
||
|
||
const calcStr = `h[${poppedIdx}]=${poppedH}, 左边界=${left < 0 ? '无' : left}, 右边界=${i-1} → 面积 = ${poppedH} × ${width} = ${area}`;
|
||
steps.push({stage:'pop', msg:`h[${i}]=${h[i]} < h[${poppedIdx}]=${poppedH},弹出 ${poppedIdx}。${calcStr}`, currentIdx:i, stack:[...stack], maxArea, areaCalc:{h:poppedH, width, area, poppedIdx, left:left+1, right:i-1}, barColors:colors});
|
||
}
|
||
|
||
stack.push(i);
|
||
steps.push({stage:'push', msg:`h[${i}]=${h[i]} 入栈`, currentIdx:i, stack:[...stack], maxArea, areaCalc:null, barColors:{}});
|
||
}
|
||
|
||
steps.push({stage:'done', msg:`遍历完毕,最大矩形面积 = ${maxArea}`, currentIdx:-1, stack:[], maxArea, areaCalc:maxInfo, barColors:{}});
|
||
}
|
||
|
||
function render(step) {
|
||
const s = steps[step];
|
||
const arr = heightsArr;
|
||
const n = arr.length;
|
||
const maxH = Math.max(...arr, 1);
|
||
|
||
// Histogram bars
|
||
let viz = '<div class="histo-container">';
|
||
for (let i = 0; i < n; i++) {
|
||
const pct = (arr[i] / maxH) * 85 + 10;
|
||
let barColor = s.barColors[i] || '#93c5fd';
|
||
if (i === s.currentIdx) barColor = '#f59e0b';
|
||
|
||
viz += `<div class="histo-bar-wrap">`;
|
||
viz += `<div class="histo-val" style="color:${i===s.currentIdx?'#f59e0b':'var(--text)'};">${arr[i]}</div>`;
|
||
viz += `<div class="histo-bar" style="height:${pct}%;background:${barColor};"></div>`;
|
||
viz += `<div class="histo-idx">${i}</div>`;
|
||
viz += '</div>';
|
||
}
|
||
viz += '</div>';
|
||
|
||
// Area calculation display
|
||
if (s.areaCalc) {
|
||
const ac = s.areaCalc;
|
||
viz += `<div class="area-label">面积 = ${ac.h} × ${ac.width} = <b>${ac.area}</b> (柱 ${ac.left}~${ac.right})</div>`;
|
||
}
|
||
|
||
// Max area so far
|
||
viz += `<div style="margin-top:6px;font-size:14px;">当前最大面积:<b style="color:var(--green);">${s.maxArea}</b></div>`;
|
||
|
||
// Stack
|
||
viz += '<div style="margin-top:10px;"><b>单调栈(高度递增,存索引):</b>';
|
||
const stackItems = s.stack.filter(i => i < n).map(i => `${i}(h=${arr[i]})`);
|
||
if (s.stack.includes(n)) stackItems.push(`${n}(哨兵)`);
|
||
viz += renderStack(stackItems.length > 0 ? stackItems : [], {topIndex: stackItems.length - 1});
|
||
viz += '</div>';
|
||
|
||
$('vizArea').innerHTML = viz;
|
||
|
||
let detail = `<div class="calc-block">${s.msg}</div>`;
|
||
detail += '<div style="margin-top:4px;font-size:12px;color:var(--text-secondary);">核心:单调递增栈,遇到更矮的柱子则弹出并计算以弹出柱为高度的矩形面积</div>';
|
||
$('detailContent').innerHTML = detail;
|
||
|
||
if (s.stage === 'done') {
|
||
let resMsg = `<div class="final-answer">最大矩形面积 = <b>${s.maxArea}</b>`;
|
||
if (s.areaCalc) {
|
||
const ac = s.areaCalc;
|
||
resMsg += `<br>高度=${ac.h},宽度=${ac.width}(区间 [${ac.left}, ${ac.right}])`;
|
||
}
|
||
resMsg += `<br>时间复杂度 O(n),空间 O(n)</div>`;
|
||
$('resultContent').innerHTML = resMsg;
|
||
}
|
||
|
||
$('hintText').textContent = s.msg;
|
||
const stages = ['init→初始化','visit→访问','push→入栈','pop→弹出','done→完成'];
|
||
$('pipeline').innerHTML = stages.map(st => {
|
||
const [k,l] = st.split('→');
|
||
return `<span class="pipe-step ${s.stage===k?'active':''}">${l}</span>`;
|
||
}).join('<i>→</i>');
|
||
}
|
||
|
||
function init() {
|
||
const sel = $('exampleSelect');
|
||
examples.forEach((e,i) => { sel.innerHTML += `<option value="${i}">${e.label}</option>`; });
|
||
$('inputArea').value = 'heights=[2,1,5,6,2,3]';
|
||
|
||
buildSteps(examples[0].heights);
|
||
stepCtrl = new StepController({onStep: render});
|
||
stepCtrl.setSteps(steps.map((_,i)=>i));
|
||
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
|
||
stepCtrl.onStep = (idx) => { render(idx); $('stepInfo').textContent = `步骤 ${idx+1} / ${steps.length}`; };
|
||
|
||
$('applyBtn').onclick = () => {
|
||
try {
|
||
const m = $('inputArea').value.match(/heights=\[([^\]]+)\]/);
|
||
if (!m) { alert('格式: heights=[2,1,5,6,2,3]'); return; }
|
||
const arr = m[1].split(',').map(Number);
|
||
buildSteps(arr);
|
||
stepCtrl.setSteps(steps.map((_,i)=>i)); render(0);
|
||
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
|
||
} catch(e) { alert('输入格式错误'); }
|
||
};
|
||
$('exampleSelect').onchange = () => {
|
||
const e = examples[parseInt($('exampleSelect').value)];
|
||
$('inputArea').value = `heights=[${e.heights}]`;
|
||
buildSteps(e.heights);
|
||
stepCtrl.setSteps(steps.map((_,i)=>i)); render(0);
|
||
};
|
||
$('prevBtn').onclick = () => stepCtrl.prev();
|
||
$('nextBtn').onclick = () => stepCtrl.next();
|
||
$('jumpBtn').onclick = () => stepCtrl.jumpToEnd();
|
||
$('autoBtn').onclick = () => { const on = stepCtrl.toggleAuto(); $('autoBtn').textContent = on ? '暂停' : '自动播放'; };
|
||
$('resetBtn').onclick = () => { stepCtrl.reset(); $('autoBtn').textContent = '自动播放'; };
|
||
}
|
||
init();
|
||
|
||
$('codeArea').innerHTML = renderCode(`def largestRectangleArea(heights):
|
||
heights.append(0) # 哨兵
|
||
stack = []
|
||
max_area = 0
|
||
for i, h in enumerate(heights):
|
||
while stack and h < heights[stack[-1]]:
|
||
height = heights[stack.pop()]
|
||
left = stack[-1] if stack else -1
|
||
width = i - left - 1
|
||
max_area = max(max_area, height * width)
|
||
stack.append(i)
|
||
heights.pop() # 恢复
|
||
return max_area`, {lang:'Python'});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|