用C++实现的数独解题程序 SudokuSolver 2.7 及实例分析
引言:一个 bug 的发现
在 MobaXterm 上看到有内置的 Sudoku 游戏,于是拿 SudokuSolver 求解,随机出题,一上来是个 medium 级别的题:
073 000 060 980 460 000 000 007 304 000 000 578 010 000 090 248 000 000 105 900 000 000 034 081 090 000 230
运行过程信息如下:
Order please: load-quiz h:\s.txt Quiz loaded. Order please: guess-mode 1 In interactive guessing mode:1 (0:no; other:yes) Order please: run 7) row 1 complete shrunken by group 9) row 3 complete shrunken by group 11) row 3 complete shrunken by group 13) row 3 complete shrunken by group 15) row 4 complete shrunken by group 1GW: 6 shrunken out of [4,3] 1GW: 3 shrunken out of [4,4] 1GW: 6 shrunken out of [4,4] 1GW: 6 shrunken out of [4,6] 17) row 4 incomplete shrunken by group Take a guess please, by default it will be [3,1]=5: 18) Guess [3,1] level 1 at 1 out of 2 18) shrinking 2 from [8,2] went wrong 19) Forward guess [3,1] level 1 at 2 out of 2 473 215 869 982 463 157 651 897 324 369 142 578 517 386 492 248 759 613 135 928 746 726 534 981 894 671 235 Fine [steps:23, solution sum:1]. Run time: 1958 milliseconds; steps: 23, solution sum: 1. Biggest level on this run(til): 1 Order please: step 24) No more solution (solution sum is 1). ... Order please:
可以看到走到 17 步时做了一次交互式猜测。好奇这么一个 medium 级别的题是否真的需要猜测,重新进到 16 步时的上下文做仔细考察:
Order please: runtil 16 7) row 1 complete shrunken by group 9) row 3 complete shrunken by group 11) row 3 complete shrunken by group 13) row 3 complete shrunken by group 15) row 4 complete shrunken by group 473 000 869 982 463 157 001 897 324 000 040 578 010 000 492 248 000 613 105 900 746 000 034 981 090 000 235 Steps:16 Candidates: [1,4]: 1 2 5 [1,5]: 1 2 5 [1,6]: 1 2 5 [3,1]: 5 6 [3,2]: 5 6 [4,1]: 3 6 [4,2]: 3 6 [4,3]: 6 9 [4,4]: 1 2 3 6 [4,6]: 1 2 6 9 [5,1]: 3 5 6 7 [5,3]: 6 7 [5,4]: 3 5 6 7 [5,5]: 5 7 8 [5,6]: 5 6 8 [6,4]: 5 7 [6,5]: 5 7 [6,6]: 5 9 [7,2]: 2 3 [7,5]: 2 8 [7,6]: 2 8 [8,1]: 6 7 [8,2]: 2 6 [8,3]: 6 7 [8,4]: 2 5 6 7 [9,1]: 6 7 8 [9,3]: 4 6 7 [9,4]: 1 6 7 [9,5]: 1 7 8 [9,6]: 1 6 8 The foremost cell with 2 candidate(s) at [3,1] Run time: 49 milliseconds; steps: 16, solution sum: 0. Biggest level on this run(til): 0
再走一步就要对 [3,1] 位置进行猜测,这时已填数的分布情况为:
473 000 869 982 463 157 001 897 324 000 040 578 010 000 492 248 000 613 105 900 746 000 034 981 090 000 235
考察第 7 行,三个空位的候选值分布情况为:
[7,2]: 2 3 [7,5]: 2 8 [7,6]: 2 8
候选值 3 只出现在 [7,2] 位置,因而必有 [7,2] = 3。而这是早期版本就已经实现了的算法,怎么到这里会做一次猜测?一定是程序有 bug。
调试程序很快发现问题出在 filterCandidates 接口实现如下标黄的语句:
u8 CQuizDealer::filterCandidates() { incSteps(); u8 ret = RET_PENDING; if (m_mode != 2) { for (u8 row = 0; row < 9; ++row) if (ret = filterRowGroup(row)) return ret; for (u8 col = 0; col < 9; ++col) if (ret = filterColGroup(col)) return ret; for (u8 blk = 0; blk < 9; ++blk) if (ret = filterBlkGroup(blk)) return ret; } ...
return ret; }
从第一行起逐行去调用 filterRowGroup 接口对该行做收缩处理,只要返回值不为 0,即出错或有收缩,就结束当前这一步的收缩处理。出问题时,第 4 行发生了不完全收缩,对应如下输出信息:
1GW: 6 shrunken out of [4,3]
1GW: 3 shrunken out of [4,4]
1GW: 6 shrunken out of [4,4]
1GW: 6 shrunken out of [4,6]
17) row 4 incomplete shrunken by group
这时就结束整个 filterCandidates 的处理,就会导致上面所示的发生在第 7 行的完全收缩 [7,2] = 3 直接被跳过了,随后就出现了不必要的猜测。
SudokuSolver 2.7 版代码修改
filterCandidates 接口实现修改:
1 u8 CQuizDealer::filterCandidates() 2 { 3 incSteps(); 4 u8 ret = RET_PENDING; 5 bool partlyShrunken = false; 6 if (m_mode != 2) { 7 for (u8 row = 0; row < 9; ++row) { 8 ret = filterRowGroup(row); 9 if (IsDone(ret)) 10 return ret; 11 if (!partlyShrunken && ret == RET_SHRUNKEN) 12 partlyShrunken = true; 13 } 14 for (u8 col = 0; col < 9; ++col) { 15 ret = filterColGroup(col); 16 if (IsDone(ret)) 17 return ret; 18 if (!partlyShrunken && ret == RET_SHRUNKEN) 19 partlyShrunken = true; 20 } 21 for (u8 blk = 0; blk < 9; ++blk) { 22 ret = filterBlkGroup(blk); 23 if (IsDone(ret)) 24 return ret; 25 if (!partlyShrunken && ret == RET_SHRUNKEN) 26 partlyShrunken = true; 27 } 28 } 29 if (m_mode != 1) { 30 for (u8 row = 0; row < 9; ++row) { 31 ret = filterRowCandidatesEx(row); 32 if (IsDone(ret)) 33 return ret; 34 if (!partlyShrunken && ret == RET_SHRUNKEN) 35 partlyShrunken = true; 36 } 37 for (u8 col = 0; col < 9; ++col) { 38 ret = filterColCandidatesEx(col); 39 if (IsDone(ret)) 40 return ret; 41 if (!partlyShrunken && ret == RET_SHRUNKEN) 42 partlyShrunken = true; 43 } 44 } 45 if (partlyShrunken) { 46 printf("%d) incomplete shrink met, filter again\n", m_steps); 47 return filterCandidates(); 48 } 49 if (ret == RET_PENDING) 50 printf("%d) no shrink happened\n", m_steps); 51 return ret; 52 }
修改了上述的 bug,末尾部分也增加了未能收缩时输出 no shrink happened 提示信息的处理。
版本信息修改:
// 2.6 2021/10/30 #define STR_VER "Sudoku Solver 2.7 2021/11/1 by readalps\n\n"
用 2.7 版求解“最难”数独题
800 000 000 003 600 000 070 090 200 050 007 000 000 045 700 000 100 030 001 000 068 008 500 010 090 000 400
Order please: load-quiz h:\s.txt Quiz loaded. Order please: run 2) no shrink happened 3) Guess [8,7] level 1 at 1 out of 2 4) no shrink happened 5) Guess [7,7] level 2 at 1 out of 2 ... 380) Guess [1,8] level 9 at 1 out of 2 382) no shrink happened 383) Guess [2,5] level 10 at 1 out of 2 386) shrinking 4 from [6,3] went wrong 387) Forward guess [2,5] level 10 at 2 out of 2 812 753 649 943 682 175 675 491 283 154 237 896 369 845 721 287 169 534 521 974 368 438 526 917 796 318 452 Fine [steps:392, solution sum:1]. Run time: 733 milliseconds; steps: 392, solution sum: 1. Biggest level on this run(til): 10 Order please:
第二次 run 的输出:
787) No more solution (solution sum is 1). 800 000 306 003 600 800 670 893 201 352 967 184 186 345 700 040 128 635 001 479 568 068 530 910 090 286 473 Invalid quiz [steps:787] - no more solution (solution sum is 1) Run time: 573 milliseconds; steps: 787, solution sum: 1. Biggest level on this run(til): 8
2.4 版的两次 run 用了两千多步,这里只有 787 步;最大猜测级别也由 12 降到了 10。
用 2.7 版求解另一道难题
005 300 000 800 000 020 070 010 500 400 005 300 010 070 006 003 200 080 060 500 009 004 000 030 000 009 700
因为上述的 bug,此前 2.4 版求解这道数独题时,最大猜测级别达到了 11。来看 2.7 版求解的效果:
H:\Read\num\Release>sudoku.exe Order please: load-quiz h:\s.txt Quiz loaded. Order please: run 2) row 2 complete shrunken by group 4) row 5 complete shrunken by group 2GW: 2 shrunken out of [4,3] 2GW: 8 shrunken out of [4,3] 2GW: 9 shrunken out of [4,3] 2GW: 5 shrunken out of [6,1] 2GW: 9 shrunken out of [6,1] CelSet: [4,3] [6,1] ValSet: 6 7 6) blk 4 incomplete shrunken by group 6) incomplete shrink met, filter again 7) no shrink happened 8) Guess [6,2] level 1 at 1 out of 2 ... 137) col 8 complete shrunken by group 145 327 698 839 654 127 672 918 543 496 185 372 218 473 956 753 296 481 367 542 819 984 761 235 521 839 764 Fine [steps:141, solution sum:1]. Run time: 168 milliseconds; steps: 141, solution sum: 1. Biggest level on this run(til): 10 Order please: run 142) Forward guess [1,7] level 5 at 2 out of 2 144) shrinking 8 from [9,4] went wrong ... 287) col 7 complete shrunken by group 288) shrinking 6 from [2,4] went wrong 289) No more solution (solution sum is 1). 145 300 900 839 050 127 672 018 543 426 005 371 518 473 296 793 261 485 067 500 819 954 000 632 081 609 750 Invalid quiz [steps:289] - no more solution (solution sum is 1) Run time: 188 milliseconds; steps: 289, solution sum: 1. Biggest level on this run(til): 6 Order please:
最大猜测级别也达到了 10。