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

2996 lines
162 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
#!/usr/bin/env python3
"""
LeetCode Hot 100 图解 HTML 页面生成器
用法: python3 generate.py [--category 哈希] [--slugs two-sum,group-anagrams] [--all]
"""
import json, os, sys, textwrap
from linked_list_algos import (
js_intersection_of_two_linked_lists,
js_reverse_linked_list,
js_palindrome_linked_list,
js_linked_list_cycle,
js_linked_list_cycle_ii,
js_merge_two_sorted_lists,
js_add_two_numbers,
js_remove_nth_node_from_end_of_list,
js_swap_nodes_in_pairs,
js_reverse_nodes_in_k_group,
js_copy_list_with_random_pointer,
js_sort_list,
js_merge_k_sorted_lists,
js_lru_cache,
)
from binary_tree_algos import (
js_binary_tree_inorder_traversal,
js_maximum_depth_of_binary_tree,
js_invert_binary_tree,
js_symmetric_tree,
js_diameter_of_binary_tree,
js_binary_tree_level_order_traversal,
js_convert_sorted_array_to_bst,
js_validate_binary_search_tree,
js_kth_smallest_element_in_a_bst,
js_binary_tree_right_side_view,
js_flatten_binary_tree_to_linked_list,
js_construct_binary_tree_from_preorder_and_inorder,
js_path_sum_iii,
js_lowest_common_ancestor_of_a_binary_tree,
js_binary_tree_maximum_path_sum,
)
BASE = os.path.dirname(os.path.abspath(__file__))
PROBLEMS_FILE = os.path.join(BASE, 'shared', 'problems.json')
from dp_algos import (
js_climbing_stairs, js_pascals_triangle, js_house_robber,
js_perfect_squares, js_coin_change, js_word_break,
js_longest_increasing_subsequence, js_maximum_product_subarray,
js_partition_equal_subset_sum, js_longest_valid_parentheses,
)
with open(PROBLEMS_FILE, encoding='utf-8') as f:
ALL_PROBLEMS = json.load(f)
# ========== Difficulty class ==========
DIFF_CLASS = {'简单': 'easy', '中等': 'medium', '困难': 'hard'}
DIFF_ICON = {'简单': '🟢', '中等': '🟡', '困难': '🔴'}
# ========== Template ==========
def gen_html(p, algo_js):
"""Generate complete HTML for a problem."""
d = p['difficulty']
slug = p['slug']
title = p['title']
cat = p['category']
did = str(p['id']).zfill(3)
diff_cls = DIFF_CLASS.get(d, 'medium')
diff_icon = DIFF_ICON.get(d, '🟡')
return f'''<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{did}. {title} – 图解</title>
<link rel="stylesheet" href="../shared/style.css">
<style>
/* page-specific overrides */
.vis-area {{ min-height: 120px; padding: 16px 0; }}
.code-section {{ margin-top: 16px; }}
</style>
</head>
<body>
<div class="container">
<h1>{diff_icon} {did}. {title} <span class="badge {diff_cls}">{d}</span></h1>
<p class="subtitle">分类:{cat} | 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() {{
// ========== Algorithm Logic ==========
{algo_js}
}})();
</script>
</body>
</html>'''
# ========== Algorithm JS Generators ==========
# Each function returns a JS string for the specific algorithm
def js_two_sum():
return r'''
const examples = [
{input: [2,7,11,15], target: 9, label: '示例1: nums=[2,7,11,15], target=9'},
{input: [3,2,4], target: 6, label: '示例2: nums=[3,2,4], target=6'},
{input: [3,3], target: 6, label: '示例3: nums=[3,3], target=6'},
];
let nums, target, steps, stepCtrl;
function buildSteps(arr, tgt) {
nums = arr; target = tgt;
steps = [];
const map = {};
steps.push({stage:'start', msg:'开始遍历数组,用哈希表记录已访问的元素', highlights:{}, mapKeys:[], idx:-1});
for (let i = 0; i < arr.length; i++) {
const complement = tgt - arr[i];
steps.push({stage:'check', msg:`检查 nums[${i}]=${arr[i]},需要的补数 complement = ${tgt} - ${arr[i]} = ${complement}`, highlights:{[i]:'orange'}, mapKeys:Object.entries(map), idx:i, complement});
if (complement in map) {
steps.push({stage:'found', msg:`找到!补数 ${complement} 在哈希表中,对应索引 ${map[complement]}`, highlights:{[i]:'green', [map[complement]]:'green'}, mapKeys:Object.entries(map), idx:i, result:[map[complement], i]});
return;
}
map[arr[i]] = i;
steps.push({stage:'store', msg:`补数 ${complement} 不在哈希表中,将 nums[${i}]=${arr[i]} → 索引 ${i} 存入哈希表`, highlights:{[i]:'blue'}, mapKeys:Object.entries(map), idx:i});
}
steps.push({stage:'done', msg:'遍历完毕,未找到满足条件的两个数', highlights:{}, mapKeys:Object.entries(map), idx:-1});
}
function render(step) {
if (!step) return;
const s = steps[step];
let viz = renderArray(nums, {highlights: s.highlights});
if (s.complement !== undefined) {
viz += '<div style="margin-top:8px;color:#64748b;">complement = ' + s.complement + '</div>';
}
$('vizArea').innerHTML = viz;
let detail = '<div class="calc-block">' + s.msg + '</div>';
if (s.mapKeys && s.mapKeys.length > 0) {
detail += '<div style="margin-top:8px;"><b>哈希表:</b>';
s.mapKeys.forEach(([k,v]) => { detail += `<code>${k}→${v}</code> `; });
detail += '</div>';
}
$('detailContent').innerHTML = detail;
if (s.stage === 'found') {
$('resultContent').innerHTML = `<div class="final-answer">返回 <b>[${s.result}]</b><br>nums[${s.result[0]}] + nums[${s.result[1]}] = ${nums[s.result[0]]} + ${nums[s.result[1]]} = ${target}</div>`;
} else if (s.stage === 'done') {
$('resultContent').innerHTML = '<div class="final-answer" style="border-color:#f87171;background:#fef2f2;">未找到答案</div>';
}
$('hintText').textContent = s.msg;
const stages = ['start→开始','check→检查','store→存入','found→找到'];
$('pipeline').innerHTML = stages.map(st => {
const [key, label] = st.split('→');
return `<span class="pipe-step ${s.stage===key?'active':''}">${label}</span>`;
}).join('<i>→</i>');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `<option value="${i}">${e.label}</option>`; });
$('inputArea').value = 'nums=[2,7,11,15], target=9';
buildSteps(examples[0].input, examples[0].target);
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 = () => {
const m = $('inputArea').value.match(/nums=\[([^\]]+)\].*target=(\d+)/);
if (!m) { alert('格式: nums=[2,7,11,15], target=9'); return; }
const arr = m[1].split(',').map(Number);
const tgt = parseInt(m[2]);
buildSteps(arr, tgt);
stepCtrl.setSteps(steps.map((_,i) => i));
render(0);
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
$('inputArea').value = `nums=[${e.input}], target=${e.target}`;
buildSteps(e.input, e.target);
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 twoSum(nums, target):
hashmap = {}
for i, num in enumerate(nums):
complement = target - num
if complement in hashmap:
return [hashmap[complement], i]
hashmap[num] = i
return []`, {lang:'Python'});
'''
def js_group_anagrams():
return r'''
const examples = [
{input: ["eat","tea","tan","ate","nat","bat"], label: '示例1'},
{input: [""], label: '示例2: [""]'},
{input: ["a"], label: '示例3: ["a"]'},
];
let strs, steps, stepCtrl;
function sortedKey(s) { return s.split('').sort().join(''); }
function buildSteps(arr) {
strs = arr; steps = [];
const groups = {};
steps.push({stage:'start', msg:'遍历每个字符串,将其按字母排序后的结果作为 key 分组', current:'', key:'', groups:{}});
for (let i = 0; i < arr.length; i++) {
const s = arr[i];
const key = sortedKey(s);
steps.push({stage:'sort', msg:`处理 "${s}":排序后 key = "${key}"`, current:s, key, groups:JSON.parse(JSON.stringify(groups)), idx:i});
if (!groups[key]) groups[key] = [];
groups[key].push(s);
steps.push({stage:'group', msg:`将 "${s}" 加入分组 ["${key}"]`, current:s, key, groups:JSON.parse(JSON.stringify(groups)), idx:i});
}
steps.push({stage:'done', msg:'分组完成', current:'', key:'', groups:JSON.parse(JSON.stringify(groups))});
}
function render(step) {
const s = steps[step];
let viz = '<div class="nums-line">';
strs.forEach((st,i) => {
const cls = s.idx===i ? (s.stage==='group'?'green':'orange') : 'default';
viz += `<span class="chip ${cls}" style="min-width:auto;padding:4px 12px;">"${st}"</span>`;
});
viz += '</div>';
if (s.current) viz += `<div style="margin-top:8px;">当前: <code>"${s.current}"</code> → key: <code>"${s.key}"</code></div>`;
$('vizArea').innerHTML = viz;
let detail = '<div class="calc-block">' + s.msg + '</div>';
detail += '<div style="margin-top:8px;"><b>分组结果:</b></div>';
Object.entries(s.groups).forEach(([k,v]) => {
detail += `<div style="margin:4px 0;"><code>${k}</code> → [${v.map(x=>`"${x}"`).join(', ')}]</div>`;
});
$('detailContent').innerHTML = detail;
if (s.stage === 'done') {
const result = Object.values(s.groups);
$('resultContent').innerHTML = `<div class="final-answer">返回 <b>${JSON.stringify(result)}</b><br>共 ${result.length} 个分组</div>`;
}
$('hintText').textContent = s.msg;
const stages = ['start→开始','sort→排序','group→分组','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 = '["eat","tea","tan","ate","nat","bat"]';
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 arr = JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); }
catch(e) { alert('请输入合法 JSON 数组,如 ["eat","tea"]'); }
};
$('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 groupAnagrams(strs):
groups = {}
for s in strs:
key = ''.join(sorted(s))
if key not in groups:
groups[key] = []
groups[key].append(s)
return list(groups.values())`, {lang:'Python'});
'''
def js_longest_consecutive():
return r'''
const examples = [
{input: [100,4,200,1,3,2], label: '示例1: [100,4,200,1,3,2]'},
{input: [0,3,7,2,5,8,4,6,0,1], label: '示例2: [0,3,7,2,5,8,4,6,0,1]'},
];
let nums, steps, stepCtrl;
function buildSteps(arr) {
nums = arr; steps = [];
const numSet = new Set(arr);
let best = 0;
steps.push({stage:'start', msg:'将所有数放入集合,寻找每个连续序列的起点', sorted:[...new Set(arr)].sort((a,b)=>a-b), current:-1, streakNums:[], best:0});
const sortedUnique = [...new Set(arr)].sort((a,b)=>a-b);
for (const num of sortedUnique) {
if (numSet.has(num - 1)) continue;
let streak = 1, current = num;
const streakNums = [num];
steps.push({stage:'start_seq', msg:`${num} 是连续序列的起点(${num-1} 不在集合中)`, sorted:sortedUnique, current:num, streakNums, streak, best});
while (numSet.has(current + 1)) {
current++; streak++; streakNums.push(current);
steps.push({stage:'extend', msg:`序列延伸:${current} 在集合中`, sorted:sortedUnique, current, streakNums, streak, best});
}
best = Math.max(best, streak);
steps.push({stage:'end_seq', msg:`序列结束,长度=${streak},最长=${best}`, sorted:sortedUnique, current, streakNums, streak, best});
}
steps.push({stage:'done', msg:`最长连续序列长度为 ${best}`, sorted:sortedUnique, current:-1, streakNums:[], best});
}
function render(step) {
const s = steps[step];
const hl = {};
if (s.streakNums) s.streakNums.forEach(n => { hl[n] = s.stage==='end_seq'?'green':'purple'; });
if (s.current >= 0) hl[s.current] = 'orange';
let viz = '<div style="margin-bottom:8px;"><b>排序去重后:</b></div>';
viz += renderArray(s.sorted, {highlights: hl});
if (s.streakNums && s.streakNums.length > 0)
viz += '<div style="margin-top:8px;color:#8b5cf6;">连续序列: [' + s.streakNums.join(', ') + '] 长度=' + s.streak + '</div>';
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.best > 0) $('detailContent').innerHTML += `<div class="current-answer">当前最长:<b>${s.best}</b></div>`;
if (s.stage === 'done') $('resultContent').innerHTML = `<div class="final-answer">最长连续序列长度 = <b>${s.best}</b></div>`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','start_seq→起点','extend→延伸','end_seq→结束','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 = '[100,4,200,1,3,2]';
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 arr = JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); }
catch(e) { alert('请输入合法 JSON 数组'); }
};
$('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 longestConsecutive(nums):
num_set = set(nums)
longest = 0
for num in num_set:
if num - 1 not in num_set:
streak = 1
while num + streak in num_set:
streak += 1
longest = max(longest, streak)
return longest`, {lang:'Python'});
'''
def js_move_zeros():
return r'''
const examples = [
{input: [0,1,0,3,12], label: '示例1: [0,1,0,3,12]'},
{input: [0], label: '示例2: [0]'},
{input: [1,0,2,0,3], label: '示例3: [1,0,2,0,3]'},
];
let nums, steps, stepCtrl;
function buildSteps(arr) {
nums = [...arr]; steps = [];
let slow = 0;
steps.push({stage:'start', msg:'快慢指针:slow 指向第一个可放非零元素的位置', arr:[...nums], slow:0, fast:0});
for (let fast = 0; fast < nums.length; fast++) {
steps.push({stage:'check', msg:`fast=${fast},nums[${fast}]=${nums[fast]} ${nums[fast]===0?'是零,跳过':'不是零,交换到前面'}`, arr:[...nums], slow, fast});
if (nums[fast] !== 0) {
if (slow !== fast) {
[nums[slow], nums[fast]] = [nums[fast], nums[slow]];
steps.push({stage:'swap', msg:`交换 nums[${slow}] 和 nums[${fast}] → [${nums}]`, arr:[...nums], slow, fast});
}
slow++;
steps.push({stage:'advance', msg:`slow 前进到 ${slow}`, arr:[...nums], slow, fast});
}
}
steps.push({stage:'done', msg:'完成!', arr:[...nums], slow, fast:nums.length});
}
function render(step) {
const s = steps[step];
const hl = {};
if (s.fast < s.arr.length) hl[s.fast] = 'orange';
if (s.slow < s.arr.length) hl[s.slow] = 'blue';
for (let i = 0; i < s.slow; i++) { if (s.arr[i] !== 0) hl[i] = 'green'; }
let viz = renderArray(s.arr, {highlights: hl, pointers: {slow: s.slow, fast: s.fast}});
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') $('resultContent').innerHTML = `<div class="final-answer">结果:<b>[${s.arr}]</b></div>`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','check→检查','swap→交换','advance→前进','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 = '[0,1,0,3,12]';
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 arr = JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); }
catch(e) { alert('请输入合法 JSON 数组'); }
};
$('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 moveZeroes(nums):
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1`, {lang:'Python'});
'''
def js_container_with_most_water():
return r'''
const examples = [
{input: [1,8,6,2,5,4,8,3,7], label: '示例1: [1,8,6,2,5,4,8,3,7]'},
{input: [1,1], label: '示例2: [1,1]'},
];
let height, steps, stepCtrl;
function buildSteps(arr) {
height = [...arr]; steps = [];
let left = 0, right = arr.length - 1, maxArea = 0;
steps.push({stage:'init', msg:'双指针从两端开始', left, right, maxArea, area:0});
while (left < right) {
const h = Math.min(arr[left], arr[right]);
const w = right - left;
const area = h * w;
maxArea = Math.max(maxArea, area);
steps.push({stage:'calc', msg:`left=${left}(h=${arr[left]}) right=${right}(h=${arr[right]}) → 高=${h} 宽=${w} 面积=${area}`, left, right, maxArea, area});
if (arr[left] < arr[right]) { left++; steps.push({stage:'move', msg:`左边矮,左指针→${left}`, left, right, maxArea, area}); }
else { right--; steps.push({stage:'move', msg:`右边矮,右指针→${right}`, left, right, maxArea, area}); }
}
steps.push({stage:'done', msg:`指针相遇,最大面积=${maxArea}`, left, right, maxArea, area:0});
}
function render(step) {
const s = steps[step];
const maxH = Math.max(...height);
let viz = '<div style="display:flex;align-items:flex-end;gap:4px;height:120px;margin-top:12px;padding:8px 0;">';
height.forEach((h,i) => {
const pct = (h / maxH * 100); const bg = i===s.left||i===s.right ? 'var(--blue)' : '#cbd5e1';
viz += `<div style="width:36px;height:${pct}%;background:${bg};border-radius:4px 4px 0 0;display:flex;align-items:flex-start;justify-content:center;font-size:11px;font-weight:700;color:${i===s.left||i===s.right?'white':'#475569'};padding-top:4px;">${h}</div>`;
});
viz += '</div>';
viz += `<div style="margin-top:10px;color:var(--green);">💧 最大面积 = ${s.maxArea}</div>`;
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') $('resultContent').innerHTML = `<div class="final-answer">最大盛水量 = <b>${s.maxArea}</b></div>`;
$('hintText').textContent = s.msg;
const stages = ['init→初始化','calc→计算','move→移动','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 = '[1,8,6,2,5,4,8,3,7]';
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 arr = JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); }
catch(e) { alert('请输入合法 JSON 数组'); }
};
$('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 maxArea(height):
left, right = 0, len(height) - 1
max_area = 0
while left < right:
h = min(height[left], height[right])
max_area = max(max_area, h * (right - left))
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_area`, {lang:'Python'});
'''
def js_3sum():
return r'''
const examples = [
{input: [-1,0,1,2,-1,-4], label: '示例1: [-1,0,1,2,-1,-4]'},
{input: [0,1,1], label: '示例2: [0,1,1]'},
{input: [0,0,0], label: '示例3: [0,0,0]'},
];
let nums, steps, stepCtrl;
function buildSteps(arr) {
nums = [...arr].sort((a,b) => a - b); steps = [];
const result = [];
steps.push({stage:'sort', msg:`排序: [${arr}] → [${nums}]`, arr:[...nums], i:-1, l:-1, r:-1, triplets:[], currentSum:null});
for (let i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] === nums[i-1]) continue;
let l = i + 1, r = nums.length - 1;
steps.push({stage:'fix', msg:`固定 nums[${i}]=${nums[i]},双指针 [${l}..${r}]`, arr:[...nums], i, l, r, triplets:JSON.parse(JSON.stringify(result)), currentSum:null});
while (l < r) {
const sum = nums[i] + nums[l] + nums[r];
steps.push({stage:'calc', msg:`${nums[i]}+${nums[l]}+${nums[r]}=${sum}` + (sum===0?' ✓':sum<0?' → L右移':' → R左移'), arr:[...nums], i, l, r, triplets:JSON.parse(JSON.stringify(result)), currentSum:sum});
if (sum === 0) {
result.push([nums[i], nums[l], nums[r]]);
steps.push({stage:'found', msg:`找到 [${nums[i]},${nums[l]},${nums[r]}]`, arr:[...nums], i, l, r, triplets:JSON.parse(JSON.stringify(result)), currentSum:0});
while (l < r && nums[l] === nums[l+1]) l++;
while (l < r && nums[r] === nums[r-1]) r--;
l++; r--;
} else if (sum < 0) { l++; } else { r--; }
}
}
steps.push({stage:'done', msg:`共找到 ${result.length} 个三元组`, arr:[...nums], i:-1, l:-1, r:-1, triplets:JSON.parse(JSON.stringify(result)), currentSum:null});
}
function render(step) {
const s = steps[step];
const hl = {};
if (s.i>=0) hl[s.i] = s.stage==='found'?'green':'purple';
if (s.l>=0) hl[s.l] = s.stage==='found'?'green':'orange';
if (s.r>=0) hl[s.r] = s.stage==='found'?'green':'blue';
const pointers = {};
if (s.i>=0) pointers['i']=s.i; if (s.l>=0) pointers['L']=s.l; if (s.r>=0) pointers['R']=s.r;
let viz = renderArray(s.arr, {highlights:hl, pointers});
if (s.currentSum !== null) viz += `<div style="margin-top:8px;">和 = <b style="color:${s.currentSum===0?'var(--green)':s.currentSum<0?'var(--blue)':'var(--red)'};">${s.currentSum}</b></div>`;
$('vizArea').innerHTML = viz;
let detail = '<div class="calc-block">' + s.msg + '</div>';
if (s.triplets.length > 0) { s.triplets.forEach((t,i)=>{ detail += `<div><code>${i+1}. [${t.join(', ')}]</code></div>`; }); }
$('detailContent').innerHTML = detail;
if (s.stage === 'done') {
if (s.triplets.length > 0) $('resultContent').innerHTML = `<div class="final-answer">返回 <b>${JSON.stringify(s.triplets)}</b></div>`;
else $('resultContent').innerHTML = '<div class="final-answer" style="border-color:#f87171;background:#fef2f2;">未找到</div>';
}
$('hintText').textContent = s.msg;
const stages = ['sort→排序','fix→固定','calc→计算','found→找到','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 = '[-1,0,1,2,-1,-4]';
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 arr = JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length; }
catch(e) { alert('请输入合法 JSON 数组'); }
};
$('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 threeSum(nums):
nums.sort()
res = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i-1]:
continue
l, r = i + 1, len(nums) - 1
while l < r:
s = nums[i] + nums[l] + nums[r]
if s == 0:
res.append([nums[i], nums[l], nums[r]])
while l < r and nums[l] == nums[l+1]: l += 1
while l < r and nums[r] == nums[r-1]: r -= 1
l += 1; r -= 1
elif s < 0: l += 1
else: r -= 1
return res`, {lang:'Python'});
'''
def js_trapping_rain_water():
return r'''
const examples = [
{input: [0,1,0,2,1,0,1,3,2,1,2,1], label: '示例1: [0,1,0,2,1,0,1,3,2,1,2,1]'},
{input: [4,2,0,3,2,5], label: '示例2: [4,2,0,3,2,5]'},
];
let height, steps, stepCtrl;
function buildSteps(arr) {
height = [...arr]; steps = [];
let left = 0, right = arr.length - 1;
let leftMax = arr[left], rightMax = arr[right];
let totalWater = 0;
const waterAt = new Array(arr.length).fill(0);
steps.push({stage:'init', msg:`双指针从两端开始,left_max=${leftMax},right_max=${rightMax}`, left, right, leftMax, rightMax, totalWater, waterAt:[...waterAt]});
while (left < right) {
if (leftMax <= rightMax) {
const water = Math.max(0, leftMax - arr[left]);
waterAt[left] = water; totalWater += water;
steps.push({stage:'calcL', msg:`左端: min(${leftMax},${rightMax})=${leftMax}, h[${left}]=${arr[left]}, 储水=${water}`, left, right, leftMax, rightMax, totalWater, waterAt:[...waterAt]});
left++; leftMax = Math.max(leftMax, arr[left]);
} else {
const water = Math.max(0, rightMax - arr[right]);
waterAt[right] = water; totalWater += water;
steps.push({stage:'calcR', msg:`右端: min(${leftMax},${rightMax})=${rightMax}, h[${right}]=${arr[right]}, 储水=${water}`, left, right, leftMax, rightMax, totalWater, waterAt:[...waterAt]});
right--; rightMax = Math.max(rightMax, arr[right]);
}
}
steps.push({stage:'done', msg:`总储水量=${totalWater}`, left, right, leftMax, rightMax, totalWater, waterAt:[...waterAt]});
}
function render(step) {
const s = steps[step];
const maxH = Math.max(...height);
const chartH = 160; const unitH = chartH / (maxH||1); const barW = 36;
let viz = '<div style="position:relative;display:flex;align-items:flex-end;gap:2px;height:' + (chartH+30) + 'px;padding:0 4px;margin-top:8px;">';
height.forEach((h, i) => {
const barH = h * unitH; const waterH = s.waterAt[i] * unitH;
const isL = i===s.left, isR = i===s.right;
const barBg = isL ? 'var(--blue)' : isR ? 'var(--purple)' : '#94a3b8';
viz += `<div style="position:relative;width:${barW}px;display:flex;flex-direction:column;align-items:stretch;">`;
if (waterH > 0) viz += `<div style="height:${waterH}px;background:rgba(59,130,246,0.25);border-radius:2px 2px 0 0;border-top:2px solid rgba(59,130,246,0.5);"></div>`;
viz += `<div style="height:${barH}px;background:${barBg};border-radius:3px 3px 0 0;display:flex;align-items:flex-start;justify-content:center;font-size:11px;font-weight:700;color:${isL||isR?'white':'#334155'};padding-top:2px;min-height:2px;">${h>0?h:''}</div>`;
viz += `<div style="text-align:center;font-size:10px;color:var(--text-muted);margin-top:2px;">${i}</div></div>`;
});
viz += '</div>';
viz += `<div style="margin-top:10px;display:flex;gap:16px;"><span style="color:var(--blue);">■ left_max=${s.leftMax}</span><span style="color:var(--purple);">■ right_max=${s.rightMax}</span><span style="color:var(--green);">💧 储水=${s.totalWater}</span></div>`;
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') $('resultContent').innerHTML = `<div class="final-answer">总储水量 = <b>${s.totalWater}</b></div>`;
$('hintText').textContent = s.msg;
const stages = ['init→初始化','calcL→左端','calcR→右端','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 = '[0,1,0,2,1,0,1,3,2,1,2,1]';
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 arr = JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length; }
catch(e) { alert('请输入合法 JSON 数组'); }
};
$('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 trap(height):
left, right = 0, len(height) - 1
left_max, right_max = height[left], height[right]
water = 0
while left < right:
if left_max <= right_max:
water += left_max - height[left]
left += 1
left_max = max(left_max, height[left])
else:
water += right_max - height[right]
right -= 1
right_max = max(right_max, height[right])
return water`, {lang:'Python'});
'''
# ==================== 图论 4 题 ====================
def js_number_of_islands():
return r'''
const examples = [
{grid: [["1","1","0","0","0"],["1","1","0","0","0"],["0","0","1","0","0"],["0","0","0","1","1"]], label: '示例1: 4×5 3个岛'},
{grid: [["1","1","1"],["0","1","0"],["1","1","1"]], label: '示例2: 3×3 1个岛'},
];
let grid, R, C, steps, stepCtrl, islandColors, finalMap;
function buildSteps(g) {
grid = g.map(r=>[...r]); R = grid.length; C = grid[0].length;
steps = []; islandColors = {}; finalMap = {};
const visited = Array.from({length:R},()=>Array(C).fill(false));
let islands = 0;
const islandColorList = ['blue','green','purple','orange','cyan','red'];
steps.push({stage:'start', msg:'遍历网格,遇到未访问的陆地进行DFS染色', hl:{}, im:{}, islands:0});
for (let r=0; r<R; r++) {
for (let c=0; c<C; c++) {
if (grid[r][c]==='1' && !visited[r][c]) {
islands++;
const color = islandColorList[(islands-1) % islandColorList.length];
islandColors[islands] = color;
steps.push({stage:'new_island', msg:`发现岛屿 #${islands},从 (${r},${c}) 开始DFS`, hl:{[r+','+c]:'current'}, im:JSON.parse(JSON.stringify(finalMap)), islands});
const stack = [[r,c]];
visited[r][c] = true;
finalMap[r+','+c] = islands;
while (stack.length > 0) {
const [cr, cc] = stack.pop();
const dirs = [[0,1],[0,-1],[1,0],[-1,0]];
for (const [dr,dc] of dirs) {
const nr=cr+dr, nc=cc+dc;
if (nr>=0 && nr<R && nc>=0 && nc<C && grid[nr][nc]==='1' && !visited[nr][nc]) {
visited[nr][nc] = true;
stack.push([nr,nc]);
finalMap[nr+','+nc] = islands;
const hl2 = {}; hl2[nr+','+nc] = 'current';
steps.push({stage:'dfs', msg:`DFS: (${cr},${cc}) → (${nr},${nc}) 染色为岛 #${islands}`, hl:hl2, im:JSON.parse(JSON.stringify(finalMap)), islands});
}
}
}
steps.push({stage:'island_done', msg:`岛屿 #${islands} DFS完成`, hl:{}, im:JSON.parse(JSON.stringify(finalMap)), islands});
}
}
}
steps.push({stage:'done', msg:`遍历完毕,共 ${islands} 个岛屿`, hl:{}, im:JSON.parse(JSON.stringify(finalMap)), islands});
}
function render(step) {
const s = steps[step];
const cellClass = (val,r,c) => {
if (val==='0') return 'water';
const key = r+','+c;
const iid = s.im[key];
if (iid) return islandColors[iid] || 'island';
return 'default';
};
const cellStyle = (val,r,c) => {
if (val==='0') return 'background:#e0f2fe;color:#0369a1;';
const key = r+','+c;
const iid = s.im[key];
const colorMap = {blue:'background:#dbeafe;color:#1e40af;',green:'background:#dcfce7;color:#166534;',purple:'background:#ede9fe;color:#5b21b6;',orange:'background:#fff7ed;color:#9a3412;',cyan:'background:#cffafe;color:#155e75;',red:'background:#fee2e2;color:#991b1b;'};
if (iid && colorMap[islandColors[iid]]) return colorMap[islandColors[iid]];
return 'background:#f1f5f9;color:#475569;';
};
let viz = renderGrid(grid, {highlights:s.hl, cellSize:48, cellClass, cellStyle});
viz += `<div style="margin-top:8px;">🏝️ 岛屿数量:${s.islands}</div>`;
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage==='done') {
let det = `<div class="final-answer">岛屿数量 = <b>${s.islands}</b></div>`;
det += '<div style="margin-top:8px;">';
for (let i=1; i<=s.islands; i++) det += `<span style="margin-right:12px;"><span style="display:inline-block;width:14px;height:14px;border-radius:3px;${islandColors[i]==='blue'?'background:#dbeafe':islandColors[i]==='green'?'background:#dcfce7':islandColors[i]==='purple'?'background:#ede9fe':'background:#fff7ed'};vertical-align:middle;"></span> 岛屿 #${i}</span>`;
det += '</div>';
$('resultContent').innerHTML = det;
}
$('hintText').textContent = s.msg;
const stages = ['start→开始','new_island→发现岛屿','dfs→DFS染色','island_done→岛屿完成','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 = '[["1","1","0"],["0","1","0"],["0","0","1"]]';
buildSteps(examples[0].grid);
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 g = JSON.parse($('inputArea').value); buildSteps(g); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length; }
catch(e) { alert('请输入合法 JSON 二维数组'); }
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
buildSteps(e.grid); 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 numIslands(grid):
if not grid: return 0
rows, cols = len(grid), len(grid[0])
def dfs(r, c):
if r<0 or r>=rows or c<0 or c>=cols or grid[r][c]!='1':
return
grid[r][c] = '2'
dfs(r+1,c); dfs(r-1,c)
dfs(r,c+1); dfs(r,c-1)
islands = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
islands += 1
dfs(r, c)
return islands`, {lang:'Python'});
'''
def js_rotting_oranges():
return r'''
const examples = [
{grid: [[2,1,1],[1,1,0],[0,1,1]], label: '示例1: 3×3'},
{grid: [[2,1,1],[0,1,1],[1,0,1]], label: '示例2: 有不可达'},
{grid: [[0,2]], label: '示例3: 无橘子'},
];
let grid, R, C, steps, stepCtrl;
function buildSteps(g) {
grid = g.map(r=>[...r]); R = grid.length; C = grid[0].length;
steps = [];
const queue = [];
let fresh = 0;
for (let r=0; r<R; r++) for (let c=0; c<C; c++) {
if (grid[r][c]===2) queue.push([r,c]);
else if (grid[r][c]===1) fresh++;
}
steps.push({stage:'init', msg:`BFS多源最短路径:${queue.length}个腐烂源,${fresh}个新鲜橘子`, grid:grid.map(r=>[...r]), minutes:0, fresh});
if (fresh === 0) { steps.push({stage:'done', msg:'没有新鲜橘子,返回0', grid:grid.map(r=>[...r]), minutes:0, fresh:0}); return; }
let minutes = 0;
const dirs = [[0,1],[0,-1],[1,0],[-1,0]];
while (queue.length > 0 && fresh > 0) {
const size = queue.length;
const newlyRotten = [];
for (let i=0; i<size; i++) {
const [r,c] = queue.shift();
for (const [dr,dc] of dirs) {
const nr=r+dr, nc=c+dc;
if (nr>=0 && nr<R && nc>=0 && nc<C && grid[nr][nc]===1) {
grid[nr][nc] = 2;
fresh--;
queue.push([nr,nc]);
newlyRotten.push([nr,nc]);
}
}
}
if (newlyRotten.length > 0) {
minutes++;
steps.push({stage:'rot', msg:`第 ${minutes} 分钟:${newlyRotten.length}个橘子腐烂 (剩余${fresh}个新鲜)`, grid:grid.map(r=>[...r]), minutes, fresh, newlyRotten});
}
}
if (fresh > 0) steps.push({stage:'impossible', msg:`仍有${fresh}个新鲜橘子无法腐烂`, grid:grid.map(r=>[...r]), minutes, fresh});
else steps.push({stage:'done', msg:`所有橘子腐烂,用时 ${minutes} 分钟`, grid:grid.map(r=>[...r]), minutes, fresh:0});
}
function render(step) {
const s = steps[step];
const hl = {};
if (s.newlyRotten) s.newlyRotten.forEach(([r,c]) => { hl[r+','+c] = 'current'; });
const cellStyle = (val,r,c) => {
const key = r+','+c;
if (hl[key]) return 'background:#fbbf24;color:#78350f;box-shadow:0 0 0 3px rgba(245,158,11,0.4);';
if (val===2) return 'background:#fed7aa;color:#9a3412;';
if (val===1) return 'background:#dcfce7;color:#166534;';
return 'background:#f1f5f9;color:#94a3b8;';
};
let viz = renderGrid(s.grid, {highlights:hl, cellSize:52, cellStyle});
viz += '<div style="margin-top:8px;display:flex;gap:16px;">';
viz += '<span style="color:#9a3412;">🟠 腐烂</span>';
viz += '<span style="color:#166534;">🟢 新鲜</span>';
viz += '<span style="color:#94a3b8;">⬜ 空</span>';
viz += `<span style="color:var(--blue);">⏱ 分钟=${s.minutes}</span>`;
viz += '</div>';
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage==='done') $('resultContent').innerHTML = `<div class="final-answer">经过 <b>${s.minutes}</b> 分钟,所有橘子腐烂</div>`;
if (s.stage==='impossible') $('resultContent').innerHTML = `<div class="final-answer" style="border-color:#f87171;background:#fef2f2;">返回 <b>-1</b>(有新鲜橘子无法腐烂)</div>`;
$('hintText').textContent = s.msg;
const stages = ['init→初始化','rot→腐烂传播','done→完成','impossible→不可能'];
$('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 = '[[2,1,1],[1,1,0],[0,1,1]]';
buildSteps(examples[0].grid);
stepCtrl = new StepController({onStep: render, autoInterval:800});
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 g = JSON.parse($('inputArea').value); buildSteps(g); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length; }
catch(e) { alert('请输入合法 JSON 二维数组'); }
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
buildSteps(e.grid); 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 orangesRotting(grid):
rows, cols = len(grid), len(grid[0])
queue = deque()
fresh = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
queue.append((r, c))
elif grid[r][c] == 1:
fresh += 1
minutes = 0
while queue and fresh > 0:
for _ in range(len(queue)):
r, c = queue.popleft()
for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
nr, nc = r+dr, c+dc
if 0<=nr<rows and 0<=nc<cols and grid[nr][nc]==1:
grid[nr][nc] = 2
fresh -= 1
queue.append((nr, nc))
minutes += 1
return minutes if fresh == 0 else -1`, {lang:'Python'});
'''
def js_course_schedule():
return r'''
const examples = [
{numCourses: 4, prerequisites: [[1,0],[2,0],[3,1],[3,2]], label: '示例1: 4门课, [1,0],[2,0],[3,1],[3,2]'},
{numCourses: 2, prerequisites: [[1,0],[0,1]], label: '示例2: 2门课, 有环'},
{numCourses: 3, prerequisites: [[1,0],[2,1]], label: '示例3: 3门课, 无环'},
];
let N, edges, steps, stepCtrl, adj, stateMap;
function buildSteps(n, prereq) {
N = n; edges = prereq; steps = [];
adj = Array.from({length:n}, ()=>[]);
for (const [a,b] of prereq) adj[a].push(b);
stateMap = new Array(n).fill(0); // 0=未访问, 1=进行中, 2=完成
steps.push({stage:'init', msg:`DFS环检测: ${n}门课程, ${prereq.length}条依赖`, node:-1, state:[...stateMap], topo:[], hasCycle:false});
let hasCycle = false;
const topoOrder = [];
function dfs(node) {
stateMap[node] = 1;
steps.push({stage:'visiting', msg:`访问课程 ${node},标记为"进行中"`, node, state:[...stateMap], topo:[...topoOrder], hasCycle:false});
for (const nb of adj[node]) {
if (stateMap[nb] === 1) {
hasCycle = true;
steps.push({stage:'cycle', msg:`课程 ${nb} 正在访问中!检测到环 ${node}→${nb}`, node:nb, state:[...stateMap], topo:[...topoOrder], hasCycle:true});
return true;
}
if (stateMap[nb] === 0) {
if (dfs(nb)) return true;
}
}
stateMap[node] = 2;
topoOrder.push(node);
steps.push({stage:'done', msg:`课程 ${node} 完成,加入拓扑序列`, node, state:[...stateMap], topo:[...topoOrder], hasCycle:false});
return false;
}
for (let i=0; i<n; i++) {
if (stateMap[i] === 0) {
if (dfs(i)) break;
}
}
if (hasCycle) steps.push({stage:'result', msg:'检测到环,无法完成所有课程', node:-1, state:[...stateMap], topo:[...topoOrder], hasCycle:true});
else steps.push({stage:'result', msg:'无环,可以完成所有课程!拓扑序: [' + topoOrder.reverse().join(', ') + ']', node:-1, state:[...stateMap], topo:[...topoOrder], hasCycle:false});
}
function render(step) {
const s = steps[step];
const stateColors = {0:'#e2e8f0', 1:'#fef3c7', 2:'#dcfce7'};
const stateText = {0:'未访问', 1:'进行中', 2:'已完成'};
const stateBorder = {0:'#94a3b8', 1:'#f59e0b', 2:'#16a34a'};
let viz = '<div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:12px;">';
for (let i=0; i<N; i++) {
const bg = stateColors[s.state[i]];
const border = stateBorder[s.state[i]];
const isCurr = i===s.node;
viz += `<div style="width:60px;height:60px;border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:${bg};border:3px solid ${border};font-weight:700;transition:all 0.3s;${isCurr?'transform:scale(1.15);box-shadow:0 0 0 4px rgba(59,130,246,0.3);':''}">${i}<span style="font-size:10px;font-weight:400;">${stateText[s.state[i]]}</span></div>`;
}
viz += '</div>';
viz += '<div style="margin-top:8px;"><b>依赖关系:</b></div>';
for (const [a,b] of edges) viz += `<span style="margin-right:12px;"><code>${a}←${b}</code></span>`;
if (s.topo.length > 0) viz += `<div style="margin-top:8px;"><b>拓扑序:</b>${s.topo.join(' → ')}</div>`;
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage==='result') {
if (s.hasCycle) $('resultContent').innerHTML = '<div class="final-answer" style="border-color:#f87171;background:#fef2f2;">返回 <b>False</b>(检测到环,无法完成)</div>';
else $('resultContent').innerHTML = `<div class="final-answer">返回 <b>True</b>(可以完成所有课程)<br>拓扑序: ${s.topo.join(' → ')}</div>`;
}
$('hintText').textContent = s.msg;
const stages = ['init→初始化','visiting→访问中','done→完成','cycle→检测到环','result→结果'];
$('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 = '4, [[1,0],[2,0],[3,1],[3,2]]';
buildSteps(examples[0].numCourses, examples[0].prerequisites);
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(/(\d+),\s*(\[[\s\S]*\])/);
if (!m) { alert('格式: numCourses, [[1,0],[2,0]]'); return; }
buildSteps(parseInt(m[1]), JSON.parse(m[2]));
stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length;
} catch(e) { alert('输入格式错误'); }
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
buildSteps(e.numCourses, e.prerequisites); 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 canFinish(numCourses, prerequisites):
adj = [[] for _ in range(numCourses)]
for a, b in prerequisites:
adj[a].append(b)
state = [0] * numCourses # 0=未访问 1=进行中 2=完成
def dfs(node):
state[node] = 1
for nb in adj[node]:
if state[nb] == 1:
return True # 环
if state[nb] == 0 and dfs(nb):
return True
state[node] = 2
return False
for i in range(numCourses):
if state[i] == 0 and dfs(i):
return False
return True`, {lang:'Python'});
'''
def js_implement_trie_prefix_tree():
return r'''
const examples = [
{ops: ['insert("apple")','search("apple")','search("app")','startsWith("app")','insert("app")','search("app")'], label: '示例1: apple/app'},
{ops: ['insert("dog")','insert("deer")','search("dog")','startsWith("de")'], label: '示例2: dog/deer'},
];
let steps, stepCtrl, trieNodes;
function buildSteps(ops) {
steps = [];
trieNodes = [{id:0, char:'ROOT', children:{}, isEnd:false, depth:0}];
steps.push({stage:'init', msg:'初始化Trie根节点', path:[], currentNode:-1, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'init'});
for (const op of ops) {
const insertM = op.match(/insert\("(\w+)"\)/);
const searchM = op.match(/search\("(\w+)"\)/);
const startsM = op.match(/startsWith\("(\w+)"\)/);
if (insertM) {
const word = insertM[1];
steps.push({stage:'op_start', msg:`操作: insert("${word}")`, path:[], currentNode:0, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'insert("'+word+'")'});
let cur = 0;
const path = [0];
for (let i=0; i<word.length; i++) {
const ch = word[i];
if (!trieNodes[cur].children[ch]) {
const newId = trieNodes.length;
trieNodes.push({id:newId, char:ch, children:{}, isEnd:false, depth:i+1});
trieNodes[cur].children[ch] = newId;
steps.push({stage:'create', msg:`创建新节点 '${ch}'(不存在)`, path:[...path], currentNode:newId, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'insert("'+word+'")'});
} else {
steps.push({stage:'traverse', msg:`节点 '${ch}' 已存在,沿路径前进`, path:[...path], currentNode:trieNodes[cur].children[ch], nodes:JSON.parse(JSON.stringify(trieNodes)), op:'insert("'+word+'")'});
}
cur = trieNodes[cur].children[ch];
path.push(cur);
}
trieNodes[cur].isEnd = true;
steps.push({stage:'mark_end', msg:`标记节点为单词结尾 (${word})`, path:[...path], currentNode:cur, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'insert("'+word+'")'});
} else if (searchM) {
const word = searchM[1];
steps.push({stage:'op_start', msg:`操作: search("${word}")`, path:[], currentNode:0, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'search("'+word+'")'});
let cur = 0; let found = true;
const path = [0];
for (let i=0; i<word.length; i++) {
const ch = word[i];
if (!trieNodes[cur].children[ch]) {
found = false;
steps.push({stage:'not_found', msg:`节点 '${ch}' 不存在,搜索失败`, path:[...path], currentNode:-1, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'search("'+word+'")'});
break;
}
cur = trieNodes[cur].children[ch];
path.push(cur);
steps.push({stage:'traverse', msg:`沿 '${ch}' 前进`, path:[...path], currentNode:cur, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'search("'+word+'")'});
}
if (found) {
if (trieNodes[cur].isEnd) steps.push({stage:'found', msg:`"${word}" 存在且为完整单词 ✓`, path:[...path], currentNode:cur, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'search("'+word+'")'});
else steps.push({stage:'prefix_only', msg:`"${word}" 只是前缀,不是完整单词`, path:[...path], currentNode:cur, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'search("'+word+'")'});
}
} else if (startsM) {
const prefix = startsM[1];
steps.push({stage:'op_start', msg:`操作: startsWith("${prefix}")`, path:[], currentNode:0, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'startsWith("'+prefix+'")'});
let cur = 0; let found = true;
const path = [0];
for (let i=0; i<prefix.length; i++) {
const ch = prefix[i];
if (!trieNodes[cur].children[ch]) {
found = false;
steps.push({stage:'not_found', msg:`节点 '${ch}' 不存在,前缀不存在`, path:[...path], currentNode:-1, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'startsWith("'+prefix+'")'});
break;
}
cur = trieNodes[cur].children[ch];
path.push(cur);
steps.push({stage:'traverse', msg:`沿 '${ch}' 前进`, path:[...path], currentNode:cur, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'startsWith("'+prefix+'")'});
}
if (found) steps.push({stage:'found', msg:`前缀 "${prefix}" 存在 ✓`, path:[...path], currentNode:cur, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'startsWith("'+prefix+'")'});
}
}
steps.push({stage:'done', msg:'所有操作完成', path:[], currentNode:-1, nodes:JSON.parse(JSON.stringify(trieNodes)), op:'done'});
}
function renderTrie(nodes, path, current) {
const pathSet = new Set(path);
// Build adjacency from nodes
function buildTree(nodeId) {
const node = nodes[nodeId];
const childKeys = Object.keys(node.children);
if (childKeys.length === 0) return `<div class="tree-node"><div class="node-circle ${pathSet.has(nodeId)?(nodeId===current?'current':'visited'):''}${node.isEnd?' selected':''}">${node.char==='ROOT'?'⊘':node.char}</div>${node.isEnd?'<span style="font-size:9px;color:var(--green);">●</span>':''}</div>`;
const children = childKeys.map(k => buildTree(node.children[k]));
return `<div class="tree-node"><div class="node-circle ${pathSet.has(nodeId)?(nodeId===current?'current':'visited'):''}${node.isEnd?' selected':''}">${node.char==='ROOT'?'⊘':node.char}</div>${node.isEnd?'<span style="font-size:9px;color:var(--green);">●</span>':''}<div class="tree-children">${children.join('')}</div></div>`;
}
return '<div class="tree-container">' + buildTree(0) + '</div>';
}
function render(step) {
const s = steps[step];
let viz = renderTrie(s.nodes, s.path, s.currentNode);
viz += `<div style="margin-top:8px;"><b>当前操作:</b><code>${s.op}</code></div>`;
viz += '<div style="margin-top:4px;display:flex;gap:12px;font-size:13px;"><span style="color:var(--green);">● 单词结尾</span><span style="color:#fef3c7;border:2px solid #f59e0b;border-radius:50%;width:14px;height:14px;display:inline-block;"></span> 当前路径</div>';
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage==='done') {
$('resultContent').innerHTML = '<div class="final-answer">所有操作完成 ✓</div>';
}
$('hintText').textContent = s.msg;
const stages = ['init→初始化','op_start→开始操作','create→创建节点','traverse→遍历','mark_end→标记结尾','found→找到','not_found→未找到','prefix_only→仅前缀','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>`; });
buildSteps(examples[0].ops);
stepCtrl = new StepController({onStep: render, autoInterval:700});
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 ops = JSON.parse($('inputArea').value);
buildSteps(ops); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length;
} catch(e) { alert('请输入操作数组,如 ["insert(\\"apple\\")","search(\\"app\\")"]'); }
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
buildSteps(e.ops); 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(`class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.is_end = True
def search(self, word):
node = self._find(word)
return node is not None and node.is_end
def startsWith(self, prefix):
return self._find(prefix) is not None
def _find(self, prefix):
node = self.root
for ch in prefix:
if ch not in node.children:
return None
node = node.children[ch]
return node`, {lang:'Python'});
'''
# ==================== 回溯 8 题 ====================
def js_permutations():
return r'''
const examples = [
{input: [1,2,3], label: '示例1: [1,2,3]'},
{input: [0,1], label: '示例2: [0,1]'},
{input: [1], label: '示例3: [1]'},
];
let nums, steps, stepCtrl;
function buildSteps(arr) {
nums = [...arr]; steps = [];
const result = [];
const path = [];
function backtrack(first) {
if (first === nums.length) {
result.push([...nums]);
steps.push({stage:'collect', msg:`排列完成: [${nums}]`, arr:[...nums], first, swapping:null, result:JSON.parse(JSON.stringify(result))});
return;
}
for (let i = first; i < nums.length; i++) {
steps.push({stage:'try', msg:`first=${first},尝试交换位置 ${first}↔${i}`, arr:[...nums], first, swapping:[first,i], result:JSON.parse(JSON.stringify(result))});
[nums[first], nums[i]] = [nums[i], nums[first]];
steps.push({stage:'swap', msg:`交换 nums[${first}]↔nums[${i}] → [${nums}]`, arr:[...nums], first, swapping:null, result:JSON.parse(JSON.stringify(result))});
backtrack(first + 1);
[nums[first], nums[i]] = [nums[i], nums[first]];
steps.push({stage:'undo', msg:`回溯:恢复交换 → [${nums}]`, arr:[...nums], first, swapping:null, result:JSON.parse(JSON.stringify(result))});
}
}
steps.push({stage:'start', msg:'回溯交换法生成全排列', arr:[...nums], first:0, swapping:null, result:[]});
backtrack(0);
steps.push({stage:'done', msg:`共 ${result.length} 个排列`, arr:[...nums], first:-1, swapping:null, result:JSON.parse(JSON.stringify(result))});
}
function render(step) {
const s = steps[step];
const hl = {};
if (s.swapping) { hl[s.swapping[0]] = 'orange'; hl[s.swapping[1]] = 'orange'; }
else if (s.first >= 0) { hl[s.first] = 'purple'; }
if (s.stage==='collect') for (let i=0;i<s.arr.length;i++) hl[i]='green';
let viz = renderArray(s.arr, {highlights:hl});
viz += `<div style="margin-top:8px;">first = ${s.first >= 0 ? s.first : '完成'}</div>`;
if (s.result.length > 0) {
viz += '<div style="margin-top:8px;"><b>已找到:</b></div><div style="display:flex;flex-wrap:wrap;gap:6px;">';
s.result.forEach(r => { viz += `<span class="chip green" style="min-width:auto;padding:2px 8px;font-size:12px;">[${r}]</span>`; });
viz += '</div>';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage==='done') $('resultContent').innerHTML = `<div class="final-answer">共 <b>${s.result.length}</b> 个排列<br>${s.result.map(r=>'['+r+']').join(', ')}</div>`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','try→尝试','swap→交换','collect→收集','undo→回溯','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 = '[1,2,3]';
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 arr = JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length; }
catch(e) { alert('请输入合法 JSON 数组'); }
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
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 permute(nums):
res = []
def backtrack(first):
if first == len(nums):
res.append(nums[:])
return
for i in range(first, len(nums)):
nums[first], nums[i] = nums[i], nums[first]
backtrack(first + 1)
nums[first], nums[i] = nums[i], nums[first]
backtrack(0)
return res`, {lang:'Python'});
'''
def js_subsets():
return r'''
const examples = [
{input: [1,2,3], label: '示例1: [1,2,3]'},
{input: [0], label: '示例2: [0]'},
];
let nums, steps, stepCtrl;
function buildSteps(arr) {
nums = [...arr]; steps = [];
const result = [];
const path = [];
function backtrack(idx) {
if (idx === nums.length) {
result.push([...path]);
steps.push({stage:'collect', msg:`收集子集: [${path}]`, path:[...path], idx, choosing:-1, result:JSON.parse(JSON.stringify(result))});
return;
}
// 不选 nums[idx]
steps.push({stage:'skip', msg:`位置${idx}: 不选 ${nums[idx]}`, path:[...path], idx, choosing:idx, result:JSON.parse(JSON.stringify(result))});
backtrack(idx + 1);
// 选 nums[idx]
path.push(nums[idx]);
steps.push({stage:'choose', msg:`位置${idx}: 选择 ${nums[idx]}`, path:[...path], idx, choosing:idx, result:JSON.parse(JSON.stringify(result))});
backtrack(idx + 1);
path.pop();
steps.push({stage:'undo', msg:`回溯:移除 ${nums[idx]}`, path:[...path], idx, choosing:-1, result:JSON.parse(JSON.stringify(result))});
}
steps.push({stage:'start', msg:'回溯法:对每个元素选/不选', path:[], idx:0, choosing:-1, result:[]});
backtrack(0);
steps.push({stage:'done', msg:`共 ${result.length} 个子集`, path:[], idx:-1, choosing:-1, result:JSON.parse(JSON.stringify(result))});
}
function render(step) {
const s = steps[step];
let viz = '<div style="margin-bottom:8px;"><b>原始数组:</b></div>';
const hl = {};
if (s.choosing >= 0) hl[s.choosing] = 'orange';
for (let i=s.idx; i<nums.length; i++) if(i>=s.idx) hl[i] = hl[i] || 'default';
viz += renderArray(nums, {highlights:hl});
viz += `<div style="margin-top:10px;"><b>当前路径:</b><span class="chip purple" style="min-width:auto;padding:2px 10px;">[${s.path.join(', ')}]</span></div>`;
if (s.result.length > 0) {
viz += '<div style="margin-top:8px;"><b>已找到:</b></div><div style="display:flex;flex-wrap:wrap;gap:4px;">';
s.result.forEach(r => { viz += `<span class="chip green" style="min-width:auto;padding:2px 8px;font-size:11px;">[${r.length?r.join(','):''}]</span>`; });
viz += '</div>';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage==='done') $('resultContent').innerHTML = `<div class="final-answer">共 <b>${s.result.length}</b> 个子集</div>`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','skip→不选','choose→选择','collect→收集','undo→回溯','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 = '[1,2,3]';
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 arr = JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length; }
catch(e) { alert('请输入合法 JSON 数组'); }
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
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 subsets(nums):
res = []
def backtrack(idx, path):
if idx == len(nums):
res.append(path[:])
return
backtrack(idx + 1, path) # 不选
path.append(nums[idx])
backtrack(idx + 1, path) # 选
path.pop()
backtrack(0, [])
return res`, {lang:'Python'});
'''
def js_letter_combinations_of_a_phone_number():
return r'''
const examples = [
{input: "23", label: '示例1: "23"'},
{input: "", label: '示例2: 空'},
{input: "2", label: '示例3: "2"'},
];
const digitMap = {'2':'abc','3':'def','4':'ghi','5':'jkl','6':'mno','7':'pqrs','8':'tuv','9':'wxyz'};
let digits, steps, stepCtrl;
function buildSteps(d) {
digits = d; steps = [];
const result = [];
const path = [];
if (d.length === 0) {
steps.push({stage:'done', msg:'输入为空,返回空列表', path:[], digitIdx:-1, result:[]});
return;
}
steps.push({stage:'start', msg:`输入: "${d}",数字对应字母: ${d.split('').map(c=>c+'→'+digitMap[c]).join(' ')}`, path:[], digitIdx:0, result:[]});
function backtrack(idx) {
if (idx === digits.length) {
result.push(path.join(''));
steps.push({stage:'collect', msg:`组合完成: "${path.join('')}"`, path:[...path], digitIdx:idx, result:JSON.parse(JSON.stringify(result))});
return;
}
const letters = digitMap[digits[idx]];
for (const ch of letters) {
path.push(ch);
steps.push({stage:'choose', msg:`位置${idx}(数字${digits[idx]}): 选择字母 '${ch}'`, path:[...path], digitIdx:idx, result:JSON.parse(JSON.stringify(result))});
backtrack(idx + 1);
path.pop();
steps.push({stage:'undo', msg:`回溯:移除 '${ch}',尝试下一个字母`, path:[...path], digitIdx:idx, result:JSON.parse(JSON.stringify(result))});
}
}
backtrack(0);
steps.push({stage:'done', msg:`共 ${result.length} 个组合`, path:[], digitIdx:-1, result:JSON.parse(JSON.stringify(result))});
}
function render(step) {
const s = steps[step];
let viz = '<div style="display:flex;gap:12px;margin-bottom:12px;">';
digits.split('').forEach((d,i) => {
const isCurr = i===s.digitIdx;
viz += `<div style="text-align:center;padding:8px 12px;border-radius:10px;border:2px solid ${isCurr?'var(--orange)':'var(--border)'};background:${isCurr?'#fff7ed':'white'};">
<div style="font-size:20px;font-weight:700;">${d}</div>
<div style="font-size:12px;color:var(--text-secondary);">${digitMap[d]}</div>
</div>`;
});
viz += '</div>';
viz += `<div style="margin-top:8px;"><b>当前路径:</b><span style="font-size:18px;font-weight:700;letter-spacing:2px;color:var(--purple);">${s.path.join('')}</span><span style="color:var(--text-muted);">_`.repeat(Math.max(0, digits.length - s.path.length)) + '</span></div>';
if (s.result.length > 0) {
viz += '<div style="margin-top:8px;"><b>已找到:</b></div><div style="display:flex;flex-wrap:wrap;gap:4px;">';
s.result.forEach(r => { viz += `<span class="chip green" style="min-width:auto;padding:2px 8px;font-size:12px;">${r}</span>`; });
viz += '</div>';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage==='done' && s.result.length > 0) $('resultContent').innerHTML = `<div class="final-answer">共 <b>${s.result.length}</b> 个组合<br>[${s.result.map(r=>'"'+r+'"').join(', ')}]</div>`;
if (s.stage==='done' && s.result.length===0) $('resultContent').innerHTML = '<div class="final-answer">返回 []</div>';
$('hintText').textContent = s.msg;
const stages = ['start→开始','choose→选择字母','collect→收集','undo→回溯','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 = '"23"';
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 = () => {
const v = $('inputArea').value.replace(/"/g,'').trim();
buildSteps(v); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length;
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
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 letterCombinations(digits):
if not digits:
return []
mapping = {'2':'abc','3':'def','4':'ghi','5':'jkl',
'6':'mno','7':'pqrs','8':'tuv','9':'wxyz'}
res = []
def backtrack(idx, path):
if idx == len(digits):
res.append(''.join(path))
return
for ch in mapping[digits[idx]]:
path.append(ch)
backtrack(idx + 1, path)
path.pop()
backtrack(0, [])
return res`, {lang:'Python'});
'''
def js_combination_sum():
return r'''
const examples = [
{candidates: [2,3,6,7], target: 7, label: '示例1: [2,3,6,7] target=7'},
{candidates: [2,3,5], target: 8, label: '示例2: [2,3,5] target=8'},
{candidates: [2], target: 1, label: '示例3: [2] target=1'},
];
let cands, target, steps, stepCtrl;
function buildSteps(arr, tgt) {
cands = [...arr].sort((a,b)=>a-b); target = tgt; steps = [];
const result = [];
const path = [];
steps.push({stage:'start', msg:`排序后: [${cands}], 目标: ${tgt}`, path:[], sum:0, remain:tgt, start:0, result:[]});
function backtrack(start, sum) {
if (sum === target) {
result.push([...path]);
steps.push({stage:'found', msg:`和=${target},找到组合 [${path}]`, path:[...path], sum, remain:0, start, result:JSON.parse(JSON.stringify(result))});
return;
}
for (let i = start; i < cands.length; i++) {
if (sum + cands[i] > target) {
steps.push({stage:'prune', msg:`${sum}+${cands[i]}=${sum+cands[i]} > ${target},剪枝 ✂️`, path:[...path], sum, remain:target-sum, start:i, result:JSON.parse(JSON.stringify(result))});
break; // sorted, so all following are larger
}
path.push(cands[i]);
steps.push({stage:'choose', msg:`选择 ${cands[i]},sum=${sum}+${cands[i]}=${sum+cands[i]},remain=${target-sum-cands[i]}`, path:[...path], sum:sum+cands[i], remain:target-sum-cands[i], start:i, result:JSON.parse(JSON.stringify(result))});
backtrack(i, sum + cands[i]);
path.pop();
steps.push({stage:'undo', msg:`回溯:移除 ${cands[i]}`, path:[...path], sum, remain:target-sum, start:i, result:JSON.parse(JSON.stringify(result))});
}
}
backtrack(0, 0);
steps.push({stage:'done', msg:`共 ${result.length} 个组合`, path:[], sum:0, remain:target, start:-1, result:JSON.parse(JSON.stringify(result))});
}
function render(step) {
const s = steps[step];
const hl = {};
if (s.start >= 0) for (let i=s.start; i<cands.length; i++) hl[i] = i===s.start?'orange':'default';
let viz = '<div style="margin-bottom:8px;"><b>候选数:</b></div>';
viz += renderArray(cands, {highlights:hl});
viz += `<div style="margin-top:10px;display:flex;gap:16px;">
<span><b>当前组合:</b><span class="chip purple" style="min-width:auto;padding:2px 10px;">[${s.path.join(', ')}]</span></span>
<span><b>sum:</b>${s.sum}</span>
<span><b>remain:</b><span style="color:${s.remain===0?'var(--green)':'var(--blue)'};">${s.remain}</span></span>
</div>`;
if (s.result.length > 0) {
viz += '<div style="margin-top:8px;"><b>已找到:</b></div><div style="display:flex;flex-wrap:wrap;gap:4px;">';
s.result.forEach(r => { viz += `<span class="chip green" style="min-width:auto;padding:2px 8px;font-size:12px;">[${r.join(',')}]</span>`; });
viz += '</div>';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage==='done') $('resultContent').innerHTML = `<div class="final-answer">共 <b>${s.result.length}</b> 个组合</div>`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','choose→选择','found→找到','prune→剪枝','undo→回溯','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 = '[2,3,6,7], target=7';
buildSteps(examples[0].candidates, examples[0].target);
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 = () => {
const m = $('inputArea').value.match(/\[([^\]]+)\].*?(\d+)/);
if (!m) { alert('格式: [2,3,6,7], target=7'); return; }
const arr = m[1].split(',').map(Number);
buildSteps(arr, parseInt(m[2])); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length;
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
buildSteps(e.candidates, e.target); 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 combinationSum(candidates, target):
candidates.sort()
res = []
def backtrack(start, path, remaining):
if remaining == 0:
res.append(path[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break # 剪枝
path.append(candidates[i])
backtrack(i, path, remaining - candidates[i])
path.pop()
backtrack(0, [], target)
return res`, {lang:'Python'});
'''
def js_generate_parentheses():
return r'''
const examples = [
{input: 3, label: '示例1: n=3'},
{input: 1, label: '示例2: n=1'},
{input: 2, label: '示例3: n=2'},
];
let n, steps, stepCtrl;
function buildSteps(nn) {
n = nn; steps = [];
const result = [];
const path = [];
steps.push({stage:'start', msg:`生成 ${n} 对括号的有效组合`, path:[], open:0, close:0, result:[]});
function backtrack(open, close) {
if (path.length === 2 * n) {
result.push(path.join(''));
steps.push({stage:'collect', msg:`组合完成: "${path.join('')}"`, path:[...path], open, close, result:JSON.parse(JSON.stringify(result))});
return;
}
if (open < n) {
path.push('(');
steps.push({stage:'add_open', msg:`open(${open})<${n},添加 '(' → open=${open+1}`, path:[...path], open:open+1, close, result:JSON.parse(JSON.stringify(result))});
backtrack(open + 1, close);
path.pop();
steps.push({stage:'undo', msg:`回溯 '(',open=${open}`, path:[...path], open, close, result:JSON.parse(JSON.stringify(result))});
}
if (close < open) {
path.push(')');
steps.push({stage:'add_close', msg:`close(${close})<open(${open}),添加 ')' → close=${close+1}`, path:[...path], open, close:close+1, result:JSON.parse(JSON.stringify(result))});
backtrack(open, close + 1);
path.pop();
steps.push({stage:'undo', msg:`回溯 ')',close=${close}`, path:[...path], open, close, result:JSON.parse(JSON.stringify(result))});
}
}
backtrack(0, 0);
steps.push({stage:'done', msg:`共 ${result.length} 个有效组合`, path:[], open:0, close:0, result:JSON.parse(JSON.stringify(result))});
}
function render(step) {
const s = steps[step];
let viz = `<div style="font-size:28px;font-weight:700;letter-spacing:4px;margin:12px 0;font-family:monospace;">`;
const str = s.path.join('');
for (let i=0; i<str.length; i++) {
const ch = str[i];
viz += `<span style="color:${ch==='('?'var(--blue)':'var(--green)'};">${ch}</span>`;
}
viz += '<span style="color:var(--text-muted);">_'.repeat(Math.max(0, 2*n - str.length)) + '</span></div>';
viz += `<div style="display:flex;gap:16px;margin-top:8px;">
<span style="color:var(--blue);font-weight:700;">open = ${s.open} / ${n}</span>
<span style="color:var(--green);font-weight:700;">close = ${s.close} / ${s.open}</span>
</div>`;
// visual bar
viz += `<div style="margin-top:8px;display:flex;gap:4px;">`;
for (let i=0; i<n; i++) viz += `<div style="width:24px;height:8px;border-radius:4px;background:${i<s.open?'var(--blue)':'#e2e8f0'};"></div>`;
viz += `<span style="margin:0 8px;font-size:14px;">(</span>`;
for (let i=0; i<n; i++) viz += `<div style="width:24px;height:8px;border-radius:4px;background:${i<s.close?'var(--green)':'#e2e8f0'};"></div>`;
viz += `<span style="margin-left:8px;font-size:14px;">)</span></div>`;
if (s.result.length > 0) {
viz += '<div style="margin-top:8px;"><b>已找到:</b></div><div style="display:flex;flex-wrap:wrap;gap:4px;">';
s.result.forEach(r => { viz += `<span class="chip green" style="min-width:auto;padding:2px 10px;font-size:13px;font-family:monospace;">${r}</span>`; });
viz += '</div>';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage==='done') $('resultContent').innerHTML = `<div class="final-answer">共 <b>${s.result.length}</b> 个组合<br>[${s.result.map(r=>'"'+r+'"').join(', ')}]</div>`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','add_open→加左括号','add_close→加右括号','collect→收集','undo→回溯','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 = '3';
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 = () => {
const v = parseInt($('inputArea').value);
if (isNaN(v)||v<1||v>6) { alert('请输入1-6的整数'); return; }
buildSteps(v); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length;
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
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 generateParenthesis(n):
res = []
def backtrack(path, open, close):
if len(path) == 2 * n:
res.append(''.join(path))
return
if open < n:
path.append('(')
backtrack(path, open + 1, close)
path.pop()
if close < open:
path.append(')')
backtrack(path, open, close + 1)
path.pop()
backtrack([], 0, 0)
return res`, {lang:'Python'});
'''
def js_word_search():
return r'''
const examples = [
{board: [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word: "ABCCED", label: '示例1: ABCCED'},
{board: [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word: "SEE", label: '示例2: SEE'},
{board: [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word: "ABCB", label: '示例3: ABCB(不存在)'},
];
let board, word, R, C, steps, stepCtrl;
function buildSteps(b, w) {
board = b.map(r=>[...r]); R = board.length; C = board[0].length; word = w;
steps = [];
let found = false;
steps.push({stage:'start', msg:`在网格中搜索单词 "${word}"`, hl:{}, path:[], charIdx:0, found:false});
function dfs(r, c, idx, path, visited) {
if (idx === word.length) {
found = true;
steps.push({stage:'found', msg:`找到单词 "${word}"!`, hl:{}, path:[...path], charIdx:idx, found:true});
return true;
}
if (r<0||r>=R||c<0||c>=C||visited.has(r+','+c)||board[r][c]!==word[idx]) {
if (r>=0&&r<R&&c>=0&&c<C&&board[r][c]!==word[idx])
steps.push({stage:'mismatch', msg:`(${r},${c})='${board[r][c]}' ≠ '${word[idx]}',跳过`, hl:{[r+','+c]:'wall'}, path:[...path], charIdx:idx, found:false});
return false;
}
visited.add(r+','+c);
path.push(r+','+c);
const hl = {}; hl[r+','+c] = 'current';
// show visited cells
visited.forEach(k => { if(k!==r+','+c) hl[k]='visited'; });
steps.push({stage:'match', msg:`(${r},${c})='${board[r][c]}' = '${word[idx]}' 匹配 ✓ (第${idx+1}/${word.length}个)`, hl, path:[...path], charIdx:idx+1, found:false});
const dirs = [[0,1],[0,-1],[1,0],[-1,0]];
for (const [dr,dc] of dirs) {
if (dfs(r+dr, c+dc, idx+1, path, visited)) return true;
}
visited.delete(r+','+c);
path.pop();
steps.push({stage:'backtrack', msg:`从 (${r},${c}) 回溯`, hl:{}, path:[...path], charIdx:idx, found:false});
return false;
}
for (let r=0; r<R && !found; r++) {
for (let c=0; c<C && !found; c++) {
if (board[r][c] === word[0]) {
const hl = {}; hl[r+','+c] = 'current';
steps.push({stage:'start_pos', msg:`找到起始点 (${r},${c})='${word[0]}'`, hl, path:[], charIdx:0, found:false});
const path = [];
const visited = new Set();
dfs(r, c, 0, path, visited);
}
}
}
if (!found) steps.push({stage:'not_found', msg:`未找到单词 "${word}"`, hl:{}, path:[], charIdx:-1, found:false});
}
function render(step) {
const s = steps[step];
const cellStyle = (val,r,c) => {
const key = r+','+c;
if (s.hl[key]==='current') return 'background:#fef3c7;border-color:var(--orange);box-shadow:0 0 0 3px rgba(245,158,11,0.3);color:#92400e;font-weight:700;';
if (s.hl[key]==='visited') return 'background:#dcfce7;border-color:var(--green);color:#166534;';
if (s.hl[key]==='wall') return 'background:#fee2e2;color:#991b1b;';
return 'background:white;color:var(--text);';
};
let viz = renderGrid(board, {highlights:s.hl, cellSize:48, cellStyle});
viz += `<div style="margin-top:8px;"><b>搜索词:</b>`;
for (let i=0; i<word.length; i++) {
const matched = i < s.charIdx;
viz += `<span style="font-weight:700;font-size:16px;color:${matched?'var(--green)':'var(--text-muted)'};${i===s.charIdx?'text-decoration:underline;':''}">${word[i]}</span>`;
}
viz += '</div>';
if (s.path.length > 0) viz += `<div style="margin-top:4px;">路径: ${s.path.map(p=>'('+p+')').join(' → ')}</div>`;
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage==='found') $('resultContent').innerHTML = `<div class="final-answer">返回 <b>True</b>,路径: ${s.path.map(p=>'('+p+')').join('→')}</div>`;
if (s.stage==='not_found') $('resultContent').innerHTML = '<div class="final-answer" style="border-color:#f87171;background:#fef2f2;">返回 <b>False</b></div>';
$('hintText').textContent = s.msg;
const stages = ['start→开始','start_pos→起始点','match→匹配','mismatch→不匹配','backtrack→回溯','found→找到','not_found→未找到'];
$('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 = 'ABCCED';
buildSteps(examples[0].board, examples[0].word);
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 = () => {
buildSteps(examples[0].board, $('inputArea').value.trim());
stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length;
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
buildSteps(e.board, e.word); 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 exist(board, word):
rows, cols = len(board), len(board[0])
def dfs(r, c, idx, visited):
if idx == len(word):
return True
if (r<0 or r>=rows or c<0 or c>=cols
or (r,c) in visited or board[r][c]!=word[idx]):
return False
visited.add((r, c))
for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
if dfs(r+dr, c+dc, idx+1, visited):
return True
visited.remove((r, c))
return False
for r in range(rows):
for c in range(cols):
if dfs(r, c, 0, set()):
return True
return False`, {lang:'Python'});
'''
def js_palindrome_partitioning():
return r'''
const examples = [
{input: "aab", label: '示例1: "aab"'},
{input: "a", label: '示例2: "a"'},
{input: "racecar", label: '示例3: "racecar"'},
];
let s, steps, stepCtrl;
function isPalindrome(str, l, r) {
while (l < r) { if (str[l] !== str[r]) return false; l++; r--; }
return true;
}
function buildSteps(str) {
s = str; steps = [];
const result = [];
const path = [];
steps.push({stage:'start', msg:`对 "${str}" 进行回文分割`, path:[], start:0, checking:null, result:[]});
function backtrack(start) {
if (start === s.length) {
result.push([...path]);
steps.push({stage:'collect', msg:`分割完成: [${path.map(p=>'"'+p+'"').join(', ')}]`, path:[...path], start, checking:null, result:JSON.parse(JSON.stringify(result))});
return;
}
for (let end = start; end < s.length; end++) {
const sub = s.substring(start, end + 1);
const isPalin = isPalindrome(s, start, end);
steps.push({stage:'check', msg:`检查 "${sub}" (${start}..${end}): ${isPalin?'是回文 ✓':'不是回文 ✗'}`, path:[...path], start, checking:{from:start, to:end, isPalin}, result:JSON.parse(JSON.stringify(result))});
if (isPalin) {
path.push(sub);
steps.push({stage:'choose', msg:`选择 "${sub}" 加入路径`, path:[...path], start:end+1, checking:null, result:JSON.parse(JSON.stringify(result))});
backtrack(end + 1);
path.pop();
steps.push({stage:'undo', msg:`回溯:移除 "${sub}"`, path:[...path], start, checking:null, result:JSON.parse(JSON.stringify(result))});
}
}
}
backtrack(0);
steps.push({stage:'done', msg:`共 ${result.length} 种分割方案`, path:[], start:-1, checking:null, result:JSON.parse(JSON.stringify(result))});
}
function render(step) {
const st = steps[step];
let viz = `<div style="font-size:20px;font-family:monospace;letter-spacing:2px;margin:12px 0;">`;
// color the string by current path partitions
let pos = 0;
const parts = st.path;
const colors = ['#dbeafe','#dcfce7','#ede9fe','#fff7ed','#cffafe','#fee2e2'];
for (let pi=0; pi<parts.length; pi++) {
const part = parts[pi];
for (let j=0; j<part.length; j++) {
viz += `<span style="background:${colors[pi%colors.length]};padding:2px 1px;border-radius:3px;">${part[j]}</span>`;
pos++;
}
}
// remaining chars
for (let i=pos; i<s.length; i++) {
const isChecking = st.checking && i>=st.checking.from && i<=st.checking.to;
viz += `<span style="${isChecking?(st.checking.isPalin?'background:#dcfce7;':'background:#fee2e2;'):''}padding:2px 1px;border-radius:3px;">${s[i]}</span>`;
}
viz += '</div>';
// path
viz += `<div style="margin-top:8px;"><b>当前分割:</b>`;
if (st.path.length > 0) {
st.path.forEach((p,i) => { viz += `<span class="chip" style="min-width:auto;padding:2px 8px;background:${colors[i%colors.length]};font-size:12px;">"${p}"</span>`; });
} else viz += '<span style="color:var(--text-muted);">空</span>';
viz += '</div>';
if (st.result.length > 0) {
viz += '<div style="margin-top:8px;"><b>已找到:</b></div><div style="display:flex;flex-wrap:wrap;gap:4px;">';
st.result.forEach(r => { viz += `<span class="chip green" style="min-width:auto;padding:2px 8px;font-size:11px;">[${r.map(x=>'"'+x+'"').join(',')}]</span>`; });
viz += '</div>';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + st.msg + '</div>';
if (st.stage==='done') $('resultContent').innerHTML = `<div class="final-answer">共 <b>${st.result.length}</b> 种分割方案</div>`;
$('hintText').textContent = st.msg;
const stages = ['start→开始','check→检查回文','choose→选择','collect→收集','undo→回溯','done→完成'];
$('pipeline').innerHTML = stages.map(stt => { const [k,l]=stt.split('→'); return `<span class="pipe-step ${st.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 = '"aab"';
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 = () => {
const v = $('inputArea').value.replace(/"/g,'').trim();
buildSteps(v); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length;
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
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 partition(s):
res = []
def is_palindrome(sub, l, r):
while l < r:
if sub[l] != sub[r]: return False
l += 1; r -= 1
return True
def backtrack(start, path):
if start == len(s):
res.append(path[:])
return
for end in range(start, len(s)):
if is_palindrome(s, start, end):
path.append(s[start:end+1])
backtrack(end + 1, path)
path.pop()
backtrack(0, [])
return res`, {lang:'Python'});
'''
def js_n_queens():
return r'''
const examples = [
{input: 4, label: '示例1: n=4'},
{input: 1, label: '示例2: n=1'},
{input: 6, label: '示例3: n=6'},
];
let n, steps, stepCtrl;
function buildSteps(nn) {
n = nn; steps = [];
const result = [];
const queens = []; // queens[row] = col
steps.push({stage:'start', msg:`${n}皇后问题:逐行放置,检查列和对角线冲突`, queens:[], row:-1, col:-1, result:[]});
function isValid(row, col) {
for (let r = 0; r < queens.length; r++) {
const c = queens[r];
if (c === col || r + c === row + col || r - c === row - col) return false;
}
return true;
}
function backtrack(row) {
if (row === n) {
result.push([...queens]);
const board = queensToBoard(queens);
steps.push({stage:'collect', msg:`找到解!皇后位置: ${queens.map((c,r)=>`(${r},${c})`).join(' ')}`, queens:[...queens], row, col:-1, result:JSON.parse(JSON.stringify(result))});
return;
}
for (let col = 0; col < n; col++) {
const valid = isValid(row, col);
if (valid) {
steps.push({stage:'try_valid', msg:`行${row} 列${col}: 无冲突 ✓,放置皇后`, queens:[...queens], row, col, result:JSON.parse(JSON.stringify(result))});
queens.push(col);
backtrack(row + 1);
queens.pop();
steps.push({stage:'undo', msg:`回溯行${row},移除列${col}皇后`, queens:[...queens], row, col, result:JSON.parse(JSON.stringify(result))});
} else {
steps.push({stage:'conflict', msg:`行${row} 列${col}: 冲突 ✗(列/对角线已有皇后)`, queens:[...queens], row, col, result:JSON.parse(JSON.stringify(result))});
}
}
}
backtrack(0);
steps.push({stage:'done', msg:`共 ${result.length} 个解`, queens:[], row:-1, col:-1, result:JSON.parse(JSON.stringify(result))});
}
function queensToBoard(q) {
return q.map(c => { let row = '.'.repeat(n); return row.substring(0,c) + 'Q' + row.substring(c+1); });
}
function render(step) {
const s = steps[step];
const qSet = new Set(s.queens.map((c,r) => r+','+c));
const tryKey = s.row >= 0 && s.col >= 0 ? s.row+','+s.col : null;
// Build chess board
let viz = `<div style="display:inline-grid;grid-template-columns:repeat(${n},42px);gap:2px;padding:8px;border-radius:8px;background:#1e293b;">`;
for (let r = 0; r < n; r++) {
for (let c = 0; c < n; c++) {
const isQueen = s.queens[r] === c;
const isTry = r === s.row && c === s.col;
const isConflict = isTry && s.stage === 'conflict';
let bg = (r+c) % 2 === 0 ? '#f0f9ff' : '#e0f2fe';
if (isQueen) bg = '#dbeafe';
if (isConflict) bg = '#fee2e2';
else if (isTry && s.stage === 'try_valid') bg = '#fef3c7';
const content = isQueen ? '♛' : '';
const border = isTry ? `2px solid ${isConflict?'var(--red)':'var(--orange)'}` : isQueen ? '2px solid var(--blue)' : '1px solid #94a3b8';
viz += `<div style="width:42px;height:42px;display:flex;align-items:center;justify-content:center;background:${bg};border:${border};border-radius:4px;font-size:20px;cursor:default;${isQueen?'color:var(--blue-dark);font-weight:700;':''}">${content}</div>`;
}
}
viz += '</div>';
// Conflict lines for current try
if (s.stage === 'conflict' && s.row >= 0) {
viz += '<div style="margin-top:8px;color:var(--red);font-size:13px;">';
for (let r=0; r<s.queens.length; r++) {
const c = s.queens[r];
if (c === s.col) viz += `列冲突: 行${r}列${c} ↔ 行${s.row}列${s.col} `;
if (r+c === s.row+s.col) viz += `↗对角线冲突: (${r},${c}) ↔ (${s.row},${s.col}) `;
if (r-c === s.row-s.col) viz += `↘对角线冲突: (${r},${c}) ↔ (${s.row},${s.col}) `;
}
viz += '</div>';
}
viz += `<div style="margin-top:8px;"><b>当前放置:</b> ${s.queens.length > 0 ? s.queens.map((c,r)=>`行${r}=列${c}`).join(', ') : '无'}</div>`;
if (s.result.length > 0) {
viz += `<div style="margin-top:8px;"><b>已找到 ${s.result.length} 个解</b></div>`;
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage==='done') $('resultContent').innerHTML = `<div class="final-answer">共 <b>${s.result.length}</b> 个解</div>`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','try_valid→尝试放置','conflict→冲突','collect→收集解','undo→回溯','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 = '4';
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 = () => {
const v = parseInt($('inputArea').value);
if (isNaN(v)||v<1||v>8) { alert('请输入1-8的整数(8以上步骤过多)'); return; }
buildSteps(v); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0); $('stepInfo').textContent = '步骤 1 / ' + steps.length;
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
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 solveNQueens(n):
res = []
def backtrack(row, queens):
if row == n:
res.append(queens[:])
return
for col in range(n):
valid = True
for r, c in enumerate(queens):
if c == col or r+c == row+col or r-c == row-col:
valid = False; break
if valid:
queens.append(col)
backtrack(row + 1, queens)
queens.pop()
backtrack(0, [])
return [['.'*c + 'Q' + '.'*(n-c-1) for c in sol] for sol in res]`, {lang:'Python'});
'''
# ========== 多维动态规划 ==========
def js_unique_paths():
return r'''
const examples = [
{m:3, n:7, label: '示例1: m=3, n=7'},
{m:3, n:2, label: '示例2: m=3, n=2'},
{m:3, n:3, label: '示例3: m=3, n=3'},
];
let steps, stepCtrl, m, n;
function buildSteps(rows, cols) {
m = rows; n = cols; steps = [];
const dp = [];
for (let i = 0; i < m; i++) dp[i] = new Array(n).fill(0);
steps.push({stage:'init', msg:`创建 ${m}×${n} DP 表,dp[i][j] = 从(0,0)到(i,j)的路径数`, dp:dp.map(r=>[...r]), row:-1, col:-1});
for (let j = 0; j < n; j++) { dp[0][j] = 1; steps.push({stage:'fill', msg:`第一行 dp[0][${j}]=1(只能向右)`, dp:dp.map(r=>[...r]), row:0, col:j}); }
for (let i = 0; i < m; i++) { dp[i][0] = 1; steps.push({stage:'fill', msg:`第一列 dp[${i}][0]=1(只能向下)`, dp:dp.map(r=>[...r]), row:i, col:0}); }
for (let i = 1; i < m; i++) for (let j = 1; j < n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
steps.push({stage:'fill', msg:`dp[${i}][${j}] = dp[${i-1}][${j}] + dp[${i}][${j-1}] = ${dp[i-1][j]} + ${dp[i][j-1]} = ${dp[i][j]}`, dp:dp.map(r=>[...r]), row:i, col:j});
}
steps.push({stage:'done', msg:`不同路径共 ${dp[m-1][n-1]} 条`, dp:dp.map(r=>[...r]), row:m-1, col:n-1});
}
function render(step) {
const s = steps[step]; const hl = {};
if (s.row >= 0 && s.col >= 0) hl[`${s.row},${s.col}`] = 'current';
for (let i=0;i<m;i++) for (let j=0;j<n;j++) if (s.dp[i][j]>0 && !(s.row===i&&s.col===j)) hl[`${i},${j}`]='visited';
let viz = renderGrid(s.dp, {highlights:hl, cellSize:44, cellStyle:(v)=>v===0?'color:#94a3b8;':'font-weight:700;'});
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">'+s.msg+'</div>';
if (s.dp[m-1][n-1]>0) $('detailContent').innerHTML += `<div class="current-answer">dp[${m-1}][${n-1}] = <b>${s.dp[m-1][n-1]}</b></div>`;
if (s.stage==='done') $('resultContent').innerHTML = `<div class="final-answer">不同路径数 = <b>${s.dp[m-1][n-1]}</b></div>`;
$('hintText').textContent = s.msg;
const stages = ['init→初始化','fill→填充','done→完成'];
$('pipeline').innerHTML = stages.map(x=>{const[k,l]=x.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='m=3, n=7'; buildSteps(examples[0].m, examples[0].n);
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=()=>{const v=$('inputArea').value.match(/m\s*=\s*(\d+).*n\s*=\s*(\d+)/);if(!v){alert('格式: m=3, n=7');return;}buildSteps(parseInt(v[1]),parseInt(v[2]));stepCtrl.setSteps(steps.map((_,i)=>i));render(0);$('stepInfo').textContent='步骤 1 / '+steps.length;};
$('exampleSelect').onchange=()=>{const e=examples[parseInt($('exampleSelect').value)];$('inputArea').value=`m=${e.m}, n=${e.n}`;buildSteps(e.m,e.n);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 uniquePaths(m, n):
dp = [[0]*n for _ in range(m)]
for i in range(m): dp[i][0] = 1
for j in range(n): dp[0][j] = 1
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]`, {lang:'Python'});
'''
def js_minimum_path_sum():
return r'''
const examples = [
{input:[[1,3,1],[1,5,1],[4,2,1]], label:'示例1'},
{input:[[1,2,3],[4,5,6]], label:'示例2'},
];
let steps, stepCtrl, grid, m, n;
function buildSteps(g) {
grid=g.map(r=>[...r]); m=g.length; n=g[0].length; steps=[];
const dp=[]; for(let i=0;i<m;i++) dp[i]=new Array(n).fill(0);
steps.push({stage:'init',msg:`创建 ${m}×${n} DP 表`,dp:dp.map(r=>[...r]),row:-1,col:-1,pathCells:[]});
dp[0][0]=grid[0][0]; steps.push({stage:'fill',msg:`dp[0][0]=${grid[0][0]}`,dp:dp.map(r=>[...r]),row:0,col:0,pathCells:['0,0']});
for(let j=1;j<n;j++){dp[0][j]=dp[0][j-1]+grid[0][j];steps.push({stage:'fill',msg:`dp[0][${j}]=${dp[0][j-1]}+${grid[0][j]}=${dp[0][j]}`,dp:dp.map(r=>[...r]),row:0,col:j,pathCells:Array.from({length:j+1},(_,k)=>`0,${k}`)});}
for(let i=1;i<m;i++){dp[i][0]=dp[i-1][0]+grid[i][0];steps.push({stage:'fill',msg:`dp[${i}][0]=${dp[i-1][0]}+${grid[i][0]}=${dp[i][0]}`,dp:dp.map(r=>[...r]),row:i,col:0,pathCells:Array.from({length:i+1},(_,k)=>`${k},0`)});}
for(let i=1;i<m;i++) for(let j=1;j<n;j++){
const ft=dp[i-1][j],fl=dp[i][j-1]; dp[i][j]=Math.min(ft,fl)+grid[i][j];
steps.push({stage:'fill',msg:`dp[${i}][${j}]=min(${ft},${fl})+${grid[i][j]}=${dp[i][j]} ←${ft<=fl?'上':'左'}`,dp:dp.map(r=>[...r]),row:i,col:j,pathCells:[]});
}
const path=[];let pi=m-1,pj=n-1;path.unshift(`${pi},${pj}`);while(pi>0||pj>0){if(pi===0)pj--;else if(pj===0)pi--;else{dp[pi-1][pj]<=dp[pi][pj-1]?pi--:pj--;}path.unshift(`${pi},${pj}`);}
steps.push({stage:'done',msg:`最小路径和=${dp[m-1][n-1]}`,dp:dp.map(r=>[...r]),row:m-1,col:n-1,pathCells:path});
}
function render(step) {
const s=steps[step]; const hl={};
if(s.row>=0&&s.col>=0) hl[`${s.row},${s.col}`]='current';
s.pathCells.forEach(k=>{if(!(s.row+','+s.col===k))hl[k]='visited';});
for(let i=0;i<m;i++) for(let j=0;j<n;j++) if(s.dp[i][j]>0&&!hl[`${i},${j}`]&&!(s.row===i&&s.col===j)) hl[`${i},${j}`]='visited';
let viz='<div style="margin-bottom:8px;font-weight:600;color:#64748b;">原始网格:</div>'+renderGrid(grid,{cellSize:40});
viz+='<div style="margin:12px 0;font-weight:600;color:#64748b;">DP 表:</div>'+renderGrid(s.dp,{highlights:hl,cellSize:44,cellStyle:(v)=>v===0?'color:#94a3b8;':'font-weight:700;'});
if(s.pathCells.length>1) viz+='<div style="margin-top:8px;color:#8b5cf6;">路径: '+s.pathCells.map(p=>`(${p})`).join(' → ')+'</div>';
$('vizArea').innerHTML=viz;
$('detailContent').innerHTML='<div class="calc-block">'+s.msg+'</div>';
if(s.dp[m-1]&&s.dp[m-1][n-1]>0) $('detailContent').innerHTML+=`<div class="current-answer">dp[${m-1}][${n-1}] = <b>${s.dp[m-1][n-1]}</b></div>`;
if(s.stage==='done') $('resultContent').innerHTML=`<div class="final-answer">最小路径和 = <b>${s.dp[m-1][n-1]}</b></div>`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','fill→填充','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.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='[[1,3,1],[1,5,1],[4,2,1]]';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 g=JSON.parse($('inputArea').value);buildSteps(g);stepCtrl.setSteps(steps.map((_,i)=>i));render(0);}catch(e){alert('请输入合法JSON二维数组');}};
$('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 minPathSum(grid):
m, n = len(grid), len(grid[0])
dp = [[0]*n for _ in range(m)]
dp[0][0] = grid[0][0]
for j in range(1, n): dp[0][j] = dp[0][j-1] + grid[0][j]
for i in range(1, m): dp[i][0] = dp[i-1][0] + grid[i][0]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
return dp[m-1][n-1]`, {lang:'Python'});
'''
def js_longest_palindromic_substring():
return r'''
const examples = [
{input:'babad', label:'示例1: "babad"'},
{input:'cbbd', label:'示例2: "cbbd"'},
{input:'aacabdkacaa', label:'示例3: "aacabdkacaa"'},
];
let steps, stepCtrl, s;
function buildSteps(str) {
s=str; steps=[]; let bestL=0, bestR=0;
steps.push({stage:'init',msg:'中心扩展:以每个字符/对为中心向两边扩展',center:-1,isEven:false,bestL:0,bestR:0,expandPairs:[]});
for(let i=0;i<str.length;i++){
let l=i,r=i;const op=[];
while(l>=0&&r<str.length&&str[l]===str[r]){op.push({l,r});if(r-l>bestR-bestL){bestL=l;bestR=r;}steps.push({stage:'expand',msg:`奇数中心i=${i}: str[${l}]='${str[l]}'==str[${r}]='${str[r]}' ✓ [${l},${r}] len=${r-l+1}`,center:i,isEven:false,bestL,bestR,expandPairs:[...op]});l--;r++;}
l=i;r=i+1;const ep=[];
if(r<str.length){while(l>=0&&r<str.length&&str[l]===str[r]){ep.push({l,r});if(r-l>bestR-bestL){bestL=l;bestR=r;}steps.push({stage:'expand',msg:`偶数中心i=${i}: str[${l}]='${str[l]}'==str[${r}]='${str[r]}' ✓ [${l},${r}] len=${r-l+1}`,center:i,isEven:true,bestL,bestR,expandPairs:[...ep]});l--;r++;}
if(ep.length===0) steps.push({stage:'skip',msg:`偶数中心i=${i}: str[${i}]='${str[i]}'≠str[${i+1}]='${str[i+1]}' ✗`,center:i,isEven:true,bestL,bestR,expandPairs:[]});}
}
steps.push({stage:'done',msg:`最长回文: s[${bestL}..${bestR}]="${s.substring(bestL,bestR+1)}" 长度=${bestR-bestL+1}`,center:-1,isEven:false,bestL,bestR,expandPairs:[]});
}
function render(step) {
const st=steps[step];const hl={};
if(st.expandPairs) st.expandPairs.forEach(p=>{for(let k=p.l;k<=p.r;k++)hl[k]='orange';});
for(let k=st.bestL;k<=st.bestR;k++) hl[k]='green';
if(st.center>=0) hl[st.center]='purple';
let viz='<div class="nums-line">';s.split('').forEach((ch,i)=>{viz+=`<span class="chip-group"><span class="chip ${hl[i]||'default'}">${ch}</span><span class="chip-index">${i}</span></span>`;});viz+='</div>';
if(st.expandPairs&&st.expandPairs.length>0){const p=st.expandPairs[st.expandPairs.length-1];viz+=`<div style="margin-top:8px;color:#ea580c;">当前回文: [${p.l},${p.r}]="${s.substring(p.l,p.r+1)}"</div>`;}
viz+=`<div style="margin-top:4px;color:#16a34a;">最长回文: [${st.bestL},${st.bestR}]="${s.substring(st.bestL,st.bestR+1)}"</div>`;
$('vizArea').innerHTML=viz;
$('detailContent').innerHTML='<div class="calc-block">'+st.msg+'</div>'+`<div class="current-answer">最长回文: <b>"${s.substring(st.bestL,st.bestR+1)}"</b> (len ${st.bestR-st.bestL+1})</div>`;
if(st.stage==='done') $('resultContent').innerHTML=`<div class="final-answer">最长回文子串 = <b>"${s.substring(st.bestL,st.bestR+1)}"</b><br>长度=${st.bestR-st.bestL+1}</div>`;
$('hintText').textContent=st.msg;
const stages=['init→初始化','expand→扩展','skip→跳过','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.split('→');return`<span class="pipe-step ${st.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='"babad"';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=()=>{const v=$('inputArea').value.trim().replace(/^["']|["']$/g,'');if(!v){alert('请输入字符串');return;}buildSteps(v);stepCtrl.setSteps(steps.map((_,i)=>i));render(0);};
$('exampleSelect').onchange=()=>{const e=examples[parseInt($('exampleSelect').value)];$('inputArea').value=`"${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 longestPalindrome(s):
best_l, best_r = 0, 0
for i in range(len(s)):
l, r = i, i
while l >= 0 and r < len(s) and s[l] == s[r]:
if r - l > best_r - best_l: best_l, best_r = l, r
l -= 1; r += 1
l, r = i, i + 1
while l >= 0 and r < len(s) and s[l] == s[r]:
if r - l > best_r - best_l: best_l, best_r = l, r
l -= 1; r += 1
return s[best_l:best_r+1]`, {lang:'Python'});
'''
def js_longest_common_subsequence():
return r'''
const examples = [
{s1:'abcde', s2:'ace', label:'示例1: "abcde","ace"'},
{s1:'abc', s2:'abc', label:'示例2: "abc","abc"'},
{s1:'abc', s2:'def', label:'示例3: "abc","def"'},
];
let steps, stepCtrl, str1, str2, m, n;
function buildSteps(a, b) {
str1=a;str2=b;m=a.length;n=b.length;steps=[];
const dp=[];for(let i=0;i<=m;i++) dp[i]=new Array(n+1).fill(0);
steps.push({stage:'init',msg:`构建 (${m}+1)×(${n}+1) DP 表`,dp:dp.map(r=>[...r]),row:-1,col:-1});
for(let i=1;i<=m;i++) for(let j=1;j<=n;j++){
if(a[i-1]===b[j-1]){dp[i][j]=dp[i-1][j-1]+1;steps.push({stage:'match',msg:`s1[${i-1}]='${a[i-1]}'==s2[${j-1}]='${b[j-1]}'→dp[${i}][${j}]=${dp[i][j]}`,dp:dp.map(r=>[...r]),row:i,col:j,i1:i-1,i2:j-1});}
else{dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);steps.push({stage:'no_match',msg:`s1[${i-1}]='${a[i-1]}'≠s2[${j-1}]='${b[j-1]}'→dp[${i}][${j}]=max(${dp[i-1][j]},${dp[i][j-1]})=${dp[i][j]}`,dp:dp.map(r=>[...r]),row:i,col:j,i1:i-1,i2:j-1});}
}
const lcs=[];let ti=m,tj=n;while(ti>0&&tj>0){if(a[ti-1]===b[tj-1]){lcs.unshift(a[ti-1]);ti--;tj--;}else if(dp[ti-1][tj]>=dp[ti][tj-1])ti--;else tj--;}
steps.push({stage:'done',msg:`LCS=${dp[m][n]},子序列="${lcs.join('')}"`,dp:dp.map(r=>[...r]),row:m,col:n,lcs:[...lcs]});
}
function render(step) {
const s=steps[step];const hl={};if(s.row>=0&&s.col>=0)hl[`${s.row},${s.col}`]='current';
for(let i=1;i<=m;i++)for(let j=1;j<=n;j++)if(s.dp[i][j]>0&&!(s.row===i&&s.col===j))hl[`${i},${j}`]='visited';
let viz='<div style="display:flex;gap:24px;"><div><b>s1:</b> ';
str1.split('').forEach((ch,i)=>{viz+=`<span class="chip ${s.i1===i?'purple':'default'}" style="width:32px;">${ch}</span>`;});
viz+='</div><div><b>s2:</b> ';
str2.split('').forEach((ch,j)=>{viz+=`<span class="chip ${s.i2===j?'purple':'default'}" style="width:32px;">${ch}</span>`;});
viz+='</div></div><div style="margin-top:12px;overflow-x:auto;"><table style="border-collapse:collapse;font-size:14px;">';
viz+='<tr><td style="border:1px solid #e2e8f0;padding:6px 10px;background:#f1f5f9;"></td><td style="border:1px solid #e2e8f0;padding:6px 10px;background:#f1f5f9;">∅</td>';
for(let j=0;j<n;j++) viz+=`<td style="border:1px solid #e2e8f0;padding:6px 10px;background:#f1f5f9;">${str2[j]}</td>`;
viz+='</tr>';
for(let i=0;i<=m;i++){viz+='<tr>';viz+=`<td style="border:1px solid #e2e8f0;padding:6px 10px;background:#f1f5f9;">${i===0?'∅':str1[i-1]}</td>`;
for(let j=0;j<=n;j++){const key=`${i},${j}`;let bg='white',fw='normal';if(hl[key]==='current'){bg='#fef3c7';fw='bold';}else if(hl[key]==='visited'){bg='#ecfdf5';}else if(i===0||j===0){bg='#f1f5f9';}
viz+=`<td style="border:1px solid #e2e8f0;padding:6px 10px;text-align:center;background:${bg};font-weight:${fw};min-width:40px;">${s.dp[i][j]}</td>`;}
viz+='</tr>';}
viz+='</table></div>';$('vizArea').innerHTML=viz;
let detail='<div class="calc-block">'+s.msg+'</div>';if(s.lcs)detail+=`<div class="current-answer">LCS=<b>"${s.lcs.join('')}"</b></div>`;$('detailContent').innerHTML=detail;
if(s.stage==='done') $('resultContent').innerHTML=`<div class="final-answer">LCS长度=<b>${s.dp[m][n]}</b><br>子序列="${s.lcs.join('')}"</div>`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','match→匹配','no_match→不匹配','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.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='s1="abcde", s2="ace"';buildSteps(examples[0].s1,examples[0].s2);
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=()=>{const v=$('inputArea').value.match(/s1\s*=\s*"([^"]+)".*s2\s*=\s*"([^"]+)"/);if(!v){alert('格式: s1="abcde", s2="ace"');return;}buildSteps(v[1],v[2]);stepCtrl.setSteps(steps.map((_,i)=>i));render(0);};
$('exampleSelect').onchange=()=>{const e=examples[parseInt($('exampleSelect').value)];$('inputArea').value=`s1="${e.s1}", s2="${e.s2}"`;buildSteps(e.s1,e.s2);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 longestCommonSubsequence(s1, s2):
m, n = len(s1), len(s2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]`, {lang:'Python'});
'''
def js_edit_distance():
return r'''
const examples = [
{s1:'horse', s2:'ros', label:'示例1: "horse"→"ros"'},
{s1:'intention', s2:'execution', label:'示例2: "intention"→"execution"'},
{s1:'kitten', s2:'sitting', label:'示例3: "kitten"→"sitting"'},
];
let steps, stepCtrl, str1, str2, m, n;
function buildSteps(a, b) {
str1=a;str2=b;m=a.length;n=b.length;steps=[];
const dp=[];for(let i=0;i<=m;i++) dp[i]=new Array(n+1).fill(0);
steps.push({stage:'init',msg:`构建 (${m}+1)×(${n}+1) DP 表`,dp:dp.map(r=>[...r]),row:-1,col:-1});
for(let i=0;i<=m;i++)dp[i][0]=i;for(let j=0;j<=n;j++)dp[0][j]=j;
steps.push({stage:'base',msg:'边界: dp[i][0]=i(删除), dp[0][j]=j(插入)',dp:dp.map(r=>[...r]),row:-1,col:-1});
for(let i=1;i<=m;i++) for(let j=1;j<=n;j++){
if(a[i-1]===b[j-1]){dp[i][j]=dp[i-1][j-1];steps.push({stage:'match',msg:`w1[${i-1}]='${a[i-1]}'==w2[${j-1}]='${b[j-1]}'→dp=${dp[i][j]}`,dp:dp.map(r=>[...r]),row:i,col:j,op:'match'});}
else{const ins=dp[i][j-1]+1,del=dp[i-1][j]+1,rep=dp[i-1][j-1]+1;dp[i][j]=Math.min(ins,del,rep);
let opN=rep<=Math.min(ins,del)?'替换':(ins<=del?'插入':'删除');
steps.push({stage:'op',msg:`'${a[i-1]}'≠'${b[j-1]}'→插=${ins}删=${del}替=${rep}→${dp[i][j]}(${opN})`,dp:dp.map(r=>[...r]),row:i,col:j,op:opN});}
}
const ops=[];let ti=m,tj=n;while(ti>0||tj>0){if(ti>0&&tj>0&&a[ti-1]===b[tj-1]){ti--;tj--;}else if(ti>0&&tj>0&&dp[ti][tj]===dp[ti-1][tj-1]+1){ops.unshift(`替换 '${a[ti-1]}'→'${b[tj-1]}'`);ti--;tj--;}else if(tj>0&&dp[ti][tj]===dp[ti][tj-1]+1){ops.unshift(`插入 '${b[tj-1]}'`);tj--;}else{ops.unshift(`删除 '${a[ti-1]}'`);ti--;}}
steps.push({stage:'done',msg:`编辑距离=${dp[m][n]}`,dp:dp.map(r=>[...r]),row:m,col:n,ops:[...ops]});
}
function render(step) {
const s=steps[step];
let viz='<div style="overflow-x:auto;"><table style="border-collapse:collapse;font-size:14px;">';
viz+='<tr><td style="border:1px solid #e2e8f0;padding:6px 10px;background:#f1f5f9;"></td><td style="border:1px solid #e2e8f0;padding:6px 10px;background:#f1f5f9;">∅</td>';
for(let j=0;j<n;j++) viz+=`<td style="border:1px solid #e2e8f0;padding:6px 10px;background:#f1f5f9;">${str2[j]}</td>`;
viz+='</tr>';
for(let i=0;i<=m;i++){viz+='<tr>';viz+=`<td style="border:1px solid #e2e8f0;padding:6px 10px;background:#f1f5f9;">${i===0?'∅':str1[i-1]}</td>`;
for(let j=0;j<=n;j++){let bg='white',fw='normal';if(s.row===i&&s.col===j){bg='#fef3c7';fw='bold';}else if(s.dp[i][j]>0&&!(i===0&&j===0)){bg='#ecfdf5';}else if(i===0||j===0){bg='#f1f5f9';}
const ic=s.row===i&&s.col===j&&s.op==='match'?' ✅':s.row===i&&s.col===j&&s.op==='替换'?' 🔄':s.row===i&&s.col===j&&s.op==='插入'?' ➕':s.row===i&&s.col===j&&s.op==='删除'?' ➖':'';
viz+=`<td style="border:1px solid #e2e8f0;padding:6px 10px;text-align:center;background:${bg};font-weight:${fw};min-width:40px;">${s.dp[i][j]}${ic}</td>`;}
viz+='</tr>';}
viz+='</table></div><div style="margin-top:10px;display:flex;gap:12px;font-size:12px;color:#64748b;"><span>✅相同</span><span>🔄替换</span><span>➕插入</span><span>➖删除</span></div>';
$('vizArea').innerHTML=viz;
let detail='<div class="calc-block">'+s.msg+'</div>';if(s.ops){detail+='<div style="margin-top:8px;"><b>操作:</b></div>';s.ops.forEach((o,i)=>{detail+=`<div>${i+1}. ${o}</div>`;});}$('detailContent').innerHTML=detail;
if(s.stage==='done') $('resultContent').innerHTML=`<div class="final-answer">编辑距离=<b>${s.dp[m][n]}</b><br>${s.ops.join(' → ')}</div>`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','base→边界','match→匹配','op→操作','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.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='word1="horse", word2="ros"';buildSteps(examples[0].s1,examples[0].s2);
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=()=>{const v=$('inputArea').value.match(/word1\s*=\s*"([^"]+)".*word2\s*=\s*"([^"]+)"/);if(!v){alert('格式: word1="horse", word2="ros"');return;}buildSteps(v[1],v[2]);stepCtrl.setSteps(steps.map((_,i)=>i));render(0);};
$('exampleSelect').onchange=()=>{const e=examples[parseInt($('exampleSelect').value)];$('inputArea').value=`word1="${e.s1}", word2="${e.s2}"`;buildSteps(e.s1,e.s2);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 minDistance(word1, word2):
m, n = len(word1), len(word2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1): dp[i][0] = i
for j in range(n+1): dp[0][j] = j
for i in range(1, m+1):
for j in range(1, n+1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
return dp[m][n]`, {lang:'Python'});
'''
# ========== 技巧 ==========
def js_single_number():
return r'''
const examples = [
{input:[2,2,1], label:'示例1: [2,2,1]'},
{input:[4,1,2,1,2], label:'示例2: [4,1,2,1,2]'},
{input:[1], label:'示例3: [1]'},
];
let nums, steps, stepCtrl;
function toBin(n,bits){return(n>>>0).toString(2).padStart(bits,'0');}
function buildSteps(arr) {
nums=[...arr];steps=[];let xor=0;const bits=Math.max(4,Math.ceil(Math.log2(Math.max(...arr)+1)));
steps.push({stage:'init',msg:'a⊕a=0, a⊕0=a,异或消除成对数',idx:-1,xorResult:0,xorBin:toBin(0,bits)});
for(let i=0;i<arr.length;i++){const prev=xor;xor^=arr[i];steps.push({stage:'xor',msg:`${prev} ⊕ ${arr[i]} = ${toBin(prev,bits)} ⊕ ${toBin(arr[i],bits)} = ${toBin(xor,bits)} = ${xor}`,idx:i,xorResult:xor,xorBin:toBin(xor,bits),prevBin:toBin(prev,bits),curBin:toBin(arr[i],bits)});}
steps.push({stage:'done',msg:`结果=${xor}`,idx:-1,xorResult:xor,xorBin:toBin(xor,bits)});
}
function render(step) {
const s=steps[step];const hl={};if(s.idx>=0)hl[s.idx]='orange';for(let i=0;i<s.idx;i++)hl[i]='blue';
let viz=renderArray(nums,{highlights:hl});
viz+=`<div style="margin-top:12px;padding:12px;background:#f8fafc;border-radius:8px;"><div>XOR = <b style="color:var(--blue);font-size:18px;">${s.xorResult}</b></div>`;
if(s.stage==='xor'){viz+=`<div style="display:flex;gap:8px;align-items:center;font-family:monospace;margin-top:8px;"><div style="padding:6px 12px;background:#e0e7ff;border-radius:6px;">${s.prevBin}</div><div style="font-weight:700;">⊕</div><div style="padding:6px 12px;background:#fef3c7;border-radius:6px;">${s.curBin}</div><div style="font-weight:700;">=</div><div style="padding:6px 12px;background:#dcfce7;border-radius:6px;font-weight:700;">${s.xorBin}</div></div><div style="margin-top:6px;font-size:12px;color:#64748b;">逐位异或:相同为0,不同为1</div>`;}
viz+='</div>';$('vizArea').innerHTML=viz;
$('detailContent').innerHTML='<div class="calc-block">'+s.msg+'</div>'+`<div class="current-answer">XOR = <b>${s.xorResult}</b></div>`;
if(s.stage==='done') $('resultContent').innerHTML=`<div class="final-answer">只出现一次的数字 = <b>${s.xorResult}</b></div>`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','xor→异或','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.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='[2,2,1]';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 a=JSON.parse($('inputArea').value);buildSteps(a);stepCtrl.setSteps(steps.map((_,i)=>i));render(0);}catch(e){alert('请输入合法JSON数组');}};
$('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 singleNumber(nums):
result = 0
for num in nums:
result ^= num
return result`, {lang:'Python'});
'''
def js_majority_element():
return r'''
const examples = [
{input:[3,2,3], label:'示例1: [3,2,3]'},
{input:[2,2,1,1,1,2,2], label:'示例2: [2,2,1,1,1,2,2]'},
{input:[1], label:'示例3: [1]'},
];
let nums, steps, stepCtrl;
function buildSteps(arr) {
nums=[...arr];steps=[];let c=null,cnt=0;const hist=[];
steps.push({stage:'init',msg:'Boyer-Moore投票:同+1异-1,为0换人',idx:-1,candidate:null,count:0,history:[]});
for(let i=0;i<arr.length;i++){
if(cnt===0){c=arr[i];cnt=1;hist.push({i,v:arr[i],c:cnt,a:'换'});steps.push({stage:'change',msg:`计数器0→换候选者为${arr[i]}`,idx:i,candidate:c,count:cnt,history:[...hist]});}
else if(arr[i]===c){cnt++;hist.push({i,v:arr[i],c:cnt,a:'同'});steps.push({stage:'same',msg:`${arr[i]}==${c}→+1 count=${cnt}`,idx:i,candidate:c,count:cnt,history:[...hist]});}
else{cnt--;hist.push({i,v:arr[i],c:cnt,a:'异'});steps.push({stage:'diff',msg:`${arr[i]}≠${c}→-1 count=${cnt}`,idx:i,candidate:c,count:cnt,history:[...hist]});}
}
steps.push({stage:'done',msg:`多数元素=${c}`,idx:-1,candidate:c,count:cnt,history:[...hist]});
}
function render(step) {
const s=steps[step];const hl={};if(s.idx>=0)hl[s.idx]='orange';for(let i=0;i<=Math.min(s.idx,nums.length-1);i++){if(nums[i]===s.candidate&&!hl[i])hl[i]='green';}
let viz=renderArray(nums,{highlights:hl});
viz+=`<div style="margin-top:12px;padding:12px;background:#f8fafc;border-radius:8px;display:flex;gap:16px;align-items:center;"><div><b>候选者:</b><span style="font-size:24px;font-weight:700;color:var(--blue);">${s.candidate!==null?s.candidate:'—'}</span></div><div><b>计数:</b><div style="display:flex;align-items:center;gap:4px;"><div style="width:${Math.max(0,s.count*28)}px;height:20px;background:var(--blue);border-radius:4px;"></div><span style="font-weight:700;">${s.count}</span></div></div></div>`;
if(s.history&&s.history.length>0){viz+='<div style="margin-top:12px;"><b>投票历史:</b></div><div style="display:flex;align-items:flex-end;gap:4px;height:60px;margin-top:4px;">';const mx=Math.max(...s.history.map(h=>Math.abs(h.c)),1);s.history.forEach(h=>{const pct=Math.max(4,(Math.abs(h.c)/mx)*50);const bg=h.a==='换'?'var(--orange)':h.a==='同'?'var(--green)':'var(--red)';viz+=`<div style="width:28px;height:${pct}px;background:${bg};border-radius:3px 3px 0 0;"></div>`;});viz+='</div><div style="display:flex;gap:4px;">';s.history.forEach(h=>{viz+=`<div style="width:28px;text-align:center;font-size:9px;">${h.v}</div>`;});viz+='</div>';}
$('vizArea').innerHTML=viz;
$('detailContent').innerHTML='<div class="calc-block">'+s.msg+'</div>'+`<div class="current-answer">候选:<b>${s.candidate}</b> 计数:<b>${s.count}</b></div>`;
if(s.stage==='done') $('resultContent').innerHTML=`<div class="final-answer">多数元素=<b>${s.candidate}</b></div>`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','change→更换','same→相同','diff→不同','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.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='[3,2,3]';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 a=JSON.parse($('inputArea').value);buildSteps(a);stepCtrl.setSteps(steps.map((_,i)=>i));render(0);}catch(e){alert('请输入合法JSON数组');}};
$('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 majorityElement(nums):
candidate = None
count = 0
for num in nums:
if count == 0:
candidate = num
count += (1 if num == candidate else -1)
return candidate`, {lang:'Python'});
'''
def js_sort_colors():
return r'''
const examples = [
{input:[2,0,2,1,1,0], label:'示例1: [2,0,2,1,1,0]'},
{input:[2,0,1], label:'示例2: [2,0,1]'},
{input:[0,0,1,1,2,2], label:'示例3: 已排序'},
];
let nums, steps, stepCtrl;
const CM={0:['#fee2e2','#ef4444'],1:['#fef9c3','#eab308'],2:['#dbeafe','#3b82f6']};
function buildSteps(arr) {
nums=[...arr];steps=[];let lo=0,mid=0,hi=arr.length-1;
steps.push({stage:'init',msg:'荷兰国旗三指针: lo左全0, mid~hi待处理, hi右全2',arr:[...nums],lo,mid,hi});
while(mid<=hi){
if(nums[mid]===0){steps.push({stage:'check0',msg:`nums[${mid}]=0→交换lo=${lo}`,arr:[...nums],lo,mid,hi});[nums[lo],nums[mid]]=[nums[mid],nums[lo]];lo++;mid++;steps.push({stage:'swap',msg:`→[${nums}] lo=${lo} mid=${mid}`,arr:[...nums],lo,mid,hi});}
else if(nums[mid]===1){steps.push({stage:'skip',msg:`nums[${mid}]=1→跳过`,arr:[...nums],lo,mid,hi});mid++;steps.push({stage:'advance',msg:`mid→${mid}`,arr:[...nums],lo,mid,hi});}
else{steps.push({stage:'check2',msg:`nums[${mid}]=2→交换hi=${hi}`,arr:[...nums],lo,mid,hi});[nums[mid],nums[hi]]=[nums[hi],nums[mid]];hi--;steps.push({stage:'swap',msg:`→[${nums}] hi=${hi}`,arr:[...nums],lo,mid,hi});}
}
steps.push({stage:'done',msg:`完成: [${nums}]`,arr:[...nums],lo,mid,hi});
}
function render(step) {
const s=steps[step];
let viz='<div class="nums-line">';s.arr.forEach((v,i)=>{const[bg,fg]=CM[v];let bd='2px solid transparent';if(i===s.lo)bd='2px solid var(--blue)';else if(i===s.mid&&s.mid<=s.hi)bd='2px solid var(--green)';else if(i===s.hi)bd='2px solid var(--purple)';viz+=`<span class="chip-group"><span class="chip" style="background:${bg};color:${fg};border:${bd};min-width:40px;">${v}</span><span class="chip-index">${i}</span></span>`;});viz+='</div>';
viz+='<div class="nums-line" style="margin-top:-4px;">';s.arr.forEach((v,i)=>{let lb='';if(i===s.lo)lb='lo';if(i===s.mid&&s.mid<=s.hi)lb=lb?lb+'/mid':'mid';if(i===s.hi)lb=lb?lb+'/hi':'hi';viz+=`<span style="min-width:40px;text-align:center;font-size:10px;font-weight:700;color:${i===s.lo?'var(--blue)':i===s.mid&&s.mid<=s.hi?'var(--green)':i===s.hi?'var(--purple)':'transparent'};">${lb}</span>`;});viz+='</div>';
viz+='<div style="margin-top:8px;font-size:12px;display:flex;gap:12px;"><span style="color:#ef4444;">■0红</span><span style="color:#eab308;">■1白</span><span style="color:#3b82f6;">■2蓝</span></div>';
$('vizArea').innerHTML=viz;$('detailContent').innerHTML='<div class="calc-block">'+s.msg+'</div>';
if(s.stage==='done') $('resultContent').innerHTML=`<div class="final-answer">排序=<b>[${s.arr}]</b></div>`;
else $('resultContent').innerHTML=`<div class="current-answer">[${s.arr}] lo=${s.lo} mid=${s.mid} hi=${s.hi}</div>`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','check0→0','check2→2','swap→交换','skip→跳过','advance→前进','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.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='[2,0,2,1,1,0]';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 a=JSON.parse($('inputArea').value);buildSteps(a);stepCtrl.setSteps(steps.map((_,i)=>i));render(0);}catch(e){alert('请输入0,1,2的JSON数组');}};
$('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 sortColors(nums):
lo, mid, hi = 0, 0, len(nums) - 1
while mid <= hi:
if nums[mid] == 0:
nums[lo], nums[mid] = nums[mid], nums[lo]
lo += 1; mid += 1
elif nums[mid] == 1:
mid += 1
else:
nums[mid], nums[hi] = nums[hi], nums[mid]
hi -= 1`, {lang:'Python'});
'''
def js_next_permutation():
return r'''
const examples = [
{input:[1,2,3], label:'示例1: [1,2,3]'},
{input:[3,2,1], label:'示例2: [3,2,1]'},
{input:[1,1,5], label:'示例3: [1,1,5]'},
{input:[1,3,2], label:'示例4: [1,3,2]'},
];
let nums, steps, stepCtrl;
function buildSteps(arr) {
nums=[...arr];steps=[];const n=nums.length;
steps.push({stage:'init',msg:'①找下降点 ②找交换目标 ③反转后缀',arr:[...nums],phase:0,i:-1,j:-1});
let i=n-2;while(i>=0&&nums[i]>=nums[i+1])i--;
if(i<0){steps.push({stage:'no_desc',msg:'降序=最大排列,直接反转',arr:[...nums],phase:1,i:-1,j:-1});}
else{steps.push({stage:'found_desc',msg:`下降点: nums[${i}]=${nums[i]} < nums[${i+1}]=${nums[i+1]}`,arr:[...nums],phase:1,i,j:-1});
let j=n-1;while(nums[j]<=nums[i])j--;steps.push({stage:'found_swap',msg:`交换目标: nums[${j}]=${nums[j]}`,arr:[...nums],phase:2,i,j});
[nums[i],nums[j]]=[nums[j],nums[i]];steps.push({stage:'swap',msg:`交换→[${nums}]`,arr:[...nums],phase:2,i,j});}
let left=i+1,right=n-1;while(left<right){[nums[left],nums[right]]=[nums[right],nums[left]];steps.push({stage:'reverse',msg:`反转: swap[${left}],[${right}]→[${nums}]`,arr:[...nums],phase:3,i,j:left,k:right});left++;right--;}
steps.push({stage:'done',msg:`下一个排列[${nums}]`,arr:[...nums],phase:4,i:-1,j:-1});
}
function render(step) {
const s=steps[step];const hl={};
if(s.i>=0)hl[s.i]='orange';if(s.j>=0&&s.stage!=='reverse')hl[s.j]='purple';
if(s.stage==='reverse'){if(s.j>=0)hl[s.j]='blue';if(s.k>=0)hl[s.k]='blue';}
if(s.stage==='done'||s.stage==='reverse'){const ds=s.i>=0?s.i+1:0;for(let k=ds;k<s.arr.length;k++)if(!hl[k])hl[k]='green';}
let viz=renderArray(s.arr,{highlights:hl});
const phases=['①找下降点','②找交换','③反转','③反转','✅完成'];
viz+=`<div style="margin-top:10px;"><b>阶段:</b>${phases[Math.min(s.phase,4)]}</div>`;
$('vizArea').innerHTML=viz;$('detailContent').innerHTML='<div class="calc-block">'+s.msg+'</div>';
if(s.stage==='done') $('resultContent').innerHTML=`<div class="final-answer">下一个排列=<b>[${s.arr}]</b></div>`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','found_desc→下降点','found_swap→目标','swap→交换','reverse→反转','no_desc→无下降','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.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='[1,2,3]';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 a=JSON.parse($('inputArea').value);buildSteps(a);stepCtrl.setSteps(steps.map((_,i)=>i));render(0);}catch(e){alert('请输入合法JSON数组');}};
$('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 nextPermutation(nums):
i = len(nums) - 2
while i >= 0 and nums[i] >= nums[i+1]:
i -= 1
if i >= 0:
j = len(nums) - 1
while nums[j] <= nums[i]:
j -= 1
nums[i], nums[j] = nums[j], nums[i]
left, right = i + 1, len(nums) - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1; right -= 1`, {lang:'Python'});
'''
def js_find_the_duplicate_number():
return r'''
const examples = [
{input:[1,3,4,2,2], label:'示例1: [1,3,4,2,2]'},
{input:[3,1,3,4,2], label:'示例2: [3,1,3,4,2]'},
{input:[3,3,3,3,3], label:'示例3: [3,3,3,3,3]'},
];
let nums, steps, stepCtrl;
function buildSteps(arr) {
nums=[...arr];steps=[];
steps.push({stage:'init',msg:'数组→链表 f(x)=nums[x],必有环!快慢指针找环入口=重复数',arr:[...nums],slow:0,fast:0,phase:0});
let slow=0,fast=0;
steps.push({stage:'start',msg:'起点0',arr:[...nums],slow:0,fast:0,phase:1});
do{slow=arr[slow];fast=arr[arr[fast]];steps.push({stage:'move',msg:`慢→${slow} 快→${fast}`,arr:[...nums],slow,fast,phase:1});}while(slow!==fast);
steps.push({stage:'meet',msg:`相遇于${slow}!`,arr:[...nums],slow,fast,phase:1});
let p1=0,p2=slow;steps.push({stage:'phase2',msg:'阶段2:从0和相遇点同速走',arr:[...nums],slow:p1,fast:p2,phase:2});
while(p1!==p2){p1=arr[p1];p2=arr[p2];steps.push({stage:'seek',msg:`ptr1→${p1} ptr2→${p2}`,arr:[...nums],slow:p1,fast:p2,phase:2});}
steps.push({stage:'done',msg:`环入口=${p1}即重复数`,arr:[...nums],slow:p1,fast:p2,phase:3});
}
function render(step) {
const s=steps[step];
let viz='<div style="font-weight:600;color:#64748b;margin-bottom:8px;">数组→链表:</div><div class="nums-line">';
s.arr.forEach((v,i)=>{viz+=`<span class="chip-group"><span class="chip ${i===s.slow||i===s.fast?'purple':'default'}" style="min-width:40px;">${v}</span><span class="chip-index">${i}</span></span>`;});viz+='</div>';
viz+=`<div style="font-size:12px;color:#64748b;margin-top:4px;">f(index) = nums[index]</div>`;
viz+=`<div style="margin-top:12px;padding:12px;background:#f8fafc;border-radius:8px;">`;
if(s.phase===1){viz+=`<div><b>🏃慢:</b><span style="color:var(--blue);font-weight:700;font-size:18px;">${s.slow}</span></div><div><b>🐇快:</b><span style="color:var(--purple);font-weight:700;font-size:18px;">${s.fast}</span></div>`;}
else if(s.phase===2){viz+=`<div><b>📍ptr1:</b><span style="color:var(--blue);font-weight:700;font-size:18px;">${s.slow}</span></div><div><b>📍ptr2:</b><span style="color:var(--purple);font-weight:700;font-size:18px;">${s.fast}</span></div>`;}
else if(s.phase===3){viz+=`<div style="font-size:20px;font-weight:700;color:var(--green);">🎯重复数=${s.slow}</div>`;}
viz+='</div>';
if(s.phase>=1){viz+='<div style="margin-top:12px;"><b>链表路径:</b></div><div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px;align-items:center;">';const path=[0];let cur=0;const seen=new Set([0]);for(let k=0;k<nums.length+2;k++){cur=nums[cur];path.push(cur);if(seen.has(cur))break;seen.add(cur);}
path.forEach((node,idx)=>{const isS=node===s.slow,isF=node===s.fast;let bg='#f1f5f9',bd='2px solid #e2e8f0';if(isS&&isF){bg='#dcfce7';bd='2px solid #16a34a';}else if(isS){bg='#dbeafe';bd='2px solid #3b82f6';}else if(isF){bg='#f3e8ff';bd='2px solid #8b5cf6';}
viz+=`<div style="padding:6px 12px;background:${bg};border:${bd};border-radius:8px;font-weight:700;">${node}</div>`;if(idx<path.length-1)viz+=`<div style="color:#94a3b8;">→</div>`;});viz+='</div>';}
$('vizArea').innerHTML=viz;
const pn=['准备','阶段1:找相遇','阶段2:找环入口','完成'];
$('detailContent').innerHTML='<div class="calc-block">'+s.msg+'</div>'+`<div class="current-answer">${pn[s.phase]}</div>`;
if(s.stage==='done') $('resultContent').innerHTML=`<div class="final-answer">重复数=<b>${s.slow}</b></div>`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','start→出发','move→移动','meet→相遇','phase2→阶段2','seek→寻找','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.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='[1,3,4,2,2]';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 a=JSON.parse($('inputArea').value);buildSteps(a);stepCtrl.setSteps(steps.map((_,i)=>i));render(0);}catch(e){alert('请输入1~n的n+1个数的JSON数组');}};
$('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 findDuplicate(nums):
slow = fast = 0
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast: break
slow = 0
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return slow`, {lang:'Python'});
'''
# ========== Stubs for future algorithms ==========
# ========== Problem → JS generator mapping ==========
ALGO_MAP = {
'two-sum': js_two_sum,
'group-anagrams': js_group_anagrams,
'longest-consecutive-sequence': js_longest_consecutive,
'move-zeros': js_move_zeros,
'container-with-most-water': js_container_with_most_water,
'3sum': js_3sum,
'trapping-rain-water': js_trapping_rain_water,
# 图论
'number-of-islands': js_number_of_islands,
'rotting-oranges': js_rotting_oranges,
'course-schedule': js_course_schedule,
'implement-trie-prefix-tree': js_implement_trie_prefix_tree,
# 回溯
'permutations': js_permutations,
'subsets': js_subsets,
'letter-combinations-of-a-phone-number': js_letter_combinations_of_a_phone_number,
'combination-sum': js_combination_sum,
'generate-parentheses': js_generate_parentheses,
'word-search': js_word_search,
'palindrome-partitioning': js_palindrome_partitioning,
'n-queens': js_n_queens,
# 链表
'intersection-of-two-linked-lists': js_intersection_of_two_linked_lists,
'reverse-linked-list': js_reverse_linked_list,
'palindrome-linked-list': js_palindrome_linked_list,
'linked-list-cycle': js_linked_list_cycle,
'linked-list-cycle-ii': js_linked_list_cycle_ii,
'merge-two-sorted-lists': js_merge_two_sorted_lists,
'add-two-numbers': js_add_two_numbers,
'remove-nth-node-from-end-of-list': js_remove_nth_node_from_end_of_list,
'swap-nodes-in-pairs': js_swap_nodes_in_pairs,
'reverse-nodes-in-k-group': js_reverse_nodes_in_k_group,
'copy-list-with-random-pointer': js_copy_list_with_random_pointer,
'sort-list': js_sort_list,
'merge-k-sorted-lists': js_merge_k_sorted_lists,
'lru-cache': js_lru_cache,
# 二叉树
'binary-tree-inorder-traversal': js_binary_tree_inorder_traversal,
'maximum-depth-of-binary-tree': js_maximum_depth_of_binary_tree,
'invert-binary-tree': js_invert_binary_tree,
'symmetric-tree': js_symmetric_tree,
'diameter-of-binary-tree': js_diameter_of_binary_tree,
'binary-tree-level-order-traversal': js_binary_tree_level_order_traversal,
'convert-sorted-array-to-bst': js_convert_sorted_array_to_bst,
'validate-binary-search-tree': js_validate_binary_search_tree,
'kth-smallest-element-in-a-bst': js_kth_smallest_element_in_a_bst,
'binary-tree-right-side-view': js_binary_tree_right_side_view,
'flatten-binary-tree-to-linked-list': js_flatten_binary_tree_to_linked_list,
'construct-binary-tree-from-preorder-and-inorder': js_construct_binary_tree_from_preorder_and_inorder,
'path-sum-iii': js_path_sum_iii,
'lowest-common-ancestor-of-a-binary-tree': js_lowest_common_ancestor_of_a_binary_tree,
'binary-tree-maximum-path-sum': js_binary_tree_maximum_path_sum,
# 多维动态规划
'unique-paths': js_unique_paths,
'minimum-path-sum': js_minimum_path_sum,
'longest-palindromic-substring': js_longest_palindromic_substring,
'longest-common-subsequence': js_longest_common_subsequence,
'edit-distance': js_edit_distance,
# 技巧
'single-number': js_single_number,
'majority-element': js_majority_element,
'sort-colors': js_sort_colors,
'next-permutation': js_next_permutation,
'find-the-duplicate-number': js_find_the_duplicate_number,
# 动态规划
'climbing-stairs': js_climbing_stairs,
'pascals-triangle': js_pascals_triangle,
'house-robber': js_house_robber,
'perfect-squares': js_perfect_squares,
'coin-change': js_coin_change,
'word-break': js_word_break,
'longest-increasing-subsequence': js_longest_increasing_subsequence,
'maximum-product-subarray': js_maximum_product_subarray,
'partition-equal-subset-sum': js_partition_equal_subset_sum,
'longest-valid-parentheses': js_longest_valid_parentheses,
}
# ========== Generate a generic template for problems without specific JS ==========
def js_generic():
return r'''
const examples = [{input: [], label: '默认示例'}]; // TODO
let steps, stepCtrl;
function buildSteps() {
steps = [];
steps.push({stage:'start', msg:'算法开始执行...'});
steps.push({stage:'done', msg:'算法执行完毕'});
}
function render(step) {
const s = steps[step];
$('vizArea').innerHTML = '<div class="formula-box">' + s.msg + '</div>';
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
$('resultContent').innerHTML = '<div class="final-answer">完成!请参考代码实现。</div>';
}
$('hintText').textContent = s.msg;
}
function init() {
buildSteps();
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 = () => { buildSteps(); 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(`# TODO: 待补充`, {lang:'Python'});
'''
def generate_problem(p):
slug = p['slug']
gen_fn = ALGO_MAP.get(slug)
if gen_fn:
algo_js = gen_fn()
else:
algo_js = js_generic()
html = gen_html(p, algo_js)
out_dir = os.path.join(BASE, slug)
os.makedirs(out_dir, exist_ok=True)
out_path = os.path.join(out_dir, 'index.html')
with open(out_path, 'w', encoding='utf-8') as f:
f.write(html)
return out_path
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--all', action='store_true')
parser.add_argument('--category', type=str, default=None)
parser.add_argument('--slugs', type=str, default=None)
args = parser.parse_args()
targets = ALL_PROBLEMS
if args.category:
targets = [p for p in targets if p['category'] == args.category]
elif args.slugs:
slug_list = [s.strip() for s in args.slugs.split(',')]
targets = [p for p in targets if p['slug'] in slug_list]
elif not args.all:
print("请指定 --all, --category, 或 --slugs")
sys.exit(1)
for p in targets:
path = generate_problem(p)
print(f'✅ {p["slug"]:50s} → {path}')
print(f'\n生成完成!共 {len(targets)} 个页面')
if __name__ == '__main__':
main()