@@ -20,33 +20,80 @@ export default function InputPanel({
2020
2121 const handleSubmit = ( ) => {
2222 try {
23- const num = parseInt ( numCourses , 10 ) ;
24- if ( isNaN ( num ) || num < 1 || num > 20 ) {
23+ // 校验课程数量
24+ const trimmedNum = numCourses . trim ( ) ;
25+ if ( ! trimmedNum ) {
26+ setError ( '请输入课程数量' ) ;
27+ return ;
28+ }
29+ const num = parseInt ( trimmedNum , 10 ) ;
30+ if ( isNaN ( num ) || ! Number . isInteger ( num ) ) {
31+ setError ( '课程数量必须是整数' ) ;
32+ return ;
33+ }
34+ if ( num < 1 || num > 20 ) {
2535 setError ( '课程数量必须是 1-20 之间的整数' ) ;
2636 return ;
2737 }
2838
29- const prereqs = JSON . parse ( prerequisites ) ;
39+ // 校验先修课程格式
40+ const trimmedPrereqs = prerequisites . trim ( ) ;
41+ if ( ! trimmedPrereqs ) {
42+ setError ( '请输入先修课程数组' ) ;
43+ return ;
44+ }
45+
46+ let prereqs : unknown ;
47+ try {
48+ prereqs = JSON . parse ( trimmedPrereqs ) ;
49+ } catch {
50+ setError ( '先修课程格式错误,请使用 JSON 数组格式,如 [[1,0],[2,1]]' ) ;
51+ return ;
52+ }
53+
3054 if ( ! Array . isArray ( prereqs ) ) {
31- setError ( '先修课程必须是数组格式' ) ;
55+ setError ( '先修课程必须是数组格式,如 [[1,0],[2,1]] ' ) ;
3256 return ;
3357 }
3458
35- for ( const pair of prereqs ) {
59+ // 校验每个先修关系
60+ const edgeSet = new Set < string > ( ) ;
61+ for ( let i = 0 ; i < prereqs . length ; i ++ ) {
62+ const pair = prereqs [ i ] ;
3663 if ( ! Array . isArray ( pair ) || pair . length !== 2 ) {
37- setError ( '每个先修关系必须是 [a, b] 格式' ) ;
64+ setError ( `第 ${ i + 1 } 个先修关系格式错误,必须是 [a, b] 格式` ) ;
65+ return ;
66+ }
67+ const [ a , b ] = pair ;
68+ if ( typeof a !== 'number' || typeof b !== 'number' || ! Number . isInteger ( a ) || ! Number . isInteger ( b ) ) {
69+ setError ( `第 ${ i + 1 } 个先修关系中的课程编号必须是整数` ) ;
70+ return ;
71+ }
72+ if ( a < 0 || a >= num ) {
73+ setError ( `第 ${ i + 1 } 个先修关系中的课程 ${ a } 超出范围 (0-${ num - 1 } )` ) ;
74+ return ;
75+ }
76+ if ( b < 0 || b >= num ) {
77+ setError ( `第 ${ i + 1 } 个先修关系中的课程 ${ b } 超出范围 (0-${ num - 1 } )` ) ;
3878 return ;
3979 }
40- if ( pair [ 0 ] < 0 || pair [ 0 ] >= num || pair [ 1 ] < 0 || pair [ 1 ] >= num ) {
41- setError ( `课程编号必须在 0 到 ${ num - 1 } 之间 ` ) ;
80+ if ( a === b ) {
81+ setError ( `第 ${ i + 1 } 个先修关系中课程不能依赖自己 ` ) ;
4282 return ;
4383 }
84+ // 检查重复边
85+ const edgeKey = `${ a } -${ b } ` ;
86+ if ( edgeSet . has ( edgeKey ) ) {
87+ setError ( `存在重复的先修关系 [${ a } , ${ b } ]` ) ;
88+ return ;
89+ }
90+ edgeSet . add ( edgeKey ) ;
4491 }
4592
4693 setError ( null ) ;
47- onSubmit ( num , prereqs ) ;
94+ onSubmit ( num , prereqs as number [ ] [ ] ) ;
4895 } catch {
49- setError ( '先修课程格式错误,请使用 JSON 数组格式 ' ) ;
96+ setError ( '输入数据格式错误,请检查后重试 ' ) ;
5097 }
5198 } ;
5299
@@ -57,29 +104,55 @@ export default function InputPanel({
57104 setError ( '请先输入有效的课程数量 (2-20)' ) ;
58105 return ;
59106 }
60-
61- // 生成随机 DAG(有向无环图)
62- // 边数量:节点数的 1-2 倍,确保图有一定复杂度
63- const prereqs : number [ ] [ ] = [ ] ;
64- const maxEdges = Math . min ( num * 2 , ( num * ( num - 1 ) ) / 2 ) ;
65- const edgeCount = Math . floor ( Math . random ( ) * maxEdges ) + Math . max ( 1 , Math . floor ( num / 2 ) ) ;
107+
108+ // 生成连通的随机 DAG(有向无环图)
109+ // 策略:先生成一条主链保证基本连通,再随机添加额外边
110+ const edges : number [ ] [ ] = [ ] ;
66111 const existingEdges = new Set < string > ( ) ;
67-
68- for ( let i = 0 ; i < edgeCount && prereqs . length < maxEdges ; i ++ ) {
69- // 确保 from > to 来避免环(拓扑序:小编号是先修课)
70- const to = Math . floor ( Math . random ( ) * ( num - 1 ) ) ;
71- const from = Math . floor ( Math . random ( ) * ( num - to - 1 ) ) + to + 1 ;
112+
113+ // 随机打乱节点顺序,作为拓扑序
114+ const topoOrder = Array . from ( { length : num } , ( _ , i ) => i ) ;
115+ for ( let i = topoOrder . length - 1 ; i > 0 ; i -- ) {
116+ const j = Math . floor ( Math . random ( ) * ( i + 1 ) ) ;
117+ [ topoOrder [ i ] , topoOrder [ j ] ] = [ topoOrder [ j ] , topoOrder [ i ] ] ;
118+ }
119+
120+ // 第一步:生成主链,确保图基本连通
121+ // 每个节点(除了第一个)至少有一条来自前面节点的边
122+ for ( let i = 1 ; i < num ; i ++ ) {
123+ // 从前面的节点中随机选一个作为前置
124+ const prevIdx = Math . floor ( Math . random ( ) * i ) ;
125+ const from = topoOrder [ i ] ; // 后面的节点
126+ const to = topoOrder [ prevIdx ] ; // 前面的节点作为前置
127+ const key = `${ from } -${ to } ` ;
128+ existingEdges . add ( key ) ;
129+ edges . push ( [ from , to ] ) ;
130+ }
131+
132+ // 第二步:随机添加额外边增加复杂度
133+ // 额外边数量:节点数的 50%-100%
134+ const extraEdgeCount = Math . floor ( Math . random ( ) * ( num / 2 ) ) + Math . floor ( num / 2 ) ;
135+ let attempts = 0 ;
136+ const maxAttempts = extraEdgeCount * 3 ;
137+
138+ while ( edges . length < num - 1 + extraEdgeCount && attempts < maxAttempts ) {
139+ attempts ++ ;
140+ // 随机选两个不同位置的节点,确保拓扑序正确(后面指向前面)
141+ const idx1 = Math . floor ( Math . random ( ) * ( num - 1 ) ) + 1 ;
142+ const idx2 = Math . floor ( Math . random ( ) * idx1 ) ;
143+ const from = topoOrder [ idx1 ] ;
144+ const to = topoOrder [ idx2 ] ;
72145 const key = `${ from } -${ to } ` ;
73-
74- if ( ! existingEdges . has ( key ) && from !== to ) {
146+
147+ if ( ! existingEdges . has ( key ) ) {
75148 existingEdges . add ( key ) ;
76- prereqs . push ( [ from , to ] ) ;
149+ edges . push ( [ from , to ] ) ;
77150 }
78151 }
79-
80- setPrerequisites ( JSON . stringify ( prereqs ) ) ;
152+
153+ setPrerequisites ( JSON . stringify ( edges ) ) ;
81154 setError ( null ) ;
82- onSubmit ( num , prereqs ) ;
155+ onSubmit ( num , edges ) ;
83156 } ;
84157
85158 return (
@@ -104,12 +177,12 @@ export default function InputPanel({
104177 />
105178 </ div >
106179 { error && < div className = "input-error" > { error } </ div > }
180+ < button className = "random-button" onClick = { generateRandom } >
181+ 🎲 随机生成指定节点个数的有向无环图
182+ </ button >
107183 < button className = "run-button" onClick = { handleSubmit } >
108184 运行
109185 </ button >
110- < button className = "random-button" onClick = { generateRandom } >
111- 🎲 随机生成
112- </ button >
113186 </ div >
114187 ) ;
115188}
0 commit comments