Files

455 lines
19 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>018. 矩阵置零 – 图解</title>
<link rel="stylesheet" href="../shared/style.css">
<style>
.vis-area { min-height: 120px; padding: 16px 0; }
.code-section { margin-top: 16px; }
.legend { display:flex; flex-wrap:wrap; gap:12px; margin:10px 0; font-size:13px; }
.legend span { display:inline-flex; align-items:center; gap:4px; }
.legend .dot { width:14px; height:14px; border-radius:4px; display:inline-block; }
.marker-row-cell { background:#fef3c7 !important; border-color:#f59e0b !important; color:#92400e; font-weight:700; }
.zeroed-cell { background:#fee2e2 !important; border-color:#ef4444 !important; color:#991b1b; }
.first-row-marker { background:#ede9fe !important; border-color:#8b5cf6 !important; color:#5b21b6; }
.first-col-marker { background:#cffafe !important; border-color:#06b6d4 !important; color:#155e75; }
.origin-zero { background:#fef3c7 !important; border-color:#f59e0b !important; color:#92400e; }
.phase-tag { display:inline-block; padding:2px 8px; border-radius:999px; font-size:11px; font-weight:700; margin-right:6px; }
.phase-tag.mark { background:#fef3c7; color:#92400e; }
.phase-tag.zero { background:#fee2e2; color:#991b1b; }
.phase-tag.first { background:#ede9fe; color:#5b21b6; }
.phase-tag.done { background:#dcfce7; color:#166534; }
.info-row { display:flex; gap:16px; flex-wrap:wrap; margin-top:8px; font-size:13px; }
.info-row .tag { padding:2px 8px; border-radius:6px; font-weight:600; }
.tag.green { background:#dcfce7; color:#166534; }
.tag.red { background:#fee2e2; color:#991b1b; }
.tag.blue { background:#dbeafe; color:#1e40af; }
.tag.purple { background:#ede9fe; color:#5b21b6; }
.tag.cyan { background:#cffafe; color:#155e75; }
.border-label { font-size:11px; color:var(--text-muted); text-align:center; padding:2px 0; }
</style>
</head>
<body>
<div class="container">
<h1>🔲 018. 矩阵置零 <span class="badge medium">中等</span></h1>
<p class="subtitle">分类:矩阵 | LeetCode Hot 100</p>
<div class="controls" id="controls">
<label for="inputArea">输入:</label>
<input type="text" id="inputArea" placeholder="默认示例,可自定义">
<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 = [
{input: [[1,1,1],[1,0,1],[1,1,1]], label: '3×3 含1个0'},
{input: [[0,1,2,0],[3,4,5,2],[1,3,1,5]], label: '3×4 含2个0'},
{input: [[1,2,3,4],[5,0,7,8],[9,10,11,12],[13,14,15,0]], label: '4×4 含2个0'},
];
let origMatrix, steps, stepCtrl;
function buildSteps(mat) {
origMatrix = mat.map(r => [...r]);
const m = mat.length, n = mat[0].length;
const work = mat.map(r => [...r]);
steps = [];
let firstRowZero = false, firstColZero = false;
// --- Step 0: Init ---
steps.push({
stage: 'init', msg: '用第一行和第一列做标记空间,实现 O(1) 额外空间的矩阵置零',
phase: 'init', firstRowZero: null, firstColZero: null,
currentCell: null, markerCells: {}, zeroedCells: {},
scanPos: null, scanRow: -1, scanCol: -1,
codeLine: 1
});
// --- Step 1: Scan first row for zeros ---
for (let j = 0; j < n; j++) {
if (work[0][j] === 0) firstRowZero = true;
steps.push({
stage: 'scan_first_row', msg: `检查第一行:matrix[0][${j}] = ${work[0][j]}${work[0][j]===0 ? ' → 发现0!' : ''}`,
phase: 'scan', firstRowZero: firstRowZero || null, firstColZero: null,
currentCell: `0,${j}`, markerCells: {}, zeroedCells: {},
scanPos: `0,${j}`, scanRow: 0, scanCol: j,
codeLine: 2
});
}
steps.push({
stage: 'first_row_result', msg: `第一行扫描完毕:${firstRowZero ? '✅ 含有0,首行需最终置零' : '❌ 无0,首行无需置零'}`,
phase: 'scan', firstRowZero, firstColZero: null,
currentCell: null, markerCells: {}, zeroedCells: {},
scanPos: null, scanRow: 0, scanCol: -1,
codeLine: 2
});
// --- Step 2: Scan first col for zeros ---
for (let i = 0; i < m; i++) {
if (work[i][0] === 0) firstColZero = true;
steps.push({
stage: 'scan_first_col', msg: `检查第一列:matrix[${i}][0] = ${work[i][0]}${work[i][0]===0 ? ' → 发现0!' : ''}`,
phase: 'scan', firstRowZero, firstColZero: firstColZero || null,
currentCell: `${i},0`, markerCells: {}, zeroedCells: {},
scanPos: `${i},0`, scanRow: i, scanCol: 0,
codeLine: 3
});
}
steps.push({
stage: 'first_col_result', msg: `第一列扫描完毕:${firstColZero ? '✅ 含有0,首列需最终置零' : '❌ 无0,首列无需置零'}`,
phase: 'scan', firstRowZero, firstColZero,
currentCell: null, markerCells: {}, zeroedCells: {},
scanPos: null, scanRow: -1, scanCol: 0,
codeLine: 3
});
// --- Step 3: Mark inner cells ---
steps.push({
stage: 'mark_start', msg: '扫描内部区域 (i≥1, j≥1):发现0则在对应的首行/首列位置做标记',
phase: 'mark', firstRowZero, firstColZero,
currentCell: null, markerCells: {}, zeroedCells: {},
scanPos: null, scanRow: -1, scanCol: -1,
codeLine: 5
});
const markerCells = {};
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
if (work[i][j] === 0) {
markerCells[`${i},0`] = true;
markerCells[`0,${j}`] = true;
const snapshot = {...markerCells};
steps.push({
stage: 'mark', msg: `发现 matrix[${i}][${j}] = 0!→ 标记 row_marker[${i}][0] = 0,col_marker[0][${j}] = 0`,
phase: 'mark', firstRowZero, firstColZero,
currentCell: `${i},${j}`, markerCells: {...snapshot}, zeroedCells: {},
scanPos: `${i},${j}`, scanRow: i, scanCol: j,
codeLine: 7
});
}
}
}
steps.push({
stage: 'mark_done', msg: `标记阶段完毕,共在首行/首列做了 ${Object.keys(markerCells).length} 处标记`,
phase: 'mark', firstRowZero, firstColZero,
currentCell: null, markerCells: {...markerCells}, zeroedCells: {},
scanPos: null, scanRow: -1, scanCol: -1,
codeLine: 5
});
// --- Step 4: Zero rows based on first column markers ---
const zeroedCells = {};
steps.push({
stage: 'zero_start', msg: '根据标记,对内部区域置零:先按行标记置零,再按列标记置零',
phase: 'zero', firstRowZero, firstColZero,
currentCell: null, markerCells: {...markerCells}, zeroedCells: {},
scanPos: null, scanRow: -1, scanCol: -1,
codeLine: 9
});
for (let i = 1; i < m; i++) {
if (work[i][0] === 0) {
steps.push({
stage: 'zero_row', msg: `第一列标记 matrix[${i}][0] = 0 → 第${i}行全部置零`,
phase: 'zero', firstRowZero, firstColZero,
currentCell: `${i},0`, markerCells: {...markerCells}, zeroedCells: {...zeroedCells},
scanPos: `${i},0`, scanRow: i, scanCol: -1,
codeLine: 10
});
for (let j = 1; j < n; j++) {
zeroedCells[`${i},${j}`] = true;
}
steps.push({
stage: 'zero_row_done', msg: `第${i}行已置零(${n-1}个元素)`,
phase: 'zero', firstRowZero, firstColZero,
currentCell: null, markerCells: {...markerCells}, zeroedCells: {...zeroedCells},
scanPos: null, scanRow: i, scanCol: -1,
codeLine: 11
});
}
}
for (let j = 1; j < n; j++) {
if (work[0][j] === 0) {
steps.push({
stage: 'zero_col', msg: `第一行标记 matrix[0][${j}] = 0 → 第${j}列全部置零`,
phase: 'zero', firstRowZero, firstColZero,
currentCell: `0,${j}`, markerCells: {...markerCells}, zeroedCells: {...zeroedCells},
scanPos: `0,${j}`, scanRow: -1, scanCol: j,
codeLine: 13
});
for (let i = 1; i < m; i++) {
zeroedCells[`${i},${j}`] = true;
}
steps.push({
stage: 'zero_col_done', msg: `第${j}列已置零(${m-1}个元素)`,
phase: 'zero', firstRowZero, firstColZero,
currentCell: null, markerCells: {...markerCells}, zeroedCells: {...zeroedCells},
scanPos: null, scanRow: -1, scanCol: j,
codeLine: 14
});
}
}
// --- Step 5: First row ---
if (firstRowZero) {
for (let j = 0; j < n; j++) zeroedCells[`0,${j}`] = true;
steps.push({
stage: 'zero_first_row', msg: `首行有0标记,首行${n}个元素全部置零`,
phase: 'first', firstRowZero, firstColZero,
currentCell: null, markerCells: {}, zeroedCells: {...zeroedCells},
scanPos: null, scanRow: 0, scanCol: -1,
codeLine: 16
});
}
if (firstColZero) {
for (let i = 0; i < m; i++) zeroedCells[`${i},0`] = true;
steps.push({
stage: 'zero_first_col', msg: `首列有0标记,首列${m}个元素全部置零`,
phase: 'first', firstRowZero, firstColZero,
currentCell: null, markerCells: {}, zeroedCells: {...zeroedCells},
scanPos: null, scanRow: -1, scanCol: 0,
codeLine: 18
});
}
// --- Build result ---
const result = mat.map(r => [...r]);
for (const key of Object.keys(zeroedCells)) {
const [r, c] = key.split(',').map(Number);
result[r][c] = 0;
}
steps.push({
stage: 'done', msg: '完成!矩阵已原地修改,空间复杂度 O(1)',
phase: 'done', firstRowZero, firstColZero,
currentCell: null, markerCells: {}, zeroedCells: {...zeroedCells}, result,
scanPos: null, scanRow: -1, scanCol: -1,
codeLine: -1
});
}
function render(step) {
const s = steps[step];
const m = origMatrix.length, n = origMatrix[0].length;
// Build cell highlights
function cellClass(val, r, c) {
const key = `${r},${c}`;
// Zeroed cells (red)
if (s.zeroedCells[key]) return 'zeroed-cell';
// Current cell
if (s.currentCell === key) return 'current';
// Marker cells in first row (purple)
if (s.markerCells[key] && r === 0) return 'first-row-marker';
// Marker cells in first col (cyan)
if (s.markerCells[key] && c === 0) return 'first-col-marker';
// Original zeros being scanned
if (s.scanPos === key && val === 0) return 'origin-zero';
return '';
}
// Legend
let viz = '<div class="legend">';
viz += '<span><span class="dot" style="background:#fef3c7;border:1px solid #f59e0b;"></span> 原始0</span>';
viz += '<span><span class="dot" style="background:#ede9fe;border:1px solid #8b5cf6;"></span> 首行标记</span>';
viz += '<span><span class="dot" style="background:#cffafe;border:1px solid #06b6d4;"></span> 首列标记</span>';
viz += '<span><span class="dot" style="background:#fee2e2;border:1px solid #ef4444;"></span> 已置零</span>';
viz += '<span><span class="dot" style="background:#fef3c7;border:2px solid #f59e0b;box-shadow:0 0 0 2px rgba(245,158,11,.3);"></span> 当前位置</span>';
viz += '</div>';
// Grid with row/col labels
viz += '<div style="display:flex;gap:12px;flex-wrap:wrap;">';
viz += '<div>';
viz += '<div style="margin-bottom:6px;font-weight:600;font-size:14px;">原始矩阵</div>';
viz += renderGrid(origMatrix, { cellClass, cellSize: 44 });
viz += '</div>';
// Show result when available
if (s.result) {
viz += '<div>';
viz += '<div style="margin-bottom:6px;font-weight:600;font-size:14px;">置零结果</div>';
viz += renderGrid(s.result, { cellSize: 44 });
viz += '</div>';
}
viz += '</div>';
// Phase info box
if (s.phase === 'mark') {
const markers = Object.keys(s.markerCells);
const rowsMarked = [...new Set(markers.filter(k => k.split(',')[1]==='0').map(k=>k.split(',')[0]))];
const colsMarked = [...new Set(markers.filter(k => k.split(',')[0]==='0').map(k=>k.split(',')[1]))];
viz += '<div style="margin-top:12px;padding:10px 14px;background:#fef3c7;border-radius:8px;font-size:13px;line-height:1.7;">';
viz += '<b>标记状态:</b>需置零的行 = [' + rowsMarked.join(', ') + '],需置零的列 = [' + colsMarked.join(', ') + ']';
viz += '</div>';
}
if (s.phase === 'zero' || s.phase === 'first') {
const count = Object.keys(s.zeroedCells).length;
viz += '<div style="margin-top:12px;padding:10px 14px;background:#fee2e2;border-radius:8px;font-size:13px;line-height:1.7;">';
viz += `<b>置零进度:</b>已置零 ${count} 个格子`;
viz += '</div>';
}
$('vizArea').innerHTML = viz;
// Detail panel
const phaseLabels = {init:'初始化', scan:'扫描首行首列', mark:'标记', zero:'根据标记置零', first:'首行首列置零', done:'完成'};
const phaseClass = {init:'blue', scan:'blue', mark:'mark', zero:'zero', first:'first', done:'done'};
let detail = `<div style="margin-bottom:8px;"><span class="phase-tag ${phaseClass[s.phase]||''}">${phaseLabels[s.phase]||s.phase}</span></div>`;
detail += '<div class="calc-block">' + s.msg + '</div>';
detail += '<div class="info-row">';
if (s.firstRowZero !== null) detail += `<span class="tag ${s.firstRowZero?'red':'green'}">首行含0: ${s.firstRowZero?'是':'否'}</span>`;
if (s.firstColZero !== null) detail += `<span class="tag ${s.firstColZero?'red':'green'}">首列含0: ${s.firstColZero?'是':'否'}</span>`;
const mCount = Object.keys(s.markerCells).length;
if (mCount > 0) detail += `<span class="tag purple">标记数: ${mCount}</span>`;
const zCount = Object.keys(s.zeroedCells).length;
if (zCount > 0) detail += `<span class="tag red">已置零: ${zCount}</span>`;
detail += '</div>';
$('detailContent').innerHTML = detail;
// Result panel
if (s.stage === 'done') {
const rows = s.result.map(r => '[' + r.join(',') + ']');
$('resultContent').innerHTML = `<div class="final-answer">置零结果:<b>[${rows.join(', ')}]</b><div class="complexity">时间 O(mn) | 空间 O(1)</div></div>`;
}
$('hintText').textContent = s.msg;
const pipe = [
['init','初始化'],['scan_first_row','扫描首行'],['scan_first_col','扫描首列'],
['mark','标记0位置'],['zero_row','行置零'],['zero_col','列置零'],
['zero_first_row','首行置零'],['zero_first_col','首列置零'],['done','完成']
];
const stageToPhase = s => {
if (s.startsWith('scan_first_row')) return 'scan_first_row';
if (s.startsWith('scan_first_col')) return 'scan_first_col';
if (s.startsWith('mark')) return 'mark';
if (s.startsWith('zero_row')) return 'zero_row';
if (s.startsWith('zero_col')) return 'zero_col';
if (s.startsWith('zero_first_row')) return 'zero_first_row';
if (s.startsWith('zero_first_col')) return 'zero_first_col';
return s;
};
const activePhase = stageToPhase(s.stage);
$('pipeline').innerHTML = pipe.map(([k,l]) =>
`<span class="pipe-step ${activePhase===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 = JSON.stringify(examples[0].input);
buildSteps(examples[0].input);
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 mat = JSON.parse($('inputArea').value);
if (!Array.isArray(mat) || !Array.isArray(mat[0])) throw 0;
buildSteps(mat);
stepCtrl.setSteps(steps.map((_, i) => i));
render(0);
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
} catch (e) { alert('请输入合法 JSON 二维数组,如 [[1,1,1],[1,0,1],[1,1,1]]'); }
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
$('inputArea').value = JSON.stringify(e.input);
buildSteps(e.input);
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 setZeroes(matrix):
m, n = len(matrix), len(matrix[0])
# 1. 记录首行/首列是否本身含0
first_row_zero = any(matrix[0][j] == 0 for j in range(n))
first_col_zero = any(matrix[i][0] == 0 for i in range(m))
# 2. 用首行/首列作为标记:内部0对应位置置0
for i in range(1, m):
for j in range(1, n):
if matrix[i][j] == 0:
matrix[i][0] = 0 # 行标记
matrix[0][j] = 0 # 列标记
# 3. 根据标记对内部区域置零
for i in range(1, m):
if matrix[i][0] == 0:
for j in range(1, n):
matrix[i][j] = 0
for j in range(1, n):
if matrix[0][j] == 0:
for i in range(1, m):
matrix[i][j] = 0
# 4. 处理首行/首列
if first_row_zero:
for j in range(n):
matrix[0][j] = 0
if first_col_zero:
for i in range(m):
matrix[i][0] = 0`, { lang: 'Python' });
})();
</script>
</body>
</html>