用C++实现的数独解题程序 SudokuSolver 2.1 及实例分析
SudokuSolver 2.1 程序实现
在 2.0 版的基础上,2.1 版在输出信息上做了一些改进,并增加了 runtil <steps> 命令,方便做实例分析。
CQuizDealer 类声明部分的修改
class CQuizDealer { public: ...
void run(ulong tilsteps = 0); ...
private: ...
enum {RET_PENDING, RET_WRONG, RET_SHRUNKEN, RET_OK}; ... inline void incSteps() {++m_steps;} void parse();
...
run 接口增加了参数 ulong tilsteps,用于实现 runtil <steps> 命令;增加了常数 RET_SHRUNKEN 定义;增加了 ++m_steps; 语句的包装接口 incSteps,代码中统一使用 incSteps 接口给 m_steps 做加一操作。
输出信息增加 m_steps 输出
以 guess 接口为例:
void CQuizDealer::guess(u8 guessIdx) { incSteps(); ++m_guessLevel; m_guessPos = guessIdx; m_guessValPos = 1; pushIn(guessIdx, 1); printf("%u) Guess [%d,%d] level %d at 1 out of %d\n", m_steps, (int)(guessIdx / 9 + 1), (int)(guessIdx % 9 + 1), (int)m_guessLevel, (int)m_seqCell[guessIdx].candidates[0]); if (!shift(guessIdx, 1)) nextGuess(); }
showQuiz 输出信息优化
1 void CQuizDealer::showQuiz() 2 { 3 ...
32 u8 foremost = 0; 33 u8 least = 0; 34 for (...) { 35 ...
37 if (least == 0 || (least != 1 && pVals[0] < least)) { 38 foremost = pos; 39 least = pVals[0]; 40 } 41 ...
46 } 47 ...
49 if (foremost != 0) 50 printf("The foremost cell with %d candidate(s) at [%d,%d]\n", (int)least, (int)(foremost / 9 + 1), (int)(foremost % 9 + 1)); 51 ...
53 }
run 接口修改
1 void CQuizDealer::run(ulong tilsteps) 2 { 3 ...
18 while (m_state == STA_VALID && (tilsteps == 0 || m_steps < tilsteps)) 19 ... 22 }
shrinkCandidates 接口修改
u8 CQuizDealer::shrinkCandidates(u8 cellPos, u8* pVals) { ...
bool shrunken = false; if (vals[0] != m_seqCell[cellPos].candidates[0]) { shrunken = true; printCandidatesChange(cellPos, m_seqCell[cellPos].candidates, vals); ... } return (shrunken ? RET_SHRUNKEN : RET_PENDING); }
新增 printCandidatesChange 函数
1 void printCandidatesChange(u8 pos, u8* pOld, u8* pNew) 2 { 3 u8 sum = pOld[0]; 4 printf("[%d,%d]:", (int)(pos / 9 + 1), (int)(pos % 9 + 1)); 5 for (u8 idx = 0; idx < sum; ++idx) 6 printf(" %d", (int)pOld[idx + 1]); 7 printf(" shrunken to "); 8 sum = pNew[0]; 9 for (u8 idx = 0; idx < sum; ++idx) 10 printf(" %d", (int)pNew[idx + 1]); 11 }
shrinkRowCandidatesP1 接口修改
u8 CQuizDealer::shrinkRowCandidatesP1(u8* pBlk, u8* pRow, u8 zeroSum, u8 row, u8 colBase) { ...
for (u8 col = colBase; col < colBase + 3; ++col) { ... ret = shrinkCandidates(rowBase + col, pBlk); if (ret == RET_WRONG) { ... } if (ret != RET_PENDING) { printf(" worked by row-ply1.\n"); if (ret == RET_OK) shrunken = true; else if (ret == RET_SHRUNKEN) ret = RET_PENDING; } } ... }
shrinkRowCandidatesP2、shrinkColCandidatesP1 以及 shrinkColCandidatesP2 接口做类似修改。
showOrderList 和 dealOrder 函数修改
// 1.0 2021/9/20 // 2.0 2021/10/2 #define STR_VER "Sudoku Solver 2.1 2021/10/4 by readalps\n\n" void showOrderList() { ... printf("runtil <steps>: run till certain steps run\n"); printf("bye: quit\n"); } void dealOrder(std::string& strOrder) { ...
else if (matchPrefixEx(strOrder, "runtil ", strEx)) { ulong tilsteps = strtoul(strEx.c_str(), 0, 10); CQuizDealer::instance()->run(tilsteps); } else showOrderList(); }
实例分析
以下以 SudokuSolver 1.0:用C++实现的数独解题程序 【二】 里试验过的“最难”数独题为例,做一些分析。
H:\Read\num\Release>sudoku.exe Order please: load-quiz h:\s.txt Quiz loaded. Order please: show 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: run 3) Guess [8,7] level 1 at 1 out of 2 5) Guess [7,7] level 2 at 1 out of 2 6) row 6 shrunken by group 8) blk 9 shrunken by group [9,1]: 2 3 5 6 7 shrunken to 3 5 6 worked by row-ply2. [9,3]: 2 5 6 7 shrunken to 5 6 worked by row-ply2. [9,4]: 2 3 7 8 shrunken to 3 8 worked by row-ply2. [9,5]: 1 2 3 6 7 8 shrunken to 1 3 6 8 worked by row-ply2. [9,6]: 1 2 3 6 8 shrunken to 1 3 6 8 worked by row-ply2. [6,3]: 2 4 6 7 9 shrunken to 7 worked by col-ply2. 10) col 3 shrunken ply-2 by vblk 1 13) Guess [9,3] level 3 at 1 out of 2 ... 769) Guess [1,5] level 7 at 1 out of 2 770) shrinking 8 from [6,6] went wrong 771) Forward guess [1,5] level 7 at 2 out of 2 773) col 8 shrunken by group 776) Guess [1,6] level 8 at 1 out of 2 777) row 3 shrunken by group 779) row 3 shrunken by group 781) row 9 shrunken by group 783) col 6 shrunken by group 786) col 4 shrunken by group 788) col 6 shrunken by group 791) Guess [1,8] level 9 at 1 out of 2 794) Guess [2,5] level 10 at 1 out of 2 797) shrinking 4 from [6,3] went wrong 798) 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 Done [steps:803, solution sum:1]. Run time: 430 milliseconds; steps: 803, solution sum: 1. Order please:
第一个 run 命令求得一个解,耗时约 430 毫秒,用了 803 步。
Order please: run 804) Upward guess [1,8] level 9 at 2 out of 2 807) row 6 shrunken by group 809) row 6 shrunken by group 811) shrinking 2 from [4,4] went wrong 812) Upward guess [1,6] level 8 at 2 out of 2 ... 1778) Upward guess [3,3] level 5 at 2 out of 2 1779) row 1 shrunken by group [5,1]: 1 2 3 6 9 shrunken to 1 6 worked by row-ply1. [5,2]: 1 2 3 6 8 shrunken to 1 6 worked by row-ply1. [5,4]: 2 3 8 9 shrunken to 3 worked by row-ply2. 1781) row 5 shrunken ply-2 by blk 3 1784) row 4 shrunken by group 1786) col 2 shrunken by group 1786) shrinking 9 from blk[5,1] went wrong 1787) No more solution (solution sum is 1). 800 000 306 003 600 800 076 893 250 350 067 180 000 345 700 080 129 635 001 000 568 008 500 910 090 000 400 Invalid quiz [steps:1787] - no more solution (solution sum is 1) Run time: 610 milliseconds; steps: 1787, solution sum: 1. Order please:
第二个 run 命令耗时约 610 毫秒,两次 run 命令共用 1787 步。从完整的输出信息看,求解过程到达的最深猜测级别为 12 级,1.0 版是 21 级。
第二类候选值集合收缩算法实例
如 SudokuSolver 2.0:用C++实现的数独解题程序 【一】 里所示,2.0 版对心形数独那题的求解过程没有一次猜测(即全程处在猜测级别 0 的状态),而且只用到第一类候选值集合收缩算法,因为从输出信息只有 shrunken by group 类信息,如:
row 4 shrunken by group row 4 shrunken by group row 7 shrunken by group row 7 shrunken by group ... row 9 shrunken by group col 2 shrunken by group
而在上面的“最难”数独题解输出信息,却出现了如下的信息:
[9,1]: 2 3 5 6 7 shrunken to 3 5 6 worked by row-ply2.
[9,3]: 2 5 6 7 shrunken to 5 6 worked by row-ply2.
[9,4]: 2 3 7 8 shrunken to 3 8 worked by row-ply2.
[9,5]: 1 2 3 6 7 8 shrunken to 1 3 6 8 worked by row-ply2.
[9,6]: 1 2 3 6 8 shrunken to 1 3 6 8 worked by row-ply2.
[6,3]: 2 4 6 7 9 shrunken to 7 worked by col-ply2.
10) col 3 shrunken ply-2 by vblk 1
末行打头的 10) 指示这是当时进到第 10 步的处理过程中输出的信息。结合代码,可知这些信息是第二类候选值集合收缩算法输出的。2.0(2.1)版里实现了 4 种第二类收缩算法,分别标识为 row-ply1、row-ply2、col-ply1、col-ply2。
标蓝的信息是说第 9 行有 5 个 0-cell 的候选值集合得到了收缩,比如 [9,1] 的候选值集合从 {2, 3, 5, 6, 7} 收缩到 {3, 5, 6},采用的收缩算法是 row-ply2。
标绿的信息,第一行说明 [6,3] 这个 0-cell 的候选值集合从 {2, 4, 6, 7, 9} 收缩为 {7},采用的收缩算法是 col-ply2;第二行则进一步说明第 3 列收缩到了可以填值的地步(因为 [6,3] 只有一个候选值)。
第 10 步里的收缩过程分析
以下从头开始求解过程,并用 runtil 9 进到第 10 步开始时的上下文深入分析一下:
H:\Read\num\Release>sudoku.exe Order please: load-quiz h:\s.txt Quiz loaded. Order please: runtil 9 3) Guess [8,7] level 1 at 1 out of 2 5) Guess [7,7] level 2 at 1 out of 2 6) row 6 shrunken by group 8) blk 9 shrunken by group 800 000 000 003 600 000 070 090 200 050 007 000 000 045 700 000 100 035 001 000 568 008 500 319 090 000 400 Steps:9 Candidates: [1,2]: 1 2 4 6 [1,3]: 2 4 5 6 9 [1,4]: 2 3 4 7 [1,5]: 1 2 3 5 7 [1,6]: 1 2 3 4 [1,7]: 1 6 9 [1,8]: 4 5 7 9 [1,9]: 1 3 4 6 7 [2,1]: 1 2 4 5 9 [2,2]: 1 2 4 [2,5]: 1 2 5 7 8 [2,6]: 1 2 4 8 [2,7]: 1 8 9 [2,8]: 4 5 7 8 9 [2,9]: 1 4 7 [3,1]: 1 4 5 6 [3,3]: 4 5 6 [3,4]: 3 4 8 [3,6]: 1 3 4 8 [3,8]: 4 5 8 [3,9]: 1 3 4 6 [4,1]: 1 2 3 4 6 9 [4,3]: 2 4 6 9 [4,4]: 2 3 8 9 [4,5]: 2 3 6 8 [4,7]: 1 6 8 9 [4,8]: 2 4 8 9 [4,9]: 1 2 4 6 [5,1]: 1 2 3 6 9 [5,2]: 1 2 3 6 8 [5,3]: 2 6 9 [5,4]: 2 3 8 9 [5,8]: 2 8 9 [5,9]: 1 2 6 [6,1]: 2 4 6 7 9 [6,2]: 2 4 6 8 [6,3]: 2 4 6 7 9 [6,5]: 2 6 8 [6,6]: 2 6 8 9 [6,7]: 6 8 9 [7,1]: 2 3 4 7 [7,2]: 2 3 4 [7,4]: 2 3 4 7 9 [7,5]: 2 3 7 [7,6]: 2 3 4 9 [8,1]: 2 4 6 7 [8,2]: 2 4 6 [8,5]: 2 6 7 [8,6]: 2 4 6 [9,1]: 2 3 5 6 7 [9,3]: 2 5 6 7 [9,4]: 2 3 7 8 [9,5]: 1 2 3 6 7 8 [9,6]: 1 2 3 6 8 [9,8]: 2 7 [9,9]: 2 7 The foremost cell with 2 candidate(s) at [9,8] At guess level 2 [7,7] 1 Run time: 40 milliseconds; steps: 9, solution sum: 0. Order please:
从走完 9 步的信息看,所有 0-cell 的候选值个数均大于 1,下一步不能直接做填空操作,于是会调用 filterCandidates 依次尝试第一类和第二类收缩算法。第一类收缩算法有 3 种应用,即 row-grp、col-grp、blk-grp。从前面的输出信息看,第 10 步走到了 row-ply2(收缩了第 9 行)和 col-ply2(收缩了第 3 列)。先具体看一下 row-grp、col-grp 为何没能收缩第 9 行和第 3 列。
第 9 步走完后,第 9 行还有 7 个空位,即:
090 000 400
每个空位的候选值情况为:
[9,1]: 2 3 5 6 7 [9,3]: 2 5 6 7
[9,4]: 2 3 7 8 [9,5]: 1 2 3 6 7 8 [9,6]: 1 2 3 6 8
[9,8]: 2 7 [9,9]: 2 7
按 filterOneGroup 接口里实现的算法,这里的候选值,即 1、2、3、5、6、7、8 全都在两个或更多个空位出现,因而不能对第 9 行实施收缩。但实际上,这个 grp 算法是有改进空间的。当前实现的 grp 算法只检查有没有完全收缩(即收缩到只有一个候选值)的空位,而没有考察不完全收缩的情形。比如这里的例子,2 和 7 都出现在两个空位,且它们的空位集合都是 {[9,8], [9,9]},即交集里的空位数也是 2,这就可以推定 2 和 7 只能填到 [9,8] 和 [9,9] 两个空位上,于是第 9 行的其它空位上的候选值 2 和 7 都可以被剔除掉。这正好对应前文中的如下信息:
[9,1]: 2 3 5 6 7 shrunken to 3 5 6 worked by row-ply2. [9,3]: 2 5 6 7 shrunken to 5 6 worked by row-ply2. [9,4]: 2 3 7 8 shrunken to 3 8 worked by row-ply2. [9,5]: 1 2 3 6 7 8 shrunken to 1 3 6 8 worked by row-ply2. [9,6]: 1 2 3 6 8 shrunken to 1 3 6 8 worked by row-ply2.
就是说,第 9 行的不完全收缩是在 row-ply2 里实施的。
再看第 3 列,为节省空间,用 /030 000 180/ 表示。其 6 个空位的候选值情况为:
[1,3]: 2 4 5 6 9 [3,3]: 4 5 6 [4,3]: 2 4 6 9
[5,3]: 2 6 9 [6,3]: 2 4 6 7 9 [9,3]: 2 5 6 7
这里的候选值,即 2、4、5、6、7、9 全都在两个或更多个空位出现,因而不能对第 3 列实施完全收缩。候选值 7 有两个空位,5 有三个,4 和 9 都有四个,2 有五个,6 有六个,也没有不完全收缩的可能性。
执行一次 step 命令,输出信息如下:
Order please: step [9,1]: 2 3 5 6 7 shrunken to 3 5 6 worked by row-ply2. [9,3]: 2 5 6 7 shrunken to 5 6 worked by row-ply2. [9,4]: 2 3 7 8 shrunken to 3 8 worked by row-ply2. [9,5]: 1 2 3 6 7 8 shrunken to 1 3 6 8 worked by row-ply2. [9,6]: 1 2 3 6 8 shrunken to 1 3 6 8 worked by row-ply2. [6,3]: 2 4 6 7 9 shrunken to 7 worked by col-ply2. 10) col 3 shrunken ply-2 by vblk 1 800 000 000 003 600 000 070 090 200 050 007 000 000 045 700 007 100 035 001 000 568 008 500 319 090 000 400 Steps:11 Candidates: [1,2]: 1 2 4 6 [1,3]: 2 4 5 6 9 [1,4]: 2 3 4 7 [1,5]: 1 2 3 5 7 [1,6]: 1 2 3 4 [1,7]: 1 6 9 [1,8]: 4 5 7 9 [1,9]: 1 3 4 6 7 [2,1]: 1 2 4 5 9 [2,2]: 1 2 4 [2,5]: 1 2 5 7 8 [2,6]: 1 2 4 8 [2,7]: 1 8 9 [2,8]: 4 5 7 8 9 [2,9]: 1 4 7 [3,1]: 1 4 5 6 [3,3]: 4 5 6 [3,4]: 3 4 8 [3,6]: 1 3 4 8 [3,8]: 4 5 8 [3,9]: 1 3 4 6 [4,1]: 1 2 3 4 6 9 [4,3]: 2 4 6 9 [4,4]: 2 3 8 9 [4,5]: 2 3 6 8 [4,7]: 1 6 8 9 [4,8]: 2 4 8 9 [4,9]: 1 2 4 6 [5,1]: 1 2 3 6 9 [5,2]: 1 2 3 6 8 [5,3]: 2 6 9 [5,4]: 2 3 8 9 [5,8]: 2 8 9 [5,9]: 1 2 6 [6,1]: 2 4 6 9 [6,2]: 2 4 6 8 [6,5]: 2 6 8 [6,6]: 2 6 8 9 [6,7]: 6 8 9 [7,1]: 2 3 4 7 [7,2]: 2 3 4 [7,4]: 2 3 4 7 9 [7,5]: 2 3 7 [7,6]: 2 3 4 9 [8,1]: 2 4 6 7 [8,2]: 2 4 6 [8,5]: 2 6 7 [8,6]: 2 4 6 [9,1]: 3 5 6 [9,3]: 5 6 [9,4]: 3 8 [9,5]: 1 3 6 8 [9,6]: 1 3 6 8 [9,8]: 2 7 [9,9]: 2 7 The foremost cell with 2 candidate(s) at [9,3] At guess level 2 [7,7] 1 Order please:
可以看到,第 9 行如期实施了不完全收缩,第 3 列实施了一次完全收缩,即 [6,3] 位置填上了 7。以下分析一下这两次第二类收缩算法(row-ply2 和 col-ply2)具体是如何实施的:
800 000 000 003 600 000 070 090 200 050 007 000 000 045 700 000 100 035 001 000 568 008 500 319 090 000 400
走完 9 步后,对第 9 行考察 row-ply2。第 9 行横向跨越底部三个宫。先看左下宫,其中有 1 和 8 待填到第 9 行的第二节和第三节,对照当时第 9 行各空位候选值分布情况:
[9,1]: 2 3 5 6 7 [9,3]: 2 5 6 7 [9,4]: 2 3 7 8 [9,5]: 1 2 3 6 7 8 [9,6]: 1 2 3 6 8 [9,8]: 2 7 [9,9]: 2 7
可知,1 和 8 只能填到第二节的三个空位,且 [9,4] 的候选值集合里没有 1,进一步可知 1 只能填到 [9,5] 或 [9,6]。但凭借这些信息还不能对 [9,5] 或 [9,6] 实施收缩。
接着考察下中宫,其中有 5待填到第 9 行的第一节和第三节的空位,同样根据候选值分布情况可知,5 只能填到 [9,1] 或 [9,3],凭借这个信息还是不能对 [9,1] 或 [9,3] 实施收缩。
考察右下宫,其中有 5、6、8、3、1、9 待填到第 9 行的第一节和第二节,9 已经在第一节里,剩余 5 个候选值 {5, 6, 8, 3, 1} 正好填到第一节和第二节的 5 个空位里。
[9,1] 的候选值集合为 {2, 3, 5, 6, 7},{2, 3, 5, 6, 7} ∩ {5, 6, 8, 3, 1} = {3, 5, 6},这便是收缩后的 [9,1] 的候选值集合。同样:
[9,3]: {2, 5, 6, 7} ∩ {5, 6, 8, 3, 1} = {5, 6}
[9,4]: {2, 3, 7, 8} ∩ {5, 6, 8, 3, 1} = {3, 8}
[9,5]: {1, 2, 3, 6, 7, 8} ∩ {5, 6, 8, 3, 1} = {1, 3, 6, 8}
[9,6]: {1, 2, 3, 6, 8} ∩ {5, 6, 8, 3, 1} = {3, 5, 6}
以上就是 row-ply2 对第 9 行的不完全收缩过程分析。接着来看 col-ply2 对第 3 列的完全收缩是如何实施的:
800 000 000 003 600 000 070 090 200 050 007 000 000 045 700
000 100 035
001 000 568
008 500 319
090 000 400
第 3 列竖着跨越左侧三个宫,先考察左上宫,其中的 8 和 7 待填到第 3 列的第二节和第三节。第三节里已经有 8,于是 7 可填的空位有 [4,3]、[5,3]、[6,3]、[9,3]。这几个空位的候选值集合分别为:
[4,3]: 2 4 6 9 [5,3]: 2 6 9 [6,3]: 2 4 6 7 9 [9,3]: 5 6
于是 7 只能填到 [6,3] 位置上。由上面的分析可知, [9,3] 的候选值集合是在第 10 步过程中通过 row-ply2 才从 {2, 5, 6, 7} 收缩为 {5, 6} 的,而此处正因为 [9,3] 的这次不完全收缩才使得第 3 列通过 col-ply2 发生了一次完全收缩,[6,3] 的候选值集合从 {2, 4, 6, 7, 9} 收缩为 {7},即对应如下的输出信息:
[6,3]: 2 4 6 7 9 shrunken to 7 worked by col-ply2.
10) col 3 shrunken ply-2 by vblk 1
第二行里的 vblk 1 是指 vertical block 1,第 3 列竖直跨越三个宫,vblk 1 指竖列的第 1 宫(即左上宫),考察这一宫已经推定会发生完全收缩,便不再继续考察另外两个宫,而是进到下一步(即第 11 步),给单候选值的空位填值并对关联空位的候选值集合做相应调整处理。
程序改进思路
上面的分析过程中,已经有一个程序改进思路,即增强 grp 算法可以处理不完全收缩的情形。而从刚才的两次关联收缩的示例分析可以提炼出另一个改进程序的思路,filterCandidates 接口里有五轮收缩筛查,按顺序分别是 row-grp、col-grp、blk-grp、row-ply、col-ply。刚才的收缩接力得以在一次 filterCandidates 调用中实施,也是因为第 9 行的不完全收缩发生在第四轮的 row-ply 筛查阶段,而第 3 列的完全收缩发生在第五轮的 col-ply 筛查阶段。这样的两次关联收缩,倘若第一次不完全收缩发生在第五轮的 col-ply 筛查阶段,就会导致第二次的完全收缩接续不上,进而多加一次猜测行为。这是一个可改进之处。
上面示例中的两次关联收缩中,第二次收缩,即第 2 列的 [6,3] 收缩为 {7},是由 col-ply 算法检测到的,但显然通过更简单的 col-grp 算法一样可以检测到。因此可以推测:grp 算法增强后有可能就不需要使用第二类收缩算法了,这一点后续再做检验。
接着分析一个综合的过程。
第 1778 步之后的过程分析
第二个 run 命令的输出信息的末尾部分有如下信息:
1779) row 1 shrunken by group
[5,1]: 1 2 3 6 9 shrunken to 1 6 worked by row-ply1.
[5,2]: 1 2 3 6 8 shrunken to 1 6 worked by row-ply1.
[5,4]: 2 3 8 9 shrunken to 3 worked by row-ply2.
1781) row 5 shrunken ply-2 by blk 3
1784) row 4 shrunken by group
1786) col 2 shrunken by group
1786) shrinking 9 from blk[5,1] went wrong
1787) No more solution (solution sum is 1).
从头开始求解过程,并用 runtil 1778 进到第 1779 步开始时的上下文:
H:\Read\num\Release>sudoku.exe Order please: load-quiz h:\s.txt Quiz loaded. Order please: runtil 1778 3) Guess [8,7] level 1 at 1 out of 2 5) Guess [7,7] level 2 at 1 out of 2
...
Done [steps:803, solution sum:1]. Run time: 460 milliseconds; steps: 803, solution sum: 1.
Order please:
runtil 1778
...
800 000 300
003 600 800
076 090 250
050 067 100
000 045 700
000 100 635
001 000 568
008 500 910
090 000 400
Steps:1778
Candidates:
[1,2]: 1 2 4 [1,3]: 2 4 5 9 [1,4]: 2 4 7
[1,5]: 1 2 5 7 [1,6]: 1 2 4 [1,8]: 4 7 9
[1,9]: 1 4 6 7 9 [2,1]: 1 2 4 5 9 [2,2]: 1 2 4
[2,5]: 1 2 5 7 [2,6]: 1 2 4 [2,8]: 4 7 9
[2,9]: 1 4 7 9 [3,1]: 1 4 [3,4]: 3 8
[3,6]: 3 8 [3,9]: 1 4 [4,1]: 2 3 4 9
[4,3]: 2 4 9 [4,4]: 2 3 8 9 [4,8]: 2 4 8 9
[4,9]: 2 4 9 [5,1]: 1 2 3 6 9 [5,2]: 1 2 3 6 8
[5,3]: 2 9 [5,4]: 2 3 8 9 [5,8]: 2 8 9
[5,9]: 2 9 [6,1]: 2 4 7 9 [6,2]: 2 4 8
[6,3]: 2 4 7 9 [6,5]: 2 8 [6,6]: 2 8 9
[7,1]: 2 3 4 7 [7,2]: 2 3 4 [7,4]: 2 3 4 7 9
[7,5]: 2 3 7 [7,6]: 2 3 4 9 [8,1]: 2 3 4 6 7
[8,2]: 2 3 4 6 [8,5]: 2 3 7 [8,6]: 2 3 4 6
[8,9]: 2 3 7 [9,1]: 2 3 5 6 7 [9,3]: 2 5 7
[9,4]: 2 3 7 8 [9,5]: 1 2 3 7 8 [9,6]: 1 2 3 6 8
[9,8]: 2 7 [9,9]: 2 3 7
The foremost cell with 2 candidate(s) at [3,1]
At guess level 5 [3,3] 2
Run time: 600 milliseconds; steps: 1778, solution sum: 1.
Order please:
可以看到第一次 runtil 1778 命令因为求解成功被中断了,第二次 runtil 1778 命令才继续走到第 1779 步开始时的上下文。由 The foremost cell with 2 candidate(s) at [3,1] 可知,当时不能直接做填空操作。先分析如下信息:
1779) row 1 shrunken by group
对应的收缩过程。当时第 1 行为:800 000 300,其中 7 个空位的候选值分布情况为:
[1,2]: 1 2 4 [1,3]: 2 4 5 9 [1,4]: 2 4 7 [1,5]: 1 2 5 7 [1,6]: 1 2 4 [1,8]: 4 7 9 [1,9]: 1 4 6 7 9
由 grp 算法可推定 [1,9] = 6。与如下的 step 命令输出结果相符:
Order please: step 1779) row 1 shrunken by group 800 000 306 003 600 800 076 090 250 050 067 100 000 045 700 000 100 635 001 000 568 008 500 910 090 000 400 Steps:1780 Candidates: [1,2]: 1 2 4 [1,3]: 2 4 5 9 [1,4]: 2 4 7 [1,5]: 1 2 5 7 [1,6]: 1 2 4 [1,8]: 4 7 9 [2,1]: 1 2 4 5 9 [2,2]: 1 2 4 [2,5]: 1 2 5 7 [2,6]: 1 2 4 [2,8]: 4 7 9 [2,9]: 1 4 7 9 [3,1]: 1 4 [3,4]: 3 8 [3,6]: 3 8 [3,9]: 1 4 [4,1]: 2 3 4 9 [4,3]: 2 4 9 [4,4]: 2 3 8 9 [4,8]: 2 4 8 9 [4,9]: 2 4 9 [5,1]: 1 2 3 6 9 [5,2]: 1 2 3 6 8 [5,3]: 2 9 [5,4]: 2 3 8 9 [5,8]: 2 8 9 [5,9]: 2 9 [6,1]: 2 4 7 9 [6,2]: 2 4 8 [6,3]: 2 4 7 9 [6,5]: 2 8 [6,6]: 2 8 9 [7,1]: 2 3 4 7 [7,2]: 2 3 4 [7,4]: 2 3 4 7 9 [7,5]: 2 3 7 [7,6]: 2 3 4 9 [8,1]: 2 3 4 6 7 [8,2]: 2 3 4 6 [8,5]: 2 3 7 [8,6]: 2 3 4 6 [8,9]: 2 3 7 [9,1]: 2 3 5 6 7 [9,3]: 2 5 7 [9,4]: 2 3 7 8 [9,5]: 1 2 3 7 8 [9,6]: 1 2 3 6 8 [9,8]: 2 7 [9,9]: 2 3 7 The foremost cell with 2 candidate(s) at [3,1] At guess level 5 [3,3] 2 Order please:
第 1779 步对第 1 行实施 grp 算法把 [1,9] 的候选值集合收缩为单值集合 {6},第 1780 步把 6 填入 [1,9] 并从关联的空位(第 1 行,第 9 列,第 3 宫)的候选值集合里去掉 6。这里似乎也有一个可改进之处:对一个空位填值,而这个值是唯一推定出来的,就无需对关联的空位的候选值集合做调整处理。
接着分析如下信息:
[5,1]: 1 2 3 6 9 shrunken to 1 6 worked by row-ply1. [5,2]: 1 2 3 6 8 shrunken to 1 6 worked by row-ply1. [5,4]: 2 3 8 9 shrunken to 3 worked by row-ply2. 1781) row 5 shrunken ply-2 by blk 3
此时各行填值情况为:
800 000 306 003 600 800 076 090 250 050 067 100 000 045 700 000 100 635 001 000 568 008 500 910 090 000 400
第 5 行横跨中间三个宫,除去第 5 行,这三个宫的已填数集合分别为 {5}、{6, 7, 1}、{1, 6, 3, 5}。后面两个集合的交集为 {1, 6},即 1 和 6 要填入到第 5 行的第一节里,这一节里的三个空位的候选值分布情况为:
[5,1]: 1 2 3 6 9 [5,2]: 1 2 3 6 8 [5,3]: 2 9
由于 {2, 9} ∩ {1, 6} = {},说明 [5,3] 这个空位上 1 和 6 都不能填,因此 1 和 6 只能填入 [5, 1] 和 []5,2] 两个空位,于是有
[5,1]: {1, 2, 3, 6, 9} ∩ {1, 6} = {1, 6}
[5,2]: {1, 2, 3, 6, 8} ∩ {1, 6} = {1, 6}
上面是 row-ply1 by vlk 1 的考察。继续看 row-ply1 by vlk 2 的情形,这时 {5} ∩ {1, 6, 3, 5} = {5},且第 5 行的第二节里已经有 5,没有考察价值;row-ply1 by vlk 3 也没有考察价值,因为 {5} ∩ {6, 7, 1} = {}。
接着开始 row-ply2 by blk 1 的考察,{5} 里只有 5,待填入第 5 行的第二节和第三节,而第二节里已经有 5,没有考察价值。
row-ply2 by blk 2,{6, 7, 1} 里的这三个数待填入第 5 行的第一节和第三节的 5 个空位里,且 7 已经在第三节里,剔除 7,即 1 和 6 待填入第一节和第三节的 5 个空位里,而这 5 个空位的候选值分布情况为:
[5,1]: 1 6 [5,2]: 1 6 [5,3]: 2 9 [5,8]: 2 8 9 [5,9]: 2 9
后三个的候选值都没有 1 和 6,剔除,这时就是 1 和 6 待填入两个空位:[5,1] 和 [5,2],而这两个空位的候选值集合都已经是 {1, 6},不能做进一步的收缩。
继续看 row-ply2 by blk 3,{1, 6, 3, 5} 里的四个数待填入第5 行的第一节和第二节的四个空位里。5 已经在第二节里,即 1、6、3 待填入 4 个空位。这四个空位的候选值分布情况为:
[5,1]: 1 6 [5,2]: 1 6 [5,3]: 2 9 [5,4]: 2 3 8 9
[5,3] 的候选值集合里没有 1、6、3,剔除。这时就是 1、6、3 待填入三个空位:[5,1]、[5,2] 和 [5,4]。取交集易知 [5,4] = 3。与如下的 step 命令输出结果相符:
Order please: step [5,1]: 1 2 3 6 9 shrunken to 1 6 worked by row-ply1. [5,2]: 1 2 3 6 8 shrunken to 1 6 worked by row-ply1. [5,4]: 2 3 8 9 shrunken to 3 worked by row-ply2. 1781) row 5 shrunken ply-2 by blk 3 800 000 306 003 600 800 076 090 250 050 067 100 000 345 700 000 100 635 001 000 568 008 500 910 090 000 400 Steps:1782 Candidates: [1,2]: 1 2 4 [1,3]: 2 4 5 9 [1,4]: 2 4 7 [1,5]: 1 2 5 7 [1,6]: 1 2 4 [1,8]: 4 7 9 [2,1]: 1 2 4 5 9 [2,2]: 1 2 4 [2,5]: 1 2 5 7 [2,6]: 1 2 4 [2,8]: 4 7 9 [2,9]: 1 4 7 9 [3,1]: 1 4 [3,4]: 8 [3,6]: 3 8 [3,9]: 1 4 [4,1]: 2 3 4 9 [4,3]: 2 4 9 [4,4]: 2 8 9 [4,8]: 2 4 8 9 [4,9]: 2 4 9 [5,1]: 1 6 [5,2]: 1 6 [5,3]: 2 9 [5,8]: 2 8 9 [5,9]: 2 9 [6,1]: 2 4 7 9 [6,2]: 2 4 8 [6,3]: 2 4 7 9 [6,5]: 2 8 [6,6]: 2 8 9 [7,1]: 2 3 4 7 [7,2]: 2 3 4 [7,4]: 2 4 7 9 [7,5]: 2 3 7 [7,6]: 2 3 4 9 [8,1]: 2 3 4 6 7 [8,2]: 2 3 4 6 [8,5]: 2 3 7 [8,6]: 2 3 4 6 [8,9]: 2 3 7 [9,1]: 2 3 5 6 7 [9,3]: 2 5 7 [9,4]: 2 7 8 [9,5]: 1 2 3 7 8 [9,6]: 1 2 3 6 8 [9,8]: 2 7 [9,9]: 2 3 7 The foremost cell with 1 candidate(s) at [3,4] At guess level 5 [3,3] 2 Order please:
走完 1782 步时,[3,4] 的候选值集合成为单值集合 {8},下一步可以直接做填空操作,同时也说明上面那个猜测结论是不对的:对一个空位填值,而这个值是唯一推定出来的,就无需对关联的空位的候选值集合做调整处理。
下一步到走完 1783 步,即:
Order please: step 800 000 306 003 600 800 076 893 250 050 067 100 000 345 700 000 100 635 001 000 568 008 500 910 090 000 400 Steps:1783 Candidates: [1,2]: 1 2 4 [1,3]: 2 4 5 9 [1,4]: 2 4 7 [1,5]: 1 2 5 7 [1,6]: 1 2 4 [1,8]: 4 7 9 [2,1]: 1 2 4 5 9 [2,2]: 1 2 4 [2,5]: 1 2 5 7 [2,6]: 1 2 4 [2,8]: 4 7 9 [2,9]: 1 4 7 9 [3,1]: 1 4 [3,9]: 1 4 [4,1]: 2 3 4 9 [4,3]: 2 4 9 [4,4]: 2 9 [4,8]: 2 4 8 9 [4,9]: 2 4 9 [5,1]: 1 6 [5,2]: 1 6 [5,3]: 2 9 [5,8]: 2 8 9 [5,9]: 2 9 [6,1]: 2 4 7 9 [6,2]: 2 4 8 [6,3]: 2 4 7 9 [6,5]: 2 8 [6,6]: 2 8 9 [7,1]: 2 3 4 7 [7,2]: 2 3 4 [7,4]: 2 4 7 9 [7,5]: 2 3 7 [7,6]: 2 4 9 [8,1]: 2 3 4 6 7 [8,2]: 2 3 4 6 [8,5]: 2 3 7 [8,6]: 2 4 6 [8,9]: 2 3 7 [9,1]: 2 3 5 6 7 [9,3]: 2 5 7 [9,4]: 2 7 [9,5]: 1 2 3 7 8 [9,6]: 1 2 6 8 [9,8]: 2 7 [9,9]: 2 3 7 The foremost cell with 2 candidate(s) at [3,1] At guess level 5 [3,3] 2 Order please:
下一个 step 命令的输出:
Order please: step 1784) row 4 shrunken by group 800 000 306 003 600 800 076 893 250 350 067 180 000 345 700 000 100 635 001 000 568 008 500 910 090 000 400 Steps:1785 Candidates: [1,2]: 1 2 4 [1,3]: 2 4 5 9 [1,4]: 2 4 7 [1,5]: 1 2 5 7 [1,6]: 1 2 4 [1,8]: 4 7 9 [2,1]: 1 2 4 5 9 [2,2]: 1 2 4 [2,5]: 1 2 5 7 [2,6]: 1 2 4 [2,8]: 4 7 9 [2,9]: 1 4 7 9 [3,1]: 1 4 [3,9]: 1 4 [4,3]: 2 4 9 [4,4]: 2 9 [4,9]: 2 4 9 [5,1]: 1 6 [5,2]: 1 6 [5,3]: 2 9 [5,8]: 2 9 [5,9]: 2 9 [6,1]: 2 4 7 9 [6,2]: 2 4 8 [6,3]: 2 4 7 9 [6,5]: 2 8 [6,6]: 2 8 9 [7,1]: 2 4 7 [7,2]: 2 3 4 [7,4]: 2 4 7 9 [7,5]: 2 3 7 [7,6]: 2 4 9 [8,1]: 2 4 6 7 [8,2]: 2 3 4 6 [8,5]: 2 3 7 [8,6]: 2 4 6 [8,9]: 2 3 7 [9,1]: 2 5 6 7 [9,3]: 2 5 7 [9,4]: 2 7 [9,5]: 1 2 3 7 8 [9,6]: 1 2 6 8 [9,8]: 2 7 [9,9]: 2 3 7 The foremost cell with 2 candidate(s) at [3,1] At guess level 5 [3,3] 2 Order please:
第 4 行由 050 067 100 变到了 350 067 180
[4,1]: 2 3 4 9 [4,3]: 2 4 9 [4,4]: 2 9 [4,8]: 2 4 8 9 [4,9]: 2 4 9
再运行一次 step 命令,输出信息为:
Order please: step 1786) col 2 shrunken by group 1786) shrinking 9 from blk[5,1] went wrong 1787) No more solution (solution sum is 1). 800 000 306 003 600 800 076 893 250 350 067 180 000 345 700 080 129 635 001 000 568 008 500 910 090 000 400 Invalid quiz [steps:1787] - no more solution (solution sum is 1) Order please:
从输出信息看,先是:
1786) col 2 shrunken by group
第 2 列 从 /007 500 009/ 变到了 /007 508 009/。
第 2 列的空位候选值分布情况:
[1,2]: 1 2 4 [2,2]: 1 2 4 [5,2]: 1 6 [6,2]: 2 4 8 [7,2]: 2 3 4 [8,2]: 2 3 4 6
[6,2] = 8,符合预期。接着的输出信息是:
1786) shrinking 9 from blk[5,1] went wrong
1787) No more solution (solution sum is 1).
从 1785 步到 1786 步,第 6 行也从 000 100 635 变到了 080 129 635。
1785 步时,第 6 行各空位的候选值分布情况为:
[6,1]: 2 4 7 9 [6,2]: 2 4 8
[6,3]: 2 4 7 9 [6,5]: 2 8 [6,6]: 2 8 9
因为在 1786 步有 [6,2] = 8,所以第 6 行里剩余空位的候选值应该去除 8,即:
[6,1]: 2 4 7 9 [6,3]: 2 4 7 9 [6,5]: 2 [6,6]: 2 9
于是有 [6,5] = 2,[6,6] = 9。第 6 行里剩余空位的候选值分布进一步收缩为:
[6,1]: 4 7 [6,3]: 4 7
接着看第 5 宫的情况:
350 067 180 000 345 700 080 129 635
该宫里只剩第一个 cell 为空位,即 blk[5,1],该空位用行标和列标表示为 [4,4],1785 步时其候选值集合为:
[4,4]: 2 9
1786 步的过程中,2 和 9 依次填入 [6,5] 和 [6,6],这两个 cell 和 [4,4] 同属第 5 宫,于是 2 和 9 依次会从 [4,4] 的候选值集合里剔除掉,导致 [4,4] 的候选值集合为空,从而会输出:
1786) shrinking 9 from blk[5,1] went wrong
且当时无法做平级猜测调整或降级猜测调整,于是会输出:
1787) No more solution (solution sum is 1).