Files
illustrated-algorithm/largest-rectangle-in-histogram/index.html
T

235 lines
9.5 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>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>