#!/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'''
{did}. {title} – 图解
{diff_icon} {did}. {title} {d}
分类:{cat} | LeetCode Hot 100
输入:
生成图解
◀ 上一步
下一步 ▶
⏭ 跳到结果
自动播放
重置
Powered by QwenPaw · 图解算法 · LeetCode Hot 100
'''
# ========== 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 += 'complement = ' + s.complement + '
';
}
$('vizArea').innerHTML = viz;
let detail = '' + s.msg + '
';
if (s.mapKeys && s.mapKeys.length > 0) {
detail += '哈希表: ';
s.mapKeys.forEach(([k,v]) => { detail += `${k}→${v} `; });
detail += '
';
}
$('detailContent').innerHTML = detail;
if (s.stage === 'found') {
$('resultContent').innerHTML = `返回 [${s.result}] nums[${s.result[0]}] + nums[${s.result[1]}] = ${nums[s.result[0]]} + ${nums[s.result[1]]} = ${target}
`;
} else if (s.stage === 'done') {
$('resultContent').innerHTML = '未找到答案
';
}
$('hintText').textContent = s.msg;
const stages = ['start→开始','check→检查','store→存入','found→找到'];
$('pipeline').innerHTML = stages.map(st => {
const [key, label] = st.split('→');
return `${label} `;
}).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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 = '';
strs.forEach((st,i) => {
const cls = s.idx===i ? (s.stage==='group'?'green':'orange') : 'default';
viz += `"${st}" `;
});
viz += '
';
if (s.current) viz += `当前: "${s.current}" → key: "${s.key}"
`;
$('vizArea').innerHTML = viz;
let detail = '' + s.msg + '
';
detail += '分组结果:
';
Object.entries(s.groups).forEach(([k,v]) => {
detail += `${k} → [${v.map(x=>`"${x}"`).join(', ')}]
`;
});
$('detailContent').innerHTML = detail;
if (s.stage === 'done') {
const result = Object.values(s.groups);
$('resultContent').innerHTML = `返回 ${JSON.stringify(result)} 共 ${result.length} 个分组
`;
}
$('hintText').textContent = s.msg;
const stages = ['start→开始','sort→排序','group→分组','done→完成'];
$('pipeline').innerHTML = stages.map(st => {
const [k,l] = st.split('→');
return `${l} `;
}).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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 = '排序去重后:
';
viz += renderArray(s.sorted, {highlights: hl});
if (s.streakNums && s.streakNums.length > 0)
viz += '连续序列: [' + s.streakNums.join(', ') + '] 长度=' + s.streak + '
';
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.best > 0) $('detailContent').innerHTML += `当前最长:${s.best}
`;
if (s.stage === 'done') $('resultContent').innerHTML = `最长连续序列长度 = ${s.best}
`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','start_seq→起点','extend→延伸','end_seq→结束','done→完成'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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 = '' + s.msg + '
';
if (s.stage === 'done') $('resultContent').innerHTML = `结果:[${s.arr}]
`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','check→检查','swap→交换','advance→前进','done→完成'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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 = '';
height.forEach((h,i) => {
const pct = (h / maxH * 100); const bg = i===s.left||i===s.right ? 'var(--blue)' : '#cbd5e1';
viz += `
${h}
`;
});
viz += '
';
viz += `💧 最大面积 = ${s.maxArea}
`;
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage === 'done') $('resultContent').innerHTML = `最大盛水量 = ${s.maxArea}
`;
$('hintText').textContent = s.msg;
const stages = ['init→初始化','calc→计算','move→移动','done→完成'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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 += `和 = ${s.currentSum}
`;
$('vizArea').innerHTML = viz;
let detail = '' + s.msg + '
';
if (s.triplets.length > 0) { s.triplets.forEach((t,i)=>{ detail += `${i+1}. [${t.join(', ')}]
`; }); }
$('detailContent').innerHTML = detail;
if (s.stage === 'done') {
if (s.triplets.length > 0) $('resultContent').innerHTML = `返回 ${JSON.stringify(s.triplets)}
`;
else $('resultContent').innerHTML = '未找到
';
}
$('hintText').textContent = s.msg;
const stages = ['sort→排序','fix→固定','calc→计算','found→找到','done→完成'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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 = '';
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 += `
`;
if (waterH > 0) viz += `
`;
viz += `
${h>0?h:''}
`;
viz += `
${i}
`;
});
viz += '
';
viz += `■ left_max=${s.leftMax} ■ right_max=${s.rightMax} 💧 储水=${s.totalWater}
`;
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage === 'done') $('resultContent').innerHTML = `总储水量 = ${s.totalWater}
`;
$('hintText').textContent = s.msg;
const stages = ['init→初始化','calcL→左端','calcR→右端','done→完成'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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 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=0 && nc {
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 += `🏝️ 岛屿数量:${s.islands}
`;
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage==='done') {
let det = `岛屿数量 = ${s.islands}
`;
det += '';
for (let i=1; i<=s.islands; i++) det += ` 岛屿 #${i} `;
det += '
';
$('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 `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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]), 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=0 && nr=0 && nc 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 += '';
viz += '🟠 腐烂 ';
viz += '🟢 新鲜 ';
viz += '⬜ 空 ';
viz += `⏱ 分钟=${s.minutes} `;
viz += '
';
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage==='done') $('resultContent').innerHTML = `经过 ${s.minutes} 分钟,所有橘子腐烂
`;
if (s.stage==='impossible') $('resultContent').innerHTML = `返回 -1 (有新鲜橘子无法腐烂)
`;
$('hintText').textContent = s.msg;
const stages = ['init→初始化','rot→腐烂传播','done→完成','impossible→不可能'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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[]);
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${i}${stateText[s.state[i]]} `;
}
viz += '';
viz += '依赖关系:
';
for (const [a,b] of edges) viz += `${a}←${b} `;
if (s.topo.length > 0) viz += `拓扑序: ${s.topo.join(' → ')}
`;
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage==='result') {
if (s.hasCycle) $('resultContent').innerHTML = '返回 False (检测到环,无法完成)
';
else $('resultContent').innerHTML = `返回 True (可以完成所有课程) 拓扑序: ${s.topo.join(' → ')}
`;
}
$('hintText').textContent = s.msg;
const stages = ['init→初始化','visiting→访问中','done→完成','cycle→检测到环','result→结果'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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${node.char==='ROOT'?'⊘':node.char}
${node.isEnd?'● ':''}`;
const children = childKeys.map(k => buildTree(node.children[k]));
return `${node.char==='ROOT'?'⊘':node.char}
${node.isEnd?'
● ':''}
${children.join('')}
`;
}
return '' + buildTree(0) + '
';
}
function render(step) {
const s = steps[step];
let viz = renderTrie(s.nodes, s.path, s.currentNode);
viz += `当前操作: ${s.op}
`;
viz += '● 单词结尾 当前路径
';
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage==='done') {
$('resultContent').innerHTML = '所有操作完成 ✓
';
}
$('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 `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
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;ifirst = ${s.first >= 0 ? s.first : '完成'}`;
if (s.result.length > 0) {
viz += '已找到:
';
s.result.forEach(r => { viz += `[${r}] `; });
viz += '
';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage==='done') $('resultContent').innerHTML = `共 ${s.result.length} 个排列 ${s.result.map(r=>'['+r+']').join(', ')}
`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','try→尝试','swap→交换','collect→收集','undo→回溯','done→完成'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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 = '原始数组:
';
const hl = {};
if (s.choosing >= 0) hl[s.choosing] = 'orange';
for (let i=s.idx; i=s.idx) hl[i] = hl[i] || 'default';
viz += renderArray(nums, {highlights:hl});
viz += `当前路径: [${s.path.join(', ')}]
`;
if (s.result.length > 0) {
viz += '已找到:
';
s.result.forEach(r => { viz += `[${r.length?r.join(','):''}] `; });
viz += '
';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage==='done') $('resultContent').innerHTML = `共 ${s.result.length} 个子集
`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','skip→不选','choose→选择','collect→收集','undo→回溯','done→完成'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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 = '';
digits.split('').forEach((d,i) => {
const isCurr = i===s.digitIdx;
viz += `
`;
});
viz += '
';
viz += `当前路径: ${s.path.join('')} _`.repeat(Math.max(0, digits.length - s.path.length)) + '
';
if (s.result.length > 0) {
viz += '已找到:
';
s.result.forEach(r => { viz += `${r} `; });
viz += '
';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage==='done' && s.result.length > 0) $('resultContent').innerHTML = `共 ${s.result.length} 个组合 [${s.result.map(r=>'"'+r+'"').join(', ')}]
`;
if (s.stage==='done' && s.result.length===0) $('resultContent').innerHTML = '返回 []
';
$('hintText').textContent = s.msg;
const stages = ['start→开始','choose→选择字母','collect→收集','undo→回溯','done→完成'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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
当前组合: [${s.path.join(', ')}]
sum: ${s.sum}
remain: ${s.remain}
`;
if (s.result.length > 0) {
viz += '已找到:
';
s.result.forEach(r => { viz += `[${r.join(',')}] `; });
viz += '
';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage==='done') $('resultContent').innerHTML = `共 ${s.result.length} 个组合
`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','choose→选择','found→找到','prune→剪枝','undo→回溯','done→完成'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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})`;
const str = s.path.join('');
for (let i=0; i${ch}`;
}
viz += '_'.repeat(Math.max(0, 2*n - str.length)) + ' ';
viz += `
open = ${s.open} / ${n}
close = ${s.close} / ${s.open}
`;
// visual bar
viz += ``;
for (let i=0; i
`;
viz += `( `;
for (let i=0; i`;
viz += `) `;
if (s.result.length > 0) {
viz += '已找到:
';
s.result.forEach(r => { viz += `${r} `; });
viz += '
';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage==='done') $('resultContent').innerHTML = `共 ${s.result.length} 个组合 [${s.result.map(r=>'"'+r+'"').join(', ')}]
`;
$('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 `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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=0&&c { 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 {
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 += `搜索词: `;
for (let i=0; i${word[i]}`;
}
viz += '
';
if (s.path.length > 0) viz += `路径: ${s.path.map(p=>'('+p+')').join(' → ')}
`;
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage==='found') $('resultContent').innerHTML = `返回 True ,路径: ${s.path.map(p=>'('+p+')').join('→')}
`;
if (s.stage==='not_found') $('resultContent').innerHTML = '返回 False
';
$('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 `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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 = ``;
// 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
${part[j]}`;
pos++;
}
}
// remaining chars
for (let i=pos; i=st.checking.from && i<=st.checking.to;
viz += `${s[i]} `;
}
viz += ' ';
// path
viz += `当前分割: `;
if (st.path.length > 0) {
st.path.forEach((p,i) => { viz += `"${p}" `; });
} else viz += '空 ';
viz += '
';
if (st.result.length > 0) {
viz += '已找到:
';
st.result.forEach(r => { viz += `[${r.map(x=>'"'+x+'"').join(',')}] `; });
viz += '
';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + st.msg + '
';
if (st.stage==='done') $('resultContent').innerHTML = `共 ${st.result.length} 种分割方案
`;
$('hintText').textContent = st.msg;
const stages = ['start→开始','check→检查回文','choose→选择','collect→收集','undo→回溯','done→完成'];
$('pipeline').innerHTML = stages.map(stt => { const [k,l]=stt.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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 = ``;
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 += `
${content}
`;
}
}
viz += '
';
// Conflict lines for current try
if (s.stage === 'conflict' && s.row >= 0) {
viz += '';
for (let r=0; r
';
}
viz += `当前放置: ${s.queens.length > 0 ? s.queens.map((c,r)=>`行${r}=列${c}`).join(', ') : '无'}
`;
if (s.result.length > 0) {
viz += `已找到 ${s.result.length} 个解
`;
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage==='done') $('resultContent').innerHTML = `共 ${s.result.length} 个解
`;
$('hintText').textContent = s.msg;
const stages = ['start→开始','try_valid→尝试放置','conflict→冲突','collect→收集解','undo→回溯','done→完成'];
$('pipeline').innerHTML = stages.map(st => { const [k,l]=st.split('→'); return `${l} `; }).join('→ ');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `${e.label} `; });
$('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;i0 && !(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 = ''+s.msg+'
';
if (s.dp[m-1][n-1]>0) $('detailContent').innerHTML += `dp[${m-1}][${n-1}] = ${s.dp[m-1][n-1]}
`;
if (s.stage==='done') $('resultContent').innerHTML = `不同路径数 = ${s.dp[m-1][n-1]}
`;
$('hintText').textContent = s.msg;
const stages = ['init→初始化','fill→填充','done→完成'];
$('pipeline').innerHTML = stages.map(x=>{const[k,l]=x.split('→');return`${l} `;}).join('→ ');
}
function init() {
const sel=$('exampleSelect'); examples.forEach((e,i)=>{sel.innerHTML+=`${e.label} `;});
$('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[...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[...r]),row:0,col:j,pathCells:Array.from({length:j+1},(_,k)=>`0,${k}`)});}
for(let i=1;i[...r]),row:i,col:0,pathCells:Array.from({length:i+1},(_,k)=>`${k},0`)});}
for(let i=1;i[...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;i0&&!hl[`${i},${j}`]&&!(s.row===i&&s.col===j)) hl[`${i},${j}`]='visited';
let viz='原始网格:
'+renderGrid(grid,{cellSize:40});
viz+='DP 表:
'+renderGrid(s.dp,{highlights:hl,cellSize:44,cellStyle:(v)=>v===0?'color:#94a3b8;':'font-weight:700;'});
if(s.pathCells.length>1) viz+='路径: '+s.pathCells.map(p=>`(${p})`).join(' → ')+'
';
$('vizArea').innerHTML=viz;
$('detailContent').innerHTML=''+s.msg+'
';
if(s.dp[m-1]&&s.dp[m-1][n-1]>0) $('detailContent').innerHTML+=`dp[${m-1}][${n-1}] = ${s.dp[m-1][n-1]}
`;
if(s.stage==='done') $('resultContent').innerHTML=`最小路径和 = ${s.dp[m-1][n-1]}
`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','fill→填充','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.split('→');return`${l} `;}).join('→ ');
}
function init() {
const sel=$('exampleSelect');examples.forEach((e,i)=>{sel.innerHTML+=`${e.label} `;});
$('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=0&&rbestR-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=0&&rbestR-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='';s.split('').forEach((ch,i)=>{viz+=`${ch} ${i} `;});viz+='
';
if(st.expandPairs&&st.expandPairs.length>0){const p=st.expandPairs[st.expandPairs.length-1];viz+=`当前回文: [${p.l},${p.r}]="${s.substring(p.l,p.r+1)}"
`;}
viz+=`最长回文: [${st.bestL},${st.bestR}]="${s.substring(st.bestL,st.bestR+1)}"
`;
$('vizArea').innerHTML=viz;
$('detailContent').innerHTML=''+st.msg+'
'+`最长回文: "${s.substring(st.bestL,st.bestR+1)}" (len ${st.bestR-st.bestL+1})
`;
if(st.stage==='done') $('resultContent').innerHTML=`最长回文子串 = "${s.substring(st.bestL,st.bestR+1)}" 长度=${st.bestR-st.bestL+1}
`;
$('hintText').textContent=st.msg;
const stages=['init→初始化','expand→扩展','skip→跳过','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.split('→');return`${l} `;}).join('→ ');
}
function init() {
const sel=$('exampleSelect');examples.forEach((e,i)=>{sel.innerHTML+=`${e.label} `;});
$('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='s1: ';
str1.split('').forEach((ch,i)=>{viz+=`${ch} `;});
viz+='
s2: ';
str2.split('').forEach((ch,j)=>{viz+=`${ch} `;});
viz+='
';
viz+='∅ ';
for(let j=0;j${str2[j]}`;
viz+=' ';
for(let i=0;i<=m;i++){viz+='';viz+=`${i===0?'∅':str1[i-1]} `;
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+=`${s.dp[i][j]} `;}
viz+=' ';}
viz+='
';$('vizArea').innerHTML=viz;
let detail=''+s.msg+'
';if(s.lcs)detail+=`LCS="${s.lcs.join('')}"
`;$('detailContent').innerHTML=detail;
if(s.stage==='done') $('resultContent').innerHTML=`LCS长度=${s.dp[m][n]} 子序列="${s.lcs.join('')}"
`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','match→匹配','no_match→不匹配','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.split('→');return`${l} `;}).join('→ ');
}
function init() {
const sel=$('exampleSelect');examples.forEach((e,i)=>{sel.innerHTML+=`${e.label} `;});
$('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='';
viz+='∅ ';
for(let j=0;j${str2[j]}`;
viz+=' ';
for(let i=0;i<=m;i++){viz+='';viz+=`${i===0?'∅':str1[i-1]} `;
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+=`${s.dp[i][j]}${ic} `;}
viz+=' ';}
viz+='
✅相同 🔄替换 ➕插入 ➖删除
';
$('vizArea').innerHTML=viz;
let detail=''+s.msg+'
';if(s.ops){detail+='操作:
';s.ops.forEach((o,i)=>{detail+=`${i+1}. ${o}
`;});}$('detailContent').innerHTML=detail;
if(s.stage==='done') $('resultContent').innerHTML=`编辑距离=${s.dp[m][n]} ${s.ops.join(' → ')}
`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','base→边界','match→匹配','op→操作','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.split('→');return`${l} `;}).join('→ ');
}
function init() {
const sel=$('exampleSelect');examples.forEach((e,i)=>{sel.innerHTML+=`${e.label} `;});
$('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=0)hl[s.idx]='orange';for(let i=0;iXOR = ${s.xorResult}
`;
if(s.stage==='xor'){viz+=`${s.prevBin}
⊕
${s.curBin}
=
${s.xorBin}
逐位异或:相同为0,不同为1
`;}
viz+=' ';$('vizArea').innerHTML=viz;
$('detailContent').innerHTML=''+s.msg+'
'+`XOR = ${s.xorResult}
`;
if(s.stage==='done') $('resultContent').innerHTML=`只出现一次的数字 = ${s.xorResult}
`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','xor→异或','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.split('→');return`${l} `;}).join('→ ');
}
function init() {
const sel=$('exampleSelect');examples.forEach((e,i)=>{sel.innerHTML+=`${e.label} `;});
$('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=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+=`候选者: ${s.candidate!==null?s.candidate:'—'}
`;
if(s.history&&s.history.length>0){viz+='投票历史:
';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+=`
`;});viz+='
';s.history.forEach(h=>{viz+=`
${h.v}
`;});viz+='
';}
$('vizArea').innerHTML=viz;
$('detailContent').innerHTML=''+s.msg+'
'+`候选:${s.candidate} 计数:${s.count}
`;
if(s.stage==='done') $('resultContent').innerHTML=`多数元素=${s.candidate}
`;
$('hintText').textContent=s.msg;
const stages=['init→初始化','change→更换','same→相同','diff→不同','done→完成'];
$('pipeline').innerHTML=stages.map(x=>{const[k,l]=x.split('→');return`${l} `;}).join('→ ');
}
function init() {
const sel=$('exampleSelect');examples.forEach((e,i)=>{sel.innerHTML+=`${e.label} `;});
$('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='';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+=`${v} ${i} `;});viz+='
';
viz+='';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+=`${lb} `;});viz+='
';
viz+='■0红 ■1白 ■2蓝
';
$('vizArea').innerHTML=viz;$('detailContent').innerHTML=''+s.msg+'
';
if(s.stage==='done') $('resultContent').innerHTML=`排序=[${s.arr}]
`;
else $('resultContent').innerHTML=`[${s.arr}] lo=${s.lo} mid=${s.mid} hi=${s.hi}
`;
$('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`${l} `;}).join('→ ');
}
function init() {
const sel=$('exampleSelect');examples.forEach((e,i)=>{sel.innerHTML+=`${e.label} `;});
$('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=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阶段: ${phases[Math.min(s.phase,4)]}`;
$('vizArea').innerHTML=viz;$('detailContent').innerHTML=''+s.msg+'
';
if(s.stage==='done') $('resultContent').innerHTML=`下一个排列=[${s.arr}]
`;
$('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`${l} `;}).join('→ ');
}
function init() {
const sel=$('exampleSelect');examples.forEach((e,i)=>{sel.innerHTML+=`${e.label} `;});
$('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='数组→链表:
';
s.arr.forEach((v,i)=>{viz+=`${v} ${i} `;});viz+='
';
viz+=`f(index) = nums[index]
`;
viz+=``;
if(s.phase===1){viz+=`
🏃慢: ${s.slow}
🐇快: ${s.fast}
`;}
else if(s.phase===2){viz+=`
📍ptr1: ${s.slow}
📍ptr2: ${s.fast}
`;}
else if(s.phase===3){viz+=`
🎯重复数=${s.slow}
`;}
viz+='
';
if(s.phase>=1){viz+='链表路径:
';const path=[0];let cur=0;const seen=new Set([0]);for(let k=0;k
{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+=`${node}
`;if(idx→ `;});viz+='';}
$('vizArea').innerHTML=viz;
const pn=['准备','阶段1:找相遇','阶段2:找环入口','完成'];
$('detailContent').innerHTML=''+s.msg+'
'+`${pn[s.phase]}
`;
if(s.stage==='done') $('resultContent').innerHTML=`重复数=${s.slow}
`;
$('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`${l} `;}).join('→ ');
}
function init() {
const sel=$('exampleSelect');examples.forEach((e,i)=>{sel.innerHTML+=`${e.label} `;});
$('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 = '' + s.msg + '
';
$('detailContent').innerHTML = '' + s.msg + '
';
if (s.stage === 'done') {
$('resultContent').innerHTML = '完成!请参考代码实现。
';
}
$('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()