feat: 添加 LeetCode 2035 将数组分成两个数组并最小化数组和的差图解
- 实现 Meet in the Middle 算法的交互式可视化 - 包含前后半部分子集和生成、排序与二分查找过程的分步演示 - 支持自定义输入数组和调节动画速度
This commit is contained in:
+606
@@ -0,0 +1,606 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-Hans">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>035. 将数组分成两个数组并最小化数组和的差 – 图解</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--blue: #3b82f6; --green: #16a34a; --purple: #8b5cf6;
|
||||||
|
--bg: #f1f5f9; --card: #ffffff; --border: #e2e8f0;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||||
|
background: var(--bg); color: #0f172a;
|
||||||
|
}
|
||||||
|
.container { max-width: 1440px; margin: 0 auto; padding: 20px; }
|
||||||
|
h1 { font-size: 25px; margin: 0 0 12px; }
|
||||||
|
h3 { margin: 0 0 10px; font-size: 16px; }
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
background: var(--card); border: 1px solid var(--border); border-radius: 14px;
|
||||||
|
padding: 12px 16px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.controls input {
|
||||||
|
width: 240px; padding: 8px 10px; border: 1px solid #cbd5e1; border-radius: 8px;
|
||||||
|
font-family: monospace; font-size: 14px;
|
||||||
|
}
|
||||||
|
.controls button, .controls select {
|
||||||
|
padding: 8px 12px; border: 1px solid #cbd5e1; border-radius: 8px;
|
||||||
|
background: #f8fafc; font-size: 14px; cursor: pointer;
|
||||||
|
}
|
||||||
|
.controls button:hover { background: #e2e8f0; }
|
||||||
|
.controls button:disabled { opacity: .45; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.pipeline {
|
||||||
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
||||||
|
background: #eef2ff; border-radius: 14px; padding: 10px 16px; margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.pipe-step {
|
||||||
|
background: white; border: 1px solid #c7d2fe; border-radius: 999px;
|
||||||
|
padding: 4px 10px; color: #3730a3;
|
||||||
|
}
|
||||||
|
.pipe-step.active { background: #4f46e5; color: white; border-color: #4f46e5; font-weight: 600; }
|
||||||
|
.pipeline i { color: #94a3b8; font-style: normal; }
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
background: #fff7ed; border-left: 4px solid #f59e0b; border-radius: 8px;
|
||||||
|
padding: 10px 16px; margin-bottom: 12px; line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels { display: flex; flex-direction: column; gap: 16px; }
|
||||||
|
.panel {
|
||||||
|
background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 20px;
|
||||||
|
}
|
||||||
|
.panel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||||
|
@media (max-width: 900px) { .panel-grid { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
|
.formula-box {
|
||||||
|
background: #f0f9ff; border-radius: 12px; padding: 12px 16px;
|
||||||
|
font-size: 15px; line-height: 1.8; margin-bottom: 12px; color: #0c4a6e;
|
||||||
|
}
|
||||||
|
.nums-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
||||||
|
.chip {
|
||||||
|
display: inline-block; min-width: 40px; text-align: center; padding: 6px 10px;
|
||||||
|
border-radius: 10px; font-family: monospace; font-weight: 600;
|
||||||
|
}
|
||||||
|
.chip.left { background: #dbeafe; color: #1e40af; }
|
||||||
|
.chip.right { background: #ffedd5; color: #9a3412; }
|
||||||
|
.split { font-size: 22px; font-weight: 800; margin: 0 8px; color: #64748b; }
|
||||||
|
.nums-meta { margin-top: 10px; font-size: 14px; color: #475569; }
|
||||||
|
|
||||||
|
.table-wrap { overflow-x: auto; margin-top: 8px; }
|
||||||
|
table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
||||||
|
th, td { border: 1px solid #e2e8f0; padding: 5px 8px; text-align: center; }
|
||||||
|
th { background: #f8fafc; color: #475569; }
|
||||||
|
.sign-plus { color: #16a34a; font-weight: 800; }
|
||||||
|
.sign-minus { color: #dc2626; font-weight: 800; }
|
||||||
|
.calc { font-family: monospace; font-size: 12px; color: #334155; }
|
||||||
|
|
||||||
|
.group-header { background: #eef2ff; font-weight: 600; }
|
||||||
|
.group-active { background: #c7d2fe; }
|
||||||
|
.nearest-row { background: #bbf7d0; }
|
||||||
|
.current-highlight { background: #bae6fd !important; }
|
||||||
|
.processed { opacity: .85; }
|
||||||
|
.dim { opacity: .3; }
|
||||||
|
.best-diff { font-weight: 800; color: var(--green); }
|
||||||
|
.nearest-val { color: var(--green); font-weight: 700; }
|
||||||
|
|
||||||
|
.chart-wrap {
|
||||||
|
background: #f8fafc; border: 1px solid var(--border); border-radius: 12px;
|
||||||
|
padding: 16px 16px 20px; margin-top: 14px;
|
||||||
|
}
|
||||||
|
.chart-scale {
|
||||||
|
position: relative; height: 80px; border-bottom: 2px solid #94a3b8;
|
||||||
|
margin: 0 8px 26px;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
position: absolute; transform: translateX(-50%); width: 14px; height: 14px;
|
||||||
|
border-radius: 50%; background: var(--blue); border: 2px solid white;
|
||||||
|
box-shadow: 0 0 0 2px var(--blue);
|
||||||
|
}
|
||||||
|
.dot.nearest { background: var(--green); box-shadow: 0 0 0 2px var(--green); }
|
||||||
|
.dot-label {
|
||||||
|
position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%);
|
||||||
|
background: rgba(255,255,255,.95); padding: 2px 5px; border-radius: 6px;
|
||||||
|
font-size: 12px; font-family: monospace; white-space: nowrap; border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.target-line {
|
||||||
|
position: absolute; top: 0; bottom: 0; width: 2px; background: var(--purple);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.target-tick {
|
||||||
|
position: absolute; bottom: -22px; transform: translateX(-50%);
|
||||||
|
font-size: 12px; color: var(--purple); font-weight: 700; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.scale-min-label { position: absolute; left: 0; bottom: -26px; font-size: 11px; color: #94a3b8; }
|
||||||
|
.scale-max-label { position: absolute; right: 0; bottom: -26px; font-size: 11px; color: #94a3b8; }
|
||||||
|
|
||||||
|
.calc-block {
|
||||||
|
background: #f8fafc; border-radius: 12px; padding: 12px 16px;
|
||||||
|
font-size: 15px; line-height: 1.9; margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.current-answer {
|
||||||
|
margin-top: 12px; padding: 10px 16px; background: #ecfdf5; border-radius: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.final-answer {
|
||||||
|
margin-top: 16px; padding: 16px 20px; background: #ecfdf5;
|
||||||
|
border: 2px solid #86efac; border-radius: 14px; line-height: 1.9;
|
||||||
|
}
|
||||||
|
.final-answer b { color: var(--green); }
|
||||||
|
|
||||||
|
#debug {
|
||||||
|
display: none; background: #fee2e2; color: #b91c1c; padding: 12px; border-radius: 10px;
|
||||||
|
margin-bottom: 14px; white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🧮 035. 将数组分成两个数组并最小化数组和的差</h1>
|
||||||
|
<div id="debug"></div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<label for="numsInput">nums =</label>
|
||||||
|
<input id="numsInput" value="3, 9, 7, 3" title="偶数个整数,建议长度 ≤ 12">
|
||||||
|
<button id="applyBtn">生成图解</button>
|
||||||
|
<select id="exampleSelect">
|
||||||
|
<option value="[3,9,7,3]">示例 1:[3,9,7,3]</option>
|
||||||
|
<option value="[-36,36]">示例 2:[-36,36]</option>
|
||||||
|
<option value="[2,-1,0,4,-2,-9]">示例 3:[2,-1,0,4,-2,-9]</option>
|
||||||
|
</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">
|
||||||
|
<span class="pipe-step" data-stage="formula">① 符号转化</span><i>→</i>
|
||||||
|
<span class="pipe-step" data-stage="left">② 左半枚举</span><i>→</i>
|
||||||
|
<span class="pipe-step" data-stage="sort">③ 分组排序</span><i>→</i>
|
||||||
|
<span class="pipe-step" data-stage="right">④ 右半枚举 + 二分</span><i>→</i>
|
||||||
|
<span class="pipe-step" data-stage="done">⑤ 答案</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hint">
|
||||||
|
<span id="stepInfo"></span><br>
|
||||||
|
<span id="hintText"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panels">
|
||||||
|
<div class="panel" id="numsPanel"></div>
|
||||||
|
<div class="panel-grid">
|
||||||
|
<div class="panel" id="leftPanel"></div>
|
||||||
|
<div class="panel" id="rightPanel"></div>
|
||||||
|
</div>
|
||||||
|
<div class="panel" id="resultPanel"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
"use strict";
|
||||||
|
(function() {
|
||||||
|
// ---------- 工具 ----------
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
function popcount(x) {
|
||||||
|
let c = 0;
|
||||||
|
while (x) { c += x & 1; x >>>= 1; }
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
function signHTML(signs) {
|
||||||
|
return signs.map(s => s === 1
|
||||||
|
? '<span class="sign-plus">+</span>'
|
||||||
|
: '<span class="sign-minus">−</span>').join('');
|
||||||
|
}
|
||||||
|
function signText(signs) {
|
||||||
|
return signs.map(s => s === 1 ? '+' : '-').join(' ');
|
||||||
|
}
|
||||||
|
function fmtArr(a) { return '[' + a.join(', ') + ']'; }
|
||||||
|
function signedTerm(s, v) {
|
||||||
|
const vv = v < 0 ? `(${v})` : `${v}`;
|
||||||
|
return (s === 1 ? '+' : '-') + vv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 全局状态 ----------
|
||||||
|
let nums = [3, 9, 7, 3];
|
||||||
|
let n = 0;
|
||||||
|
let left = [];
|
||||||
|
let right = [];
|
||||||
|
let leftRows = [];
|
||||||
|
let rightRows = [];
|
||||||
|
let leftGroups = [];
|
||||||
|
let steps = [];
|
||||||
|
let currentStep = 0;
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
// ---------- 构建数据 ----------
|
||||||
|
function buildData() {
|
||||||
|
n = nums.length / 2;
|
||||||
|
left = nums.slice(0, n);
|
||||||
|
right = nums.slice(n);
|
||||||
|
leftRows = [];
|
||||||
|
rightRows = [];
|
||||||
|
const M = 1 << n;
|
||||||
|
|
||||||
|
for (let mask = 0; mask < M; mask++) {
|
||||||
|
const signs = [];
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const s = (mask & (1 << i)) ? 1 : -1;
|
||||||
|
signs.push(s);
|
||||||
|
sum += s * left[i];
|
||||||
|
}
|
||||||
|
leftRows.push({ id: mask, signs, k: popcount(mask), sum });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let mask = 0; mask < M; mask++) {
|
||||||
|
const signs = [];
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const s = (mask & (1 << i)) ? 1 : -1;
|
||||||
|
signs.push(s);
|
||||||
|
sum += s * right[i];
|
||||||
|
}
|
||||||
|
rightRows.push({ id: mask, signs, k: popcount(mask), sum });
|
||||||
|
}
|
||||||
|
|
||||||
|
leftGroups = [];
|
||||||
|
for (let k = 0; k <= n; k++) leftGroups.push([]);
|
||||||
|
for (const r of leftRows) leftGroups[r.k].push(r);
|
||||||
|
for (let k = 0; k <= n; k++) leftGroups[k].sort((a, b) => a.sum - b.sum);
|
||||||
|
|
||||||
|
// 构建步骤序列
|
||||||
|
steps = [];
|
||||||
|
steps.push({ type: 'formula', title: '问题转化' });
|
||||||
|
for (let i = 0; i < leftRows.length; i++) {
|
||||||
|
steps.push({ type: 'left', idx: i, title: '枚举左半(' + signText(leftRows[i].signs) + ')' });
|
||||||
|
}
|
||||||
|
steps.push({ type: 'sort', title: '左半分组排序' });
|
||||||
|
for (let i = 0; i < rightRows.length; i++) {
|
||||||
|
steps.push({ type: 'right', idx: i, title: '处理右半(' + signText(rightRows[i].signs) + ')' });
|
||||||
|
}
|
||||||
|
steps.push({ type: 'done', title: '完成' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 二分查找最接近目标 ----------
|
||||||
|
function nearestInGroup(group, target) {
|
||||||
|
let lo = 0, hi = group.length - 1;
|
||||||
|
if (target <= group[0].sum) return group[0];
|
||||||
|
if (target >= group[hi].sum) return group[hi];
|
||||||
|
while (lo + 1 < hi) {
|
||||||
|
const mid = (lo + hi) >> 1;
|
||||||
|
if (group[mid].sum < target) lo = mid;
|
||||||
|
else hi = mid;
|
||||||
|
}
|
||||||
|
return Math.abs(group[lo].sum - target) <= Math.abs(group[hi].sum - target) ? group[lo] : group[hi];
|
||||||
|
}
|
||||||
|
|
||||||
|
function processRight(row) {
|
||||||
|
const q = row.sum;
|
||||||
|
const needK = n - row.k;
|
||||||
|
const group = leftGroups[needK];
|
||||||
|
const target = -q;
|
||||||
|
const nearest = nearestInGroup(group, target);
|
||||||
|
const total = q + nearest.sum;
|
||||||
|
return {
|
||||||
|
rightRow: row,
|
||||||
|
q: q,
|
||||||
|
needK: needK,
|
||||||
|
target: target,
|
||||||
|
nearestRow: nearest,
|
||||||
|
p: nearest.sum,
|
||||||
|
total: total,
|
||||||
|
diff: Math.abs(total)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 根据步骤号计算当前状态 ----------
|
||||||
|
function computeState(stepIndex) {
|
||||||
|
const state = {
|
||||||
|
stage: 'formula',
|
||||||
|
enumeratedLeftCount: 0,
|
||||||
|
sorted: false,
|
||||||
|
rightProcessed: [],
|
||||||
|
currentRight: null,
|
||||||
|
ans: Infinity,
|
||||||
|
best: null
|
||||||
|
};
|
||||||
|
for (let s = 0; s <= stepIndex; s++) {
|
||||||
|
const step = steps[s];
|
||||||
|
if (!step) continue;
|
||||||
|
if (step.type === 'formula') {
|
||||||
|
state.stage = 'formula';
|
||||||
|
} else if (step.type === 'left') {
|
||||||
|
state.stage = 'left';
|
||||||
|
state.enumeratedLeftCount++;
|
||||||
|
} else if (step.type === 'sort') {
|
||||||
|
state.stage = 'sort';
|
||||||
|
state.sorted = true;
|
||||||
|
} else if (step.type === 'right') {
|
||||||
|
const info = processRight(rightRows[step.idx]);
|
||||||
|
state.rightProcessed.push(info);
|
||||||
|
state.currentRight = info;
|
||||||
|
if (info.diff < state.ans) {
|
||||||
|
state.ans = info.diff;
|
||||||
|
state.best = info;
|
||||||
|
}
|
||||||
|
state.stage = 'right';
|
||||||
|
} else if (step.type === 'done') {
|
||||||
|
state.stage = 'done';
|
||||||
|
if (state.best) state.currentRight = state.best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 渲染 ----------
|
||||||
|
function renderNums(state) {
|
||||||
|
const h = [];
|
||||||
|
h.push('<h3>📦 数组拆分(折半)</h3>');
|
||||||
|
h.push('<div class="formula-box">');
|
||||||
|
h.push('最小化 <b>|sum(A) - sum(B)|</b>,等价于给每个元素分配符号:');
|
||||||
|
h.push(' <b>+1</b> 表示放入 A,<b>-1</b> 表示放入 B,且恰好 n 个 +1、n 个 -1。<br>');
|
||||||
|
h.push('注意这里的 +/− 是分组符号,原始元素可能为负数;贡献 = 符号 × 数值。');
|
||||||
|
h.push('</div>');
|
||||||
|
h.push('<div class="nums-line">');
|
||||||
|
h.push('<span class="half-label">左半 L</span>');
|
||||||
|
left.forEach(v => h.push(`<span class="chip left">${v}</span>`));
|
||||||
|
h.push('<span class="split">|</span>');
|
||||||
|
h.push('<span class="half-label">右半 R</span>');
|
||||||
|
right.forEach(v => h.push(`<span class="chip right">${v}</span>`));
|
||||||
|
h.push('</div>');
|
||||||
|
h.push(`<div class="nums-meta">n = ${n},共 ${2 * n} 个数,每半需枚举 2^${n} = ${1 << n} 种符号。</div>`);
|
||||||
|
$('numsPanel').innerHTML = h.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLeftTable(state) {
|
||||||
|
const h = [];
|
||||||
|
h.push(state.sorted ? '<h3>① 左半结果:按 k 分组,组内排序</h3>' : '<h3>① 左半枚举</h3>');
|
||||||
|
h.push('<div class="table-wrap"><table><thead><tr>');
|
||||||
|
h.push('<th>#</th><th>符号</th><th>k</th><th>计算</th><th>和</th>');
|
||||||
|
h.push('</tr></thead><tbody>');
|
||||||
|
|
||||||
|
if (!state.sorted) {
|
||||||
|
for (let i = 0; i < leftRows.length; i++) {
|
||||||
|
const r = leftRows[i];
|
||||||
|
const enumerated = i < state.enumeratedLeftCount;
|
||||||
|
const isCur = state.stage === 'left' && i === state.enumeratedLeftCount - 1;
|
||||||
|
const cls = `${enumerated ? '' : 'dim'} ${isCur ? 'current-highlight' : ''}`;
|
||||||
|
h.push(`<tr class="${cls}">`);
|
||||||
|
h.push(`<td>${r.id}</td><td>${signHTML(r.signs)}</td><td>${r.k}</td>`);
|
||||||
|
h.push(`<td class="calc">${r.signs.map((s, i) => signedTerm(s, left[i])).join(' ')}</td>`);
|
||||||
|
h.push(`<td>${r.sum}</td></tr>`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let k = 0; k <= n; k++) {
|
||||||
|
const hl = state.currentRight && state.currentRight.needK === k;
|
||||||
|
h.push(`<tr class="group-header ${hl ? 'group-active' : ''}"><td colspan="5">k = ${k}(共 ${leftGroups[k].length} 个)</td></tr>`);
|
||||||
|
for (const r of leftGroups[k]) {
|
||||||
|
const isSel = state.currentRight && state.currentRight.nearestRow.id === r.id;
|
||||||
|
h.push(`<tr class="${isSel ? 'nearest-row' : ''}">`);
|
||||||
|
h.push(`<td>${r.id}</td><td>${signHTML(r.signs)}</td><td>${r.k}</td>`);
|
||||||
|
h.push(`<td class="calc">${r.signs.map((s, i) => signedTerm(s, left[i])).join(' ')}</td>`);
|
||||||
|
h.push(`<td>${r.sum}</td></tr>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.push('</tbody></table></div>');
|
||||||
|
$('leftPanel').innerHTML = h.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRightTable(state) {
|
||||||
|
const h = [];
|
||||||
|
h.push('<h3>② 右半枚举 + 二分匹配</h3>');
|
||||||
|
h.push('<div class="table-wrap"><table><thead><tr>');
|
||||||
|
h.push('<th>#</th><th>符号</th><th>k</th><th>右半和 q</th><th>需 k\'</th><th>目标 -q</th><th>选 p</th><th>p+q</th><th>|diff|</th>');
|
||||||
|
h.push('</tr></thead><tbody>');
|
||||||
|
|
||||||
|
for (let i = 0; i < rightRows.length; i++) {
|
||||||
|
const r = rightRows[i];
|
||||||
|
const info = state.rightProcessed.filter(x => x.rightRow.id === r.id)[0];
|
||||||
|
const isCur = state.currentRight && state.currentRight.rightRow.id === r.id;
|
||||||
|
let cls = '';
|
||||||
|
if (!info) cls = 'dim';
|
||||||
|
else if (isCur) cls = 'current-highlight';
|
||||||
|
else cls = 'processed';
|
||||||
|
|
||||||
|
h.push(`<tr class="${cls}">`);
|
||||||
|
h.push(`<td>${r.id}</td><td>${signHTML(r.signs)}</td><td>${r.k}</td>`);
|
||||||
|
h.push(`<td>${r.sum}</td><td>${n - r.k}</td><td>${-r.sum}</td>`);
|
||||||
|
if (info) {
|
||||||
|
h.push(`<td class="nearest-val">${info.p}</td>`);
|
||||||
|
h.push(`<td>${info.total}</td>`);
|
||||||
|
h.push(`<td class="${info.diff === state.ans ? 'best-diff' : ''}">${info.diff}</td>`);
|
||||||
|
} else {
|
||||||
|
h.push('<td>-</td><td>-</td><td>-</td>');
|
||||||
|
}
|
||||||
|
h.push('</tr>');
|
||||||
|
}
|
||||||
|
h.push('</tbody></table></div>');
|
||||||
|
$('rightPanel').innerHTML = h.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChart(info) {
|
||||||
|
const group = leftGroups[info.needK];
|
||||||
|
const sums = group.map(r => r.sum);
|
||||||
|
const target = info.target;
|
||||||
|
const allSums = leftRows.map(r => r.sum);
|
||||||
|
let min = Math.min(...allSums, target);
|
||||||
|
let max = Math.max(...allSums, target);
|
||||||
|
const span = max - min;
|
||||||
|
const margin = span === 0 ? 1 : span * 0.1;
|
||||||
|
min -= margin; max += margin;
|
||||||
|
const pos = v => ((v - min) / (max - min)) * 100;
|
||||||
|
|
||||||
|
let h = '<div class="chart-wrap"><div class="chart-scale">';
|
||||||
|
h += `<div class="target-line" style="left:${pos(target)}%"></div>`;
|
||||||
|
h += `<div class="target-tick" style="left:${pos(target)}%">target ${target}</div>`;
|
||||||
|
for (let i = 0; i < sums.length; i++) {
|
||||||
|
const s = sums[i];
|
||||||
|
const cls = s === info.p ? 'dot nearest' : 'dot';
|
||||||
|
const bottom = 6 + (i % 3) * 6;
|
||||||
|
h += `<div class="${cls}" style="left:${pos(s)}%; bottom:${bottom}px;" title="候选 ${s}">`;
|
||||||
|
h += `<span class="dot-label">${s}</span></div>`;
|
||||||
|
}
|
||||||
|
h += `<div class="scale-min-label">${min.toFixed(1)}</div>`;
|
||||||
|
h += `<div class="scale-max-label">${max.toFixed(1)}</div>`;
|
||||||
|
h += '</div><div class="chart-caption">蓝色:当前分组候选;紫色竖线:目标值;绿色:二分选中的最近点。</div></div>';
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPartition(info) {
|
||||||
|
const A = [], B = [];
|
||||||
|
for (let i = 0; i < n; i++) (info.nearestRow.signs[i] === 1 ? A : B).push(left[i]);
|
||||||
|
for (let i = 0; i < n; i++) (info.rightRow.signs[i] === 1 ? A : B).push(right[i]);
|
||||||
|
return { A, B };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResult(state) {
|
||||||
|
const h = [];
|
||||||
|
h.push('<h3>④ 二分查找与答案</h3>');
|
||||||
|
|
||||||
|
if (state.stage === 'done' && state.best) {
|
||||||
|
const { A, B } = getPartition(state.best);
|
||||||
|
const sumA = A.reduce((a, b) => a + b, 0);
|
||||||
|
const sumB = B.reduce((a, b) => a + b, 0);
|
||||||
|
h.push('<div class="final-answer">');
|
||||||
|
h.push('<h3>✅ 最终答案(一种最优分组)</h3>');
|
||||||
|
h.push(`<p>最小数组和之差 = <b>${state.ans}</b></p>`);
|
||||||
|
h.push(`<p>A(+号)= ${fmtArr(A)},sum(A) = ${sumA}</p>`);
|
||||||
|
h.push(`<p>B(−号)= ${fmtArr(B)},sum(B) = ${sumB}</p>`);
|
||||||
|
h.push(`<p>|sum(A) - sum(B)| = |${sumA} − (${sumB})| = ${state.ans}</p>`);
|
||||||
|
h.push('</div>');
|
||||||
|
h.push('<div class="complexity" style="margin-top:10px;font-size:13px;color:#64748b;">复杂度:每半枚举 2^n 种,排序 + 二分查找,总时间复杂度 O(n·2^n),空间复杂度 O(2^n)。</div>');
|
||||||
|
$('resultPanel').innerHTML = h.join('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.stage === 'formula') {
|
||||||
|
h.push('<div class="calc-block">先做问题转化:把分到第一个数组记为 +,分到第二个数组记为 −。</div>');
|
||||||
|
} else if (state.stage === 'left') {
|
||||||
|
h.push(`<div class="calc-block">正在枚举左半第 ${state.enumeratedLeftCount} / ${leftRows.length} 种符号组合。</div>`);
|
||||||
|
} else if (state.stage === 'sort') {
|
||||||
|
h.push('<div class="calc-block">左半枚举完毕。按 k 分组,组内按贡献和排序,方便之后二分查找。</div>');
|
||||||
|
} else if (state.currentRight) {
|
||||||
|
const info = state.currentRight;
|
||||||
|
const sums = leftGroups[info.needK].map(r => r.sum);
|
||||||
|
h.push('<div class="calc-block">');
|
||||||
|
h.push(`<div>当前右半:${signHTML(info.rightRow.signs)},q = ${info.q},正号个数 k = ${info.k}</div>`);
|
||||||
|
h.push(`<div>需要左半正号个数 = n − k = ${info.needK}</div>`);
|
||||||
|
h.push(`<div>左半候选 k=${info.needK} = [${sums.join(', ')}]</div>`);
|
||||||
|
h.push(`<div>目标 target = −q = ${info.target}</div>`);
|
||||||
|
h.push(`<div>二分查找最接近 target 的左半和:p = ${info.p}</div>`);
|
||||||
|
h.push(`<div>总符号和 = p + q = ${info.total}</div>`);
|
||||||
|
h.push(`<div>当前差值 = |${info.total}| = ${info.diff}</div>`);
|
||||||
|
h.push('</div>');
|
||||||
|
h.push(renderChart(info));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.ans < Infinity) {
|
||||||
|
h.push(`<div class="current-answer">当前已找到的最小差值:<b>${state.ans}</b></div>`);
|
||||||
|
}
|
||||||
|
$('resultPanel').innerHTML = h.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hintText(state) {
|
||||||
|
switch (state.stage) {
|
||||||
|
case 'formula': return '问题转化:看成给每个元素分配 + 或 − 符号。';
|
||||||
|
case 'left': return `正在枚举左半第 ${state.enumeratedLeftCount} / ${leftRows.length} 种符号组合。`;
|
||||||
|
case 'sort': return '左半枚举完成,按 k 分组并排序。';
|
||||||
|
case 'right': return `正在处理右半第 ${state.rightProcessed.length} / ${rightRows.length} 种符号组合。`;
|
||||||
|
case 'done': return '全部组合处理完成,得到最小数组和之差。';
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const state = computeState(currentStep);
|
||||||
|
const step = steps[currentStep];
|
||||||
|
$('stepInfo').textContent = `步骤 ${currentStep + 1} / ${steps.length}:${step.title}`;
|
||||||
|
$('hintText').textContent = hintText(state);
|
||||||
|
|
||||||
|
document.querySelectorAll('.pipe-step').forEach(el => {
|
||||||
|
el.classList.toggle('active', el.dataset.stage === state.stage);
|
||||||
|
});
|
||||||
|
|
||||||
|
renderNums(state);
|
||||||
|
renderLeftTable(state);
|
||||||
|
renderRightTable(state);
|
||||||
|
renderResult(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 控件 ----------
|
||||||
|
function stopAuto() {
|
||||||
|
if (timer) { clearInterval(timer); timer = null; $('autoBtn').textContent = '自动播放'; }
|
||||||
|
}
|
||||||
|
function stepBy(delta) {
|
||||||
|
stopAuto();
|
||||||
|
currentStep = Math.max(0, Math.min(steps.length - 1, currentStep + delta));
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function jumpToEnd() { stopAuto(); currentStep = steps.length - 1; render(); }
|
||||||
|
function reset() { stopAuto(); currentStep = 0; render(); }
|
||||||
|
|
||||||
|
function applyInput() {
|
||||||
|
stopAuto();
|
||||||
|
let raw = $('numsInput').value.trim();
|
||||||
|
const m = raw.match(/\[.*?]/);
|
||||||
|
if (m) raw = m[0];
|
||||||
|
raw = raw.replace(/^\[|\]$/g, '').replace(/,/g, ',');
|
||||||
|
const arr = raw.split(/[\s,]+/).filter(Boolean).map(Number);
|
||||||
|
if (arr.length < 2 || arr.length % 2 !== 0 || arr.some(x => !Number.isFinite(x))) {
|
||||||
|
alert('请输入偶数个整数,例如:3, 9, 7, 3');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (arr.length > 12) {
|
||||||
|
alert('为便于演示,元素个数不能超过 12。当前:' + arr.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nums = arr;
|
||||||
|
buildData();
|
||||||
|
currentStep = 0;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 初始化 ----------
|
||||||
|
try {
|
||||||
|
buildData();
|
||||||
|
render();
|
||||||
|
|
||||||
|
$('applyBtn').addEventListener('click', applyInput);
|
||||||
|
$('exampleSelect').addEventListener('change', function() {
|
||||||
|
stopAuto();
|
||||||
|
nums = JSON.parse(this.value);
|
||||||
|
$('numsInput').value = nums.join(', ');
|
||||||
|
buildData();
|
||||||
|
currentStep = 0;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
$('prevBtn').addEventListener('click', () => stepBy(-1));
|
||||||
|
$('nextBtn').addEventListener('click', () => stepBy(1));
|
||||||
|
$('jumpBtn').addEventListener('click', jumpToEnd);
|
||||||
|
$('autoBtn').addEventListener('click', function() {
|
||||||
|
if (timer) { stopAuto(); return; }
|
||||||
|
if (currentStep >= steps.length - 1) { currentStep = 0; render(); }
|
||||||
|
timer = setInterval(() => {
|
||||||
|
if (currentStep < steps.length - 1) { currentStep++; render(); }
|
||||||
|
else stopAuto();
|
||||||
|
}, 800);
|
||||||
|
this.textContent = '暂停';
|
||||||
|
});
|
||||||
|
$('resetBtn').addEventListener('click', reset);
|
||||||
|
} catch (e) {
|
||||||
|
$('debug').style.display = 'block';
|
||||||
|
$('debug').textContent = '初始化失败:' + (e.message || e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user