Skip to content

Commit 3ad3d0d

Browse files
committed
feat: 添加算法原理概述和改进树形可视化
1. 新增AlgorithmGuide组件,包含: - 核心思想:回溯算法简介 - 关键规则:左括号优先、右括号限制、终止条件 - 执行步骤:5个步骤的详细说明,当前步骤高亮 - 结果数量:卡特兰数公式和计算结果 - 树形图例:节点状态颜色说明 2. 改进TreeVisualization组件: - 不同节点状态使用不同颜色(当前/有效/回溯/待探索) - 当前节点添加发光效果和脉冲动画 - 有效路径节点显示✓标记 - 连线颜色随状态变化 - 底部显示当前动作指示器 - 右上角显示节点状态图例 3. 所有测试通过
1 parent 38d5497 commit 3ad3d0d

File tree

7 files changed

+685
-71
lines changed

7 files changed

+685
-71
lines changed

node_modules/.vite/vitest/results.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { TreeVisualization } from './components/TreeVisualization';
55
import { StatePanel } from './components/StatePanel';
66
import { CodePanel } from './components/CodePanel';
77
import { ResultsPanel } from './components/ResultsPanel';
8+
import { AlgorithmGuide } from './components/AlgorithmGuide';
89
import { useAlgorithm } from './hooks/useAlgorithm';
910
import { useAnimation } from './hooks/useAnimation';
1011
import { updateTreeStatus } from './utils/treeBuilder';
@@ -125,13 +126,20 @@ function App() {
125126
/>
126127
</div>
127128

128-
<div id="tree-container" style={styles.treeContainer}>
129-
<TreeVisualization
130-
treeData={algorithm.treeData}
131-
currentNodeId={currentStep?.nodeId || null}
132-
width={dimensions.width}
133-
height={dimensions.height}
129+
<div style={styles.rightPanel}>
130+
<AlgorithmGuide
131+
n={algorithm.n}
132+
currentAction={currentStep?.action}
134133
/>
134+
<div id="tree-container" style={styles.treeContainer}>
135+
<TreeVisualization
136+
treeData={algorithm.treeData}
137+
currentNodeId={currentStep?.nodeId || null}
138+
width={dimensions.width}
139+
height={dimensions.height}
140+
currentAction={currentStep?.action}
141+
/>
142+
</div>
135143
</div>
136144
</div>
137145
</div>
@@ -179,6 +187,13 @@ const styles: Record<string, React.CSSProperties> = {
179187
gap: '12px',
180188
flexShrink: 0
181189
},
190+
rightPanel: {
191+
flex: 1,
192+
display: 'flex',
193+
flexDirection: 'column',
194+
gap: '12px',
195+
minWidth: 0
196+
},
182197
treeContainer: {
183198
flex: 1,
184199
minWidth: 0,
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import React from 'react';
2+
3+
export const styles: Record<string, React.CSSProperties> = {
4+
container: {
5+
padding: '12px',
6+
backgroundColor: '#fff',
7+
borderRadius: '8px',
8+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
9+
marginBottom: '12px'
10+
},
11+
header: {
12+
display: 'flex',
13+
justifyContent: 'space-between',
14+
alignItems: 'center',
15+
cursor: 'pointer',
16+
userSelect: 'none'
17+
},
18+
title: {
19+
margin: 0,
20+
fontSize: '14px',
21+
fontWeight: 600,
22+
color: '#333',
23+
display: 'flex',
24+
alignItems: 'center',
25+
gap: '8px'
26+
},
27+
toggleIcon: {
28+
fontSize: '12px',
29+
color: '#666',
30+
transition: 'transform 0.2s'
31+
},
32+
content: {
33+
marginTop: '12px',
34+
fontSize: '13px',
35+
lineHeight: 1.6,
36+
color: '#444'
37+
},
38+
section: {
39+
marginBottom: '12px'
40+
},
41+
sectionTitle: {
42+
fontWeight: 600,
43+
color: '#1976d2',
44+
marginBottom: '6px',
45+
display: 'flex',
46+
alignItems: 'center',
47+
gap: '6px'
48+
},
49+
paragraph: {
50+
margin: '0 0 8px 0',
51+
paddingLeft: '4px'
52+
},
53+
highlight: {
54+
backgroundColor: '#e3f2fd',
55+
padding: '2px 6px',
56+
borderRadius: '3px',
57+
fontFamily: 'Consolas, Monaco, monospace',
58+
fontSize: '12px'
59+
},
60+
keyPoint: {
61+
display: 'flex',
62+
alignItems: 'flex-start',
63+
gap: '8px',
64+
padding: '8px',
65+
backgroundColor: '#f5f5f5',
66+
borderRadius: '6px',
67+
marginBottom: '8px'
68+
},
69+
keyPointIcon: {
70+
fontSize: '14px',
71+
flexShrink: 0
72+
},
73+
keyPointText: {
74+
flex: 1
75+
},
76+
formula: {
77+
display: 'flex',
78+
alignItems: 'center',
79+
gap: '8px',
80+
padding: '8px 12px',
81+
backgroundColor: '#fff3e0',
82+
borderRadius: '6px',
83+
fontFamily: 'Consolas, Monaco, monospace',
84+
fontSize: '12px',
85+
marginTop: '8px'
86+
},
87+
stepsContainer: {
88+
display: 'flex',
89+
flexDirection: 'column' as const,
90+
gap: '6px'
91+
},
92+
step: {
93+
display: 'flex',
94+
alignItems: 'flex-start',
95+
gap: '8px',
96+
padding: '6px 8px',
97+
backgroundColor: '#f9f9f9',
98+
borderRadius: '4px',
99+
borderLeft: '3px solid #1976d2'
100+
},
101+
stepNumber: {
102+
width: '20px',
103+
height: '20px',
104+
borderRadius: '50%',
105+
backgroundColor: '#1976d2',
106+
color: '#fff',
107+
display: 'flex',
108+
alignItems: 'center',
109+
justifyContent: 'center',
110+
fontSize: '11px',
111+
fontWeight: 600,
112+
flexShrink: 0
113+
},
114+
stepText: {
115+
flex: 1,
116+
fontSize: '12px'
117+
},
118+
legendContainer: {
119+
display: 'flex',
120+
flexWrap: 'wrap' as const,
121+
gap: '8px',
122+
marginTop: '8px'
123+
},
124+
legendItem: {
125+
display: 'flex',
126+
alignItems: 'center',
127+
gap: '4px',
128+
fontSize: '11px',
129+
padding: '4px 8px',
130+
backgroundColor: '#f5f5f5',
131+
borderRadius: '4px'
132+
},
133+
legendDot: {
134+
width: '12px',
135+
height: '12px',
136+
borderRadius: '50%'
137+
}
138+
};

src/components/AlgorithmGuide.tsx

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { useState } from 'react';
2+
import { styles } from './AlgorithmGuide.styles';
3+
4+
interface AlgorithmGuideProps {
5+
n: number;
6+
currentAction?: string;
7+
}
8+
9+
export function AlgorithmGuide({ n, currentAction }: AlgorithmGuideProps) {
10+
const [isExpanded, setIsExpanded] = useState(true);
11+
12+
return (
13+
<div style={styles.container}>
14+
<div style={styles.header} onClick={() => setIsExpanded(!isExpanded)}>
15+
<h3 style={styles.title}>
16+
<span>📖</span>
17+
<span>算法原理</span>
18+
</h3>
19+
<span style={{
20+
...styles.toggleIcon,
21+
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)'
22+
}}>
23+
24+
</span>
25+
</div>
26+
27+
{isExpanded && (
28+
<div style={styles.content}>
29+
{/* 核心思想 */}
30+
<div style={styles.section}>
31+
<div style={styles.sectionTitle}>
32+
<span>💡</span>
33+
<span>核心思想</span>
34+
</div>
35+
<p style={styles.paragraph}>
36+
使用<span style={styles.highlight}>回溯算法</span>生成所有有效的括号组合。
37+
通过递归尝试添加左括号或右括号,并在不满足条件时回溯。
38+
</p>
39+
</div>
40+
41+
{/* 关键规则 */}
42+
<div style={styles.section}>
43+
<div style={styles.sectionTitle}>
44+
<span>📋</span>
45+
<span>关键规则</span>
46+
</div>
47+
<div style={styles.keyPoint}>
48+
<span style={styles.keyPointIcon}>1️⃣</span>
49+
<span style={styles.keyPointText}>
50+
<strong>左括号优先:</strong>只要还有剩余的左括号(left {'>'} 0),就可以添加左括号
51+
</span>
52+
</div>
53+
<div style={styles.keyPoint}>
54+
<span style={styles.keyPointIcon}>2️⃣</span>
55+
<span style={styles.keyPointText}>
56+
<strong>右括号限制:</strong>只有当已使用的左括号多于右括号时(right {'>'} left),才能添加右括号
57+
</span>
58+
</div>
59+
<div style={styles.keyPoint}>
60+
<span style={styles.keyPointIcon}>3️⃣</span>
61+
<span style={styles.keyPointText}>
62+
<strong>终止条件:</strong>当 left = 0 且 right = 0 时,找到一个有效组合
63+
</span>
64+
</div>
65+
</div>
66+
67+
{/* 执行步骤 */}
68+
<div style={styles.section}>
69+
<div style={styles.sectionTitle}>
70+
<span>🔄</span>
71+
<span>执行步骤</span>
72+
</div>
73+
<div style={styles.stepsContainer}>
74+
<StepItem
75+
number={1}
76+
text="检查是否完成:left=0 且 right=0 → 保存结果"
77+
isActive={currentAction === 'complete'}
78+
/>
79+
<StepItem
80+
number={2}
81+
text="尝试添加左括号:如果 left > 0,递归调用 backtrack(cur+'(', left-1, right)"
82+
isActive={currentAction === 'add_left'}
83+
/>
84+
<StepItem
85+
number={3}
86+
text="回溯:从添加左括号的分支返回"
87+
isActive={currentAction === 'backtrack'}
88+
/>
89+
<StepItem
90+
number={4}
91+
text="尝试添加右括号:如果 right > left,递归调用 backtrack(cur+')', left, right-1)"
92+
isActive={currentAction === 'add_right'}
93+
/>
94+
<StepItem
95+
number={5}
96+
text="回溯:从添加右括号的分支返回"
97+
isActive={currentAction === 'backtrack'}
98+
/>
99+
</div>
100+
</div>
101+
102+
{/* 结果数量 */}
103+
<div style={styles.section}>
104+
<div style={styles.sectionTitle}>
105+
<span>📊</span>
106+
<span>结果数量</span>
107+
</div>
108+
<p style={styles.paragraph}>
109+
n 对括号的有效组合数量由<strong>卡特兰数</strong>决定:
110+
</p>
111+
<div style={styles.formula}>
112+
<span>C(n) = (2n)! / ((n+1)! × n!)</span>
113+
<span style={{ color: '#666' }}>|</span>
114+
<span>n={n} → C({n}) = {catalanNumber(n)} 种组合</span>
115+
</div>
116+
</div>
117+
118+
{/* 树形图例 */}
119+
<div style={styles.section}>
120+
<div style={styles.sectionTitle}>
121+
<span>🎨</span>
122+
<span>树形图例</span>
123+
</div>
124+
<div style={styles.legendContainer}>
125+
<LegendItem color="#e0e0e0" label="待探索" />
126+
<LegendItem color="#2196F3" label="当前节点" />
127+
<LegendItem color="#4CAF50" label="有效路径" />
128+
<LegendItem color="#FF9800" label="正在回溯" />
129+
<LegendItem color="#9E9E9E" label="已回溯" />
130+
</div>
131+
</div>
132+
</div>
133+
)}
134+
</div>
135+
);
136+
}
137+
138+
function StepItem({ number, text, isActive }: { number: number; text: string; isActive?: boolean }) {
139+
return (
140+
<div style={{
141+
...styles.step,
142+
backgroundColor: isActive ? '#e3f2fd' : '#f9f9f9',
143+
borderLeftColor: isActive ? '#1976d2' : '#ddd'
144+
}}>
145+
<span style={{
146+
...styles.stepNumber,
147+
backgroundColor: isActive ? '#1976d2' : '#9e9e9e'
148+
}}>{number}</span>
149+
<span style={styles.stepText}>{text}</span>
150+
</div>
151+
);
152+
}
153+
154+
function LegendItem({ color, label }: { color: string; label: string }) {
155+
return (
156+
<div style={styles.legendItem}>
157+
<span style={{ ...styles.legendDot, backgroundColor: color }} />
158+
<span>{label}</span>
159+
</div>
160+
);
161+
}
162+
163+
function catalanNumber(n: number): number {
164+
if (n <= 0) return 1;
165+
let result = 1;
166+
for (let i = 0; i < n; i++) {
167+
result = result * 2 * (2 * i + 1) / (i + 2);
168+
}
169+
return Math.round(result);
170+
}

0 commit comments

Comments
 (0)