SudokuSolver 2.3 程序實現
用C++實現的數獨解題程序 SudokuSolver 2.2 及實例分析 里新發現了一處可以改進 grp 算法的地方,本次版本實現了對應的改進 grp 算法。
CQuizDealer 類聲明部分的修改
增加了兩個私有接口:
bool sameCandidates(u8 cel1, u8 cel2); u8 anotherGreenWorld(u8* pGrp); u8 incompleteShrinkByAGW(u8 times, u8* pTimesVals, u8* pValsCells, u8* pGrp);
anotherGreenWorld,這個略顯隨意和突兀的命名,來自 Brian Eno 於 1977 年發行的專輯《Another Green World》。
filterOneGroup 接口實現的小修改
把末尾的
return RET_PENDING;
改為
return anotherGreenWorld(pGrp);
anotherGreenWorld 接口實現
1 u8 CQuizDealer::anotherGreenWorld(u8* pGrp) 2 { 3 u8 valsCells[100] = {0}; 4 u8 size = pGrp[0]; 5 for (u8 idx = 1; idx <= size; ++idx) { 6 u8 valSum = m_seqCell[pGrp[idx]].candidates[0]; 7 for (u8 vidx = 1; vidx <= valSum; ++vidx) { 8 u8 val = m_seqCell[pGrp[idx]].candidates[vidx]; 9 u8 pos = val * 10 + valsCells[val]; 10 valsCells[pos] = pGrp[idx]; 11 valsCells[val] += 1; 12 } 13 } 14 15 u8 timesVals[100] = {0}; 16 for (u8 val = 1; val < 10; ++val) { 17 u8 times = valsCells[val]; 18 if (times == 0) 19 continue; 20 u8 pos = times * 10 + timesVals[times]; 21 timesVals[pos] = val; 22 timesVals[times] += 1; 23 } 24 25 for (u8 times = 2; times <= 6; ++times) { 26 if (times > timesVals[times]) 27 continue; 28 u8 ret = incompleteShrinkByAGW(times, timesVals, valsCells, pGrp); 29 if (ret != RET_PENDING) 30 return ret; 31 } 32 return RET_PENDING; 33 }
incompleteShrinkByAGW 接口實現
1 u8 CQuizDealer::incompleteShrinkByAGW(u8 times, u8* pTimesVals, u8* pValsCells, u8* pGrp) 2 { 3 u8 combi[10] = {0}; 4 combi[0] = pTimesVals[times]; 5 for (u8 idx = 0; idx < times; ++idx) 6 combi[idx + 1] = idx; 7 u8 base = times * 10; 8 while (true) { 9 u8 celSet[10] = {0}; 10 u8 valSet[10] = {0}; 11 if (matchValsCells(times, combi, pTimesVals, pValsCells, celSet, valSet)) { 12 valSet[0] = times; 13 bool shrunken = false; 14 for (u8 idx = 1; idx <= times; ++idx) { 15 u8 cel = celSet[idx]; 16 u8 candiSum = m_seqCell[cel].candidates[0]; 17 if (candiSum == times) 18 continue; 19 if (candiSum < times) { 20 printf("AGW: [%d,%d] candidates %d lower than times %d!\n", (int)(cel / 9 + 1), (int)(cel % 9 + 1), (int)candiSum, (int)times); 21 return RET_WRONG; 22 } 23 shrunken = true; 24 for (u8 pos = 1; pos <= candiSum; ++pos) { 25 u8 val = m_seqCell[cel].candidates[pos]; 26 if (!inSet(val, valSet)) { 27 removeVal(m_seqCell[cel].candidates, val); 28 printf("%d shrunken out of [%d,%d] by AGW\n", (int)val, (int)(cel / 9 + 1), (int)(cel % 9 + 1)); 29 } 30 } 31 } 32 if (shrunken) { 33 printCelSetValSet(celSet, valSet); 34 return RET_SHRUNKEN; 35 } 36 } 37 if (!move2NextCombi(times, combi)) 38 break; 39 } 40 return RET_PENDING; 41 }
輔助函數 move2NextCombi 的實現
1 bool move2NextCombi(u8 times, u8* pCombi) 2 { 3 if (pCombi[1] == pCombi[0] - times) 4 return false; 5 u8 bound = pCombi[0] - 1; 6 for (u8 idx = times; idx > 0; --idx) { 7 if (pCombi[idx] < bound - (times - idx)) { 8 pCombi[idx] += 1; 9 for (u8 tail = idx + 1; tail <= times; ++tail) 10 pCombi[tail] = pCombi[tail - 1] + 1; 11 return true; 12 } 13 } 14 return true; 15 }
輔助函數 matchValsCells 的實現
1 bool matchValsCells(u8 times, u8* pCombi, u8* pTimesVals, u8* pValsCells, u8* pCelSet, u8* pValSet) 2 { 3 u8 base = times * 10; 4 5 for (u8 idx = 1; idx <= times; ++idx) { 6 u8 pos = base + pCombi[idx]; 7 u8 val = pTimesVals[pos]; 8 pValSet[idx] = val; 9 if (pCelSet[0] == 0) { 10 pCelSet[0] = times; 11 memcpy(pCelSet + 1, &(pValsCells[val * 10]), times); 12 continue; 13 } 14 if (0 != memcmp(pCelSet + 1, &(pValsCells[val * 10]), times)) 15 return false; 16 } 17 return true; 18 }
輔助函數 printCelSetValSet 的實現
1 void printCelSetValSet(u8* pCelSet, u8* pValSet) 2 { 3 printf("CelSet: "); 4 for (u8 idx = 1; idx <= pCelSet[0]; ++idx) { 5 u8 cel = pCelSet[idx]; 6 printf("[%d,%d] ", (int)(cel / 9 + 1), (int)(cel % 9 + 1)); 7 } 8 printf("ValSet: "); 9 for (u8 idx = 1; idx <= pValSet[0]; ++idx) { 10 printf("%d ", (int)pValSet[idx]); 11 } 12 printf("\n"); 13 }
版本信息調整
// 1.0 2021/9/20 // 2.0 2021/10/2 // 2.1 2021/10/4 // 2.2 2021/10/10 #define STR_VER "Sudoku Solver 2.3 2021/10/17 by readalps\n\n"
實例分析
繼續以 SudokuSolver 1.0:用C++實現的數獨解題程序 【二】 里試驗過的“最難”數獨題為例做分析。第二次 run 命令的輸出中還有兩處 “ply” 信息,如下所示:
1286) col 9 complete shrunken by group [9,4]: 2 3 7 8 shrunken to 3 8 worked by row-ply1. [9,5]: 1 2 3 6 7 8 shrunken to 1 3 8 worked by row-ply1. [9,6]: 1 2 3 6 8 shrunken to 1 3 8 worked by row-ply1. [6,6]: 2 6 9 shrunken to 6 9 worked by col-ply2. [7,6]: 2 9 shrunken to 9 worked by col-ply2. 1288) col 6 shrunken ply-2 by vblk 1
一處是第 9 行的 row-ply1,另一處是緊隨其后,第 6 列的 col-ply2。依次來分析一下。
先用 runtil 1286 命令進入當時的上下文:
1286) col 9 complete shrunken by group 860 000 003 023 600 000 070 090 206 050 007 000 010 045 700 080 100 030 541 000 368 038 504 910 090 000 400 Steps:1287 Candidates: [1,3]: 4 5 9 [1,4]: 2 4 7 [1,5]: 1 2 5 7 [1,6]: 1 2 [1,7]: 1 5 [1,8]: 4 5 7 9 [2,1]: 1 4 9 [2,5]: 1 5 7 8 [2,6]: 1 8 [2,7]: 1 5 8 [2,8]: 4 5 7 8 9 [2,9]: 1 4 5 7 9 [3,1]: 1 4 [3,3]: 4 5 [3,4]: 3 4 8 [3,6]: 1 3 8 [3,8]: 4 5 8 [4,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 [4,8]: 2 4 8 9 [4,9]: 1 2 4 9 [5,1]: 2 3 6 9 [5,3]: 2 6 9 [5,4]: 2 3 8 9 [5,8]: 2 8 9 [5,9]: 2 9 [6,1]: 2 4 6 7 9 [6,3]: 2 4 6 7 9 [6,5]: 2 6 [6,6]: 2 6 9 [6,7]: 5 6 [6,9]: 2 4 5 9 [7,4]: 2 7 9 [7,5]: 2 7 [7,6]: 2 9 [8,1]: 2 6 7 [8,5]: 2 6 7 [8,9]: 2 7 [9,1]: 2 6 7 [9,3]: 2 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 5 7 [9,9]: 2 5 7 The foremost cell with 2 candidate(s) at [1,6] At guess level 5 [1,2] 2 Run time: 270 milliseconds; steps: 1287, solution sum: 1.
1286 步時,第 9 列滿足完全收縮的 grp 算法,因而有至少一個空位的候選值收縮為單值,進行填值后調整關聯空位的候選值,步數加一,當前輸出的實際上是走完 1287 步后的上下文。現在把關注點放到第 9 行,當時 quiz 已填值情況為:
860 000 003 023 600 000 070 090 206 050 007 000 010 045 700 080 100 030 541 000 368 038 504 910 090 000 400
從下一步的輸出信息看,第 9 行施用了 row-ply1,且 [9,4]、[9,5]、[9,6] 三個空位都得到了收縮,可推知是左下宮和右下宮的已填值交集作用於第 9 行的第二節所致,即:
{5, 4, 1, 3, 8} ∩ {3, 6, 8, 9, 1} = {1, 3, 8}
所以,[9,4]、[9,5]、[9,6] 三個空位必然要填入 {1, 3, 8} 里的三個數。而第 9 行當時各個空位的候選值分布情況為:
[9,1]: 2 6 7 [9,3]: 2 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 5 7 [9,9]: 2 5 7
這就很好地對應上了下一步的輸出信息:
[9,4]: 2 3 7 8 shrunken to 3 8 worked by row-ply1. [9,5]: 1 2 3 6 7 8 shrunken to 1 3 8 worked by row-ply1. [9,6]: 1 2 3 6 8 shrunken to 1 3 8 worked by row-ply1.
單純考慮第 9 行當時各個空位的候選值分布情況,這次的不完全收縮也能這樣推導出來:
[9,1]、[9,3]、[9,8]、[9,9] 這四個空位的候選值集合的並集為 {2, 5, 6, 7},第 9 行的七個空位的待填值集合為 {2, 5, 6, 7, 1, 3, 8},因而另三個空位的待填值必然為 {1, 3, 8}。
可以依此再實現一個補充的不完全收縮 grp 算法(比如叫做 thirdGreenWorld~),這也進一步推進了 用C++實現的數獨解題程序 SudokuSolver 2.1 及實例分析 里的那個推測:grp 算法增強后有可能就不需要使用第二類收縮算法。這一點要明確出來,需要數學上的嚴格推導。
接着來看第 6 列的 col-ply2。即:
[6,6]: 2 6 9 shrunken to 6 9 worked by col-ply2. [7,6]: 2 9 shrunken to 9 worked by col-ply2.
當時 quiz 已填值情況為:
860 000 003 023 600 000 070 090 206
050 007 000 010 045 700 080 100 030
541 000 368 038 504 910 090 000 400
第 6 列縱向跨越第 2 宮、第 5 宮、第 8 宮,[6,6] 和 [7,6] 分別屬於其中的后兩宮。對第 6 列施行 col-ply2,可以推定是把第 2 宮里的 6 和 9 考慮往第 6 列的第二節和第三節的空位上填。
從上面的全體空位候選值分布信息里提取出第 6 列各空位的候選值分布情況,為:
[1,6]: 1 2
[2,6]: 1 8
[3,6]: 1 3 8
[6,6]: 2 6 9
[7,6]: 2 9
[9,6]: 1 2 3 6 8
其中,[9,6] 空位因為剛才的第 9 行的 row-ply1,其候選值集合中只剩下 1、3、8。第 6 列的第二節和第三節共有三個空位,即 [6,6]、[7,6] 和 [9,6],往這三個空位里填 6 和 9,而 [9,6] 的候選值里沒有 6 和 9,因而 6 和 9 必然要填入 [6,6] 和 [7,6],即有:
[6,6]: 2 6 9 shrunken to 6 9 worked by col-ply2. [7,6]: 2 9 shrunken to 9 worked by col-ply2.
這里第 6 列的 col-ply2 得以發生是借助了第 9 行的 row-ply1。第 6 列的 col-ply2 實施的收縮效果在現有的 grp 算法一樣可以應對,即:
第 6 列的各空位中,只有 [6,6] 有候選值6,因而直接推定 [6,6] = 6,進而用同樣的方法推定 [7,6] = 9,以及 [1,6] = 2,等等。