#!/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


📊 可视化

点击「生成图解」开始

📝 当前步骤详情

等待开始...

✅ 结果

等待完成...

💻 参考代码(Python)

''' # ========== 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 += ``; }); $('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 += ``; }); $('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 += ``; }); $('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 += ``; }); $('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 += ``; }); $('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 += ``; }); $('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 += ``; }); $('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 += ``; }); $('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 += ``; }); $('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 += ``; }); $('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 += ``; }); 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 += ``; }); $('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 += ``; }); $('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 += `
${d}
${digitMap[d]}
`; }); 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 += ``; }); $('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 += ``; }); $('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 += ``; }); $('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 += ``; }); $('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 += ``; }); $('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当前放置: ${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 += ``; }); $('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+=``;}); $('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+=``;}); $('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+=``;}); $('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+=``; 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+=``;} viz+='';} viz+='
∅
${i===0?'∅':str1[i-1]}${s.dp[i][j]}
';$('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+=``;}); $('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+=``; 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+=``;} viz+='';} viz+='
∅
${i===0?'∅':str1[i-1]}${s.dp[i][j]}${ic}
✅相同🔄替换➕插入➖删除
'; $('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+=``;}); $('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;i
XOR = ${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+=``;}); $('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:'—'}
计数:
${s.count}
`; 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+=``;}); $('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+=``;}); $('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+=``;}); $('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+=``;}); $('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()