SudokuSolver 1.0:用C++实现的数独解题程序 【二】

本篇是 SudokuSolver 1.0:用C++实现的数独解题程序 【一】 的续篇。

CQuizDealer::loadQuiz 接口实现

 1 CQuizDealer* CQuizDealer::sm_pInst = NULL;
 2 
 3 void CQuizDealer::loadQuiz(std::string& strAbsFile)
 4 {
 5     if (m_state != STA_UNLOADED) {
 6         printf("A quiz loaded before.\n");
 7         return;
 8     }
 9     if (strAbsFile.empty()) {
10         printf("No quiz file specified.\n");
11         return;
12     }
13     std::ifstream oFile("H:\\s.txt"); // strAbsFile.c_str()
14     if (oFile.fail()) {
15         printf("Fail to open quiz file %s with err %d:%s.\n", strAbsFile.c_str(), 
errno, strerror(errno));
16 return; 17 } 18 int lineCount = 0; 19 int row = -1; 20 char szBuf[1024]; 21 while (oFile.getline(szBuf, 1024)) { 22 lineCount++; 23 char* pPos = szBuf; 24 if (0 == *pPos || '#' == *pPos) 25 continue; 26 size_t len = strlen(szBuf); 27 if (len < 9) { 28 printf("Invalid line %d: %s.\n", lineCount, szBuf); 29 return; 30 } 31 ++row; 32 int col = 0; 33 for (int idx = 0; idx < len; ++idx) { 34 char ch = szBuf[idx]; 35 if (ch < '0' || ch > '9') 36 continue; 37 m_seqCell[row * 9 + col].val = (u8)(ch - '0'); 38 if (++col >= 9) 39 break; 40 } 41 if (row >= 8) 42 break; 43 } 44 printf("Quiz loaded.\n"); 45 initTakens(); 46 m_state = STA_LOADED; 47 }

这里只是把文本文件里的 quiz 加载到 m_seqCell 数组中,对于取值为 0 的 Cell 此时还不求其候选值。

实际运行时,发现一个很蹊跷的问题,输入交互命令 load-quiz H:\s.txt 居然报错:

Order please:
load-quiz H:\s.txt
Fail to open quiz file  H:\s.txt with err 22:Invalid argument.

Order please:

而在程序里直接采用如下语句形式

std::ifstream oFile("H:\\s.txt");

就能正常了。没弄清楚问题出在哪里。

loadQuiz 接口的实现代码末尾部分调用了 initTakens 接口。

CQuizDealer::initTakens 和 initTaken 接口实现

 1 void CQuizDealer::initTakens()
 2 {
 3     initTaken(m_rowTaken);
 4     initTaken(m_colTaken);
 5     initTaken(m_blockTaken);
 6 }
 7 
 8 void CQuizDealer::initTaken(std::vector<std::set<u8> >& vec)
 9 {
10     std::set<u8> emptySet;
11     for (u8 idx = 0; idx < 9; ++idx)
12         vec.push_back(emptySet);
13 }    

initTakens 接口的作用仅仅是预设 每一行(共 9 行)、每一列(共 9 列)、每一宫(共 9 宫)的已填数集合全为空集。

CQuizDealer::showQuiz 接口实现

 1 void CQuizDealer::showQuiz()
 2 {
 3     if (m_state == STA_UNLOADED) {
 4         printf("Quiz not loaded yet.\n");
 5         return;
 6     }
 7     for (u8 row = 0; row < 9; ++row) {
 8         if (row == 3 || row == 6)
 9             printf("\n");
10         for (u8 col = 0; col < 9; ++col) {
11             if (col == 3 || col == 6)
12                 printf(" ");
13             printf("%d", (int)m_seqCell[row * 9 + col].val);
14         }
15         printf("\n");
16     }
17     if (m_state == STA_INVALID) {
18         if (m_soluSum == 0)
19             printf("\nInvalid quiz [steps:%u]\n", m_steps);
20         else
21             printf("\nInvalid quiz [steps:%u] - no more solution (solution sum is %u)\n", m_steps, m_soluSum);
22         return;
23     }
24     if (m_state == STA_DONE) {
25         printf("\nDone [steps:%u, solution sum:%u].\n", m_steps, m_soluSum);
26         return;
27     }
28     if (m_state != STA_VALID)
29         return;
30     printf("\nCandidates:\n");
31     for (std::set<u8>::iterator it = m_setBlank.begin(); it != m_setBlank.end(); ++it) {
32         u8 pos = *it;
33         u8* pVals = m_seqCell[pos].candidates;
34         printCandidates(pos, pVals);
35     }
36     if (m_guessLevel != 0)
37         printf("\nAt guess level %d [%d,%d] %d\n", (int)m_guessLevel, (int)(m_guessPos / 9 + 1), (int)(m_guessPos % 9 + 1), (int)m_guessValPos);
38 }

其中用到的 printCandidates 函数为:

1 void printCandidates(u8 pos, u8* pVals)
2 {
3     u8 sum = pVals[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)pVals[idx + 1]);
7     printf("\n");
8 }

CQuizDealer::step 接口实现

 1 void CQuizDealer::step()
 2 {
 3     if (m_state == STA_UNLOADED) {
 4         printf("Quiz not loaded yet.\n");
 5         return;
 6     }
 7     else if (m_state == STA_LOADED)
 8         parse();
 9     else if (m_state == STA_VALID)
10         adjust();
11     else if (m_state == STA_DONE) {
12         if (m_stkSnap.empty()) {
13             printf("No more solution (solution sum is %u).\n", m_soluSum);
14             return;
15         }
16         m_state = STA_VALID;
17         nextGuess();
18         m_steps++;
19     }
20     showQuiz();
21 }

 m_state 表示 quiz 的状态。初始状态为 STA_UNLOADED,即 quiz 未加载;通过 load-quiz 命令完成 quiz 加载后,状态变为 STA_LOADED,这时接到 step 命令,step接口会调用 parse 接口对加载的 quiz 做合规分析,随后调用 showQuiz 接口输出分析结果。

quiz 通过分析界定为合规,则状态变为 STA_VALID,这时接到 step 命令,step接口会调用 adjust 接口对 quiz 做下一步的调整处理,随后调用 showQuiz 接口输出调整后的结果。

当 quiz 经若干次调整处理后所有单元格都填上了非 0 数字且依然合规,quiz 的状态则会被置为 STA_DONE,即找到了一个解。这时接到 step 命令,step接口会尝试去寻找下一个解(把状态置为 STA_VALID 并调用 nextGuess 接口)。

CQuizDealer::parse 接口实现

 1 void CQuizDealer::parse()
 2 {
 3     for (u8 row = 0; row < 9; ++row) /// fill rows taken
 4         for (u8 col = 0; col < 9; ++col) {
 5             if (!fillTaken(row, m_seqCell[row * 9 + col].val, m_rowTaken))
 6                 return;
 7         }
 8     for (u8 col = 0; col < 9; ++col) /// fill cols taken
 9         for (u8 row = 0; row < 9; ++row) {
10             if (!fillTaken(col, m_seqCell[row * 9 + col].val, m_colTaken))
11                 return;
12         }
13     for (u8 blk = 0; blk < 9; ++blk) /// fill blocks taken
14         for (u8 idx = 0; idx < 9; ++idx) {
15             u8 row = block2row(blk, idx);
16             u8 col = block2col(blk, idx);
17             if (!fillTaken(blk, m_seqCell[row * 9 + col].val, m_blockTaken))
18                 return;
19         }
20     for (u8 idx = 0; idx < 81; ++idx) { /// fill candidates
21         if (m_seqCell[idx].val != 0)
22             continue;
23         m_setBlank.insert(idx);
24         if (!calcCandidates(idx)) {
25             m_state = STA_INVALID;
26             return;
27         }
28     }
29     m_state = (m_setBlank.empty() ? STA_DONE : STA_VALID);
30     if (m_state == STA_DONE)
31         m_soluSum++;
32 }

parse 接口仅在 quiz 加载后的第一步求解时被调用。该接口的逻辑很简单,就是由输入的 quiz 填充 m_rowTaken、m_colTaken、m_blkTaken、m_setBlank 以及 m_seqCell 里的各待填格的候选值信息。

如果把一个现成的解作为 quiz 输入,在 parse 接口就会被界定为一个解。

block2row 和 block2col 函数实现

1 u8 block2row(u8 blk, u8 idx) {
2     return (blk / 3) * 3 + (idx / 3);
3 }
4 u8 block2col(u8 blk, u8 idx) {
5     return (blk % 3) * 3 + (idx % 3);
6 }

约定上左、上中、上右三宫的序标为 0、1、2,中左、中心、中右三宫的序标为 3、4、5,下左、下中、下右三宫的序标为 6、7、8。宫内 9 个单元格的序标按同样的规则约定为 0 到 8。通过 block2row 和 block2col 函数可以求出宫序标 blk 且宫内序标 idx 的单元格的行序标 row 和列序标 col。

CQuizDealer::fillTaken 接口实现

 1 bool CQuizDealer::fillTaken(u8 key, u8 val, std::vector<std::set<u8> >& vec)
 2 {
 3     if (val == 0)
 4         return true;
 5     if (vec[key].find(val) != vec[key].end()) {
 6         m_state = STA_INVALID;
 7         return false;
 8     }
 9     vec[key].insert(val);
10     return true;
11 }

如果输入的 quiz 里某一行(列、宫)里非 0 数字存在重复,则会在 parse 接口的某次 fillTaken 调用里发现,并把 quiz 状态置为 STA_INVALID,即不合规。

CQuizDealer::calcCandidates 接口实现

 1 bool CQuizDealer::calcCandidates(u8 idx)
 2 {
 3     u8 row = idx / 9;
 4     u8 col = idx % 9;
 5     u8 blk = (row / 3) * 3 + (col / 3);
 6     for (u8 val = 1; val < 10; ++val) {
 7         if (m_rowTaken[row].find(val) != m_rowTaken[row].end())
 8             continue;
 9         if (m_colTaken[col].find(val) != m_colTaken[col].end())
10             continue;
11         if (m_blockTaken[blk].find(val) != m_blockTaken[blk].end())
12             continue;
13         u8 sum = m_seqCell[idx].candidates[0] + 1;
14         m_seqCell[idx].candidates[0] = sum;
15         m_seqCell[idx].candidates[sum] = val;
16     }
17     if (m_seqCell[idx].candidates[0] == 0) {
18         printf("Candidates for [%d,%d] went wrong\n", (int)(idx / 9 + 1), (int)(idx % 9 + 1));
19         return false;
20     }
21     return true;    
22 }

calcCandidates 接口对指定的单元格(由输入参数 idx,单元格下标指定),依据该单元格所在行、列、宫的已填数集合,计算其候选值集合,填到 m_seqCell[idx].candidates 里。如果计算出该单元格的候选值集合为空,则输出错误提示信息,并返回 false。

CQuizDealer::adjust 接口实现

 1 void CQuizDealer::adjust()
 2 {
 3     if (m_state != STA_VALID)
 4         return;
 5     m_steps++;
 6     bool changed = false;
 7     bool bWrong = false;
 8     u8 guessIdx = 0;
 9     u8 lowestSum = 10;
10     for (std::set<u8>::iterator it = m_setBlank.begin(); it != m_setBlank.end();) {
11         u8 idx = *it;
12         u8 sum = m_seqCell[idx].candidates[0];
13         if (sum != 1) {
14             if (sum < lowestSum) {
15                 lowestSum = sum;
16                 guessIdx = idx;
17             }
18             ++it;
19             continue;
20         }
21         m_seqCell[idx].val = m_seqCell[idx].candidates[1];
22         m_seqCell[idx].candidates[0] = 0;
23         if (!adjustTakens(idx, m_seqCell[idx].val)) {
24             bWrong = true;
25             break;
26         }
27         m_setBlank.erase(it++);
28         changed = true;
29     }
30     if (bWrong) {
31         nextGuess();
32         return;
33     }
34     if (m_setBlank.empty()) {
35         m_state = STA_DONE;
36         m_soluSum++;
37         return;
38     }
39     if (!changed) {
40         guess(guessIdx);
41         return;
42     }
43     if (!reCalcCandidates())
44         nextGuess();
45 }

 adjust 接口的主要逻辑是遍历考察 m_setBlank 中记录的待填格。如果待填格的候选值唯一,则直接采用该候选值填空,并调用 adjustTakens 接口去调整行、列、宫的已填数集合:若出现冲突,则置出错标志(即 bWrong = true),退出循环体后会调用 nextGuess 尝试对当时的猜测做出调整;若不出现冲突,则把新填值的单元格从 m_setBlank 中剔除,并继续考察其余的待填格。

完成这一趟遍历考察后,若 m_setBlank 为空,即不再有待填格,此时找到了一个解,quiz 状态会被置为 STA_DONE,解的总数加 1;若 m_setBlank 不为空,则调用 reCalcCandidates 重新计算更新各个待填格的候选值信息,出现冲突时会调用 nextGuess 尝试对当时的猜测做出调整。

如果 m_setBlank 中记录的所有待填格的候选值均不唯一,这一趟遍历考察会找出候选值数量最小(lowestSum)且下标最小(guessIdx)的待填格,然后调用 guess 接口去对 guessIdx 对应的待填格做猜测填数处理。

CQuizDealer::reCalcCandidates 接口实现

1 bool CQuizDealer::reCalcCandidates()
2 {
3     for (u8 idx = 0; idx < 81; ++idx)
4         m_seqCell[idx].candidates[0] = 0;
5     for (std::set<u8>::iterator it = m_setBlank.begin(); it != m_setBlank.end(); ++it)
6         if (!calcCandidates(*it))
7             return false;
8     return true;
9 }

CQuizDealer::adjustTakens 接口实现

 1 bool CQuizDealer::adjustTakens(u8 idx, u8 val)
 2 {
 3     u8 row = idx / 9;
 4     u8 col = idx % 9;
 5     u8 blk = (row / 3) * 3 + (col / 3);
 6     if (m_rowTaken[row].find(val) != m_rowTaken[row].end()) {
 7         printf("%d is in row %d before!\n", (int)val, (int)(row + 1));
 8         return false;
 9     }
10     m_rowTaken[row].insert(val);
11     if (m_colTaken[col].find(val) != m_colTaken[col].end()) {
12         printf("%d is in col %d before!\n", (int)val, (int)(col + 1));
13         return false;
14     }
15     m_colTaken[col].insert(val);
16     if (m_blockTaken[blk].find(val) != m_blockTaken[blk].end()) {
17         printf("%d is in block %d before!\n", (int)val, (int)(blk + 1));
18         return false;
19     }
20     m_blockTaken[blk].insert(val);
21     return true;
22 }

adjustTakens 接口实现里有合规检查,确保一行、一列、一宫里都不能填入重复的数。

CQuizDealer::guess 接口实现

 1 void CQuizDealer::guess(u8 guessIdx)
 2 {
 3     ++m_guessLevel;
 4     m_guessPos = guessIdx;
 5     m_guessValPos = 1;
 6     pushIn(guessIdx, 1);
 7     printf("Guess [%d,%d] level %d at 1 out of %d\n\n", (int)(guessIdx / 9 + 1), (int)(guessIdx % 9 + 1), (int)m_guessLevel, (int)m_seqCell[guessIdx].candidates[0]);
 8     if (!shift(guessIdx, 1))
 9         nextGuess();
10 }

guess 接口总是进入更深一级的猜测时,对 guessIdx 所指的待填格使用第一个候选值做出猜测。shift 实施猜测填值并做合规检查,检查不通过则调用 nextGuess 做进一步调整处理。

CQuizDealer::pushIn 接口实现

 1 void CQuizDealer::pushIn(u8 guessIdx, u8 valIdx)
 2 {
 3     Snapshot* pSnap = new Snapshot;
 4     pSnap->guessLevel = m_guessLevel;
 5     pSnap->guessPos = guessIdx;
 6     pSnap->guessValPos = valIdx;
 7     for (u8 idx = 0; idx < 81; ++idx)
 8         pSnap->seqCell[idx] = m_seqCell[idx];
 9     pSnap->rowTaken = m_rowTaken;
10     pSnap->colTaken = m_colTaken;
11     pSnap->blkTaken = m_blockTaken;
12     pSnap->setBlank = m_setBlank;
13     pSnap->state = m_state;
14     m_stkSnap.push(pSnap);
15 }

猜测填值有多种可能,因而需要把当时的上下文生成一个快照,压入堆栈。以便后面调整上下文遍历其它的可能。

CQuizDealer::shift 接口实现

 1 bool CQuizDealer::shift(u8 idx, u8 valIdx)
 2 {
 3     m_seqCell[idx].val = m_seqCell[idx].candidates[valIdx];
 4     m_seqCell[idx].candidates[0] = 0;
 5     if (!adjustTakens(idx, m_seqCell[idx].val))
 6         return false;
 7     std::set<u8>::iterator it = m_setBlank.find(idx);
 8     m_setBlank.erase(it);
 9     return reCalcCandidates();
10 }

shift 接口实施猜测填值,并在填值后做合规检查。

CQuizDealer::nextGuess 接口实现

 1 void CQuizDealer::nextGuess()
 2 {
 3     if (m_stkSnap.empty()) {
 4         if (m_soluSum == 0)
 5             printf("Quiz is invalid.\n");
 6         else
 7             printf("No more solution (solution sum is %u).\n", m_soluSum);
 8         m_state = STA_INVALID;
 9         return;
10     }
11     Snapshot* pTop = m_stkSnap.top();
12     u8 idx = pTop->guessPos;
13     u8 sum = pTop->seqCell[idx].candidates[0];
14     if (pTop->guessValPos != sum) {
15         pTop->guessValPos++;
16         printf("Forward guess [%d,%d] level %d at %d out of %d\n\n", (int)(idx / 9 + 1), (int)(idx % 9 + 1), (int)pTop->guessLevel, (int)pTop->guessValPos, (int)sum);
17         useSnapshot(pTop);
18         if (shift(idx, pTop->guessValPos))
19             return;
20         else {
21             nextGuess();
22             return;
23         }
24     }
25     m_stkSnap.pop();
26     delete pTop;
27     backGuess();
28 }

当求解走到某一级猜测填值,但猜测填值后不合规,这时就会调用 nextGuess 接口做进一步的调整。

nextGuess 首先会看快照堆栈是否为空,若为空,则说明求解过程已经走完了,再不会有新的解。

若快照堆栈不空,则检查栈顶的快照,看当前最深的那次猜测的单元格,猜测值是不是使用了最后一个候选值,如果不是,则尝试做平级猜测调整(调用 useSnapshot 和 shift);否则,弹出并丢弃栈顶快照,随后调用 backGuess 做降级猜测调整处理。

CQuizDealer::useSnapshot 接口实现

 1 void CQuizDealer::useSnapshot(Snapshot* pSnap)
 2 {
 3     m_guessLevel = pSnap->guessLevel;
 4     m_guessPos = pSnap->guessPos;
 5     m_guessValPos = pSnap->guessValPos;
 6     for (u8 idx = 0; idx < 81; ++idx)
 7         m_seqCell[idx] = pSnap->seqCell[idx];
 8     m_rowTaken = pSnap->rowTaken;
 9     m_colTaken = pSnap->colTaken;
10     m_blockTaken = pSnap->blkTaken;
11     m_setBlank = pSnap->setBlank;
12     m_state = pSnap->state;
13 }

CQuizDealer::backGuess 接口实现

 1 void CQuizDealer::backGuess()
 2 {
 3     if (m_stkSnap.empty()) {
 4         if (m_soluSum == 0)
 5             printf("Quiz is invalid.\n");
 6         else
 7             printf("No more solution (solution sum is %u).\n", m_soluSum);
 8         m_state = STA_INVALID;
 9         return;
10     }
11     Snapshot* pTop = m_stkSnap.top();
12     u8 idx = pTop->guessPos;
13     u8 sum = pTop->seqCell[idx].candidates[0];
14     if (pTop->guessValPos != sum) {
15         pTop->guessValPos++;
16         printf("Upward guess [%d,%d] level %d at %d out of %d\n\n", (int)(idx / 9 + 1), (int)(idx % 9 + 1), (int)pTop->guessLevel, (int)pTop->guessValPos, (int)sum);
17         useSnapshot(pTop);
18         if (shift(idx, pTop->guessValPos))
19             return;
20         else {
21             nextGuess();
22             return;
23         }
24     }
25     m_stkSnap.pop();
26     delete pTop;
27     backGuess();
28 }

backGuess 接口的实现和 nextGuess 类似,只是回退到上一级尝试做平级猜测调整或降级猜测调整。这两个接口都存在递归调用的情形(既有自身递归调用,又有交叉递归调用)。

CQuizDealer::run 接口实现

 1 void CQuizDealer::run()
 2 {
 3     if (m_state == STA_UNLOADED) {
 4         printf("Quiz not loaded yet.\n");
 5         return;
 6     }
 7     clock_t begin = clock();
 8     if (m_state == STA_DONE) {
 9         if (m_stkSnap.empty()) {
10             printf("No more solution.\n");
11             return;
12         }
13         m_state = STA_VALID;
14         nextGuess();
15     }
16     if (m_state == STA_LOADED)
17         parse();
18     while (m_state == STA_VALID)
19         adjust();
20     showQuiz();
21     std::cout << "Run time: " << clock() - begin << " milliseconds; steps: " << m_steps << ", solution sum: " << m_soluSum << ".\n\n";
22 }

run 接口和 step 接口实现是类似的,只是调用 adjust 的判断条件由 if (m_state == STA_VALID) 换成了 while (m_state == STA_VALID),这样就不再一步一停,而是一直到求出一个解或者不再有解时才停下来。

试验号称世界最难数独题

从网上以“世界最难数独”为关键字查找,一般都会搜出来如下这个题:

 以下是对这个题的求解过程:

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
Guess [8,7] level 1 at 1 out of 2
Guess [7,7] level 2 at 1 out of 2
Guess [9,8] level 3 at 1 out of 2
...
6 is in col 2 before!
Upward guess [2,7] level 14 at 2 out of 2
6 is in row 6 before!
Upward guess [2,5] level 13 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:4879, solution sum:1].
Run time: 2467 milliseconds; steps: 4879, solution sum: 1.

Order please:
run

...
3 is in col 2 before!
Upward guess [2,8] level 10 at 2 out of 2

9 is in col 6 before!
Upward guess [1,3] level 8 at 2 out of 2

Candidates for [4,8] went wrong
No more solution (solution sum is 1).
829 000 300
513 600 800
476 090 251

054 007 102
002 045 789
087 100 630

001 000 568
008 500 910
090 000 400

Invalid quiz [steps:9683] - no more solution (solution sum is 1)
Run time: 2220 milliseconds; steps: 9683, solution sum: 1.

Order please:
bye

H:\Read\num\Release>

从求解过程看,该题有唯一解。求出这个解耗时约 2467 毫秒,用了 4879 步(steps);第二次 run 耗时约 2220 毫秒。两次 run 共计 9683 步。

全空数独题

81 个单元格全部为空时是一个特殊的数独题,它的解是最多的,事实上任意一个数独题的解都是它的解。以下是用程序求其前两个解的过程示意:

H:\Read\num\Release>sudoku.exe

Order please:
load-quiz H:\s.txt
Quiz loaded.

Order please:
show
000 000 000
000 000 000
000 000 000

000 000 000
000 000 000
000 000 000

000 000 000
000 000 000
000 000 000

Order please:
run
...
Guess [7,5] level 45 at 1 out of 2

Guess [8,1] level 46 at 1 out of 2

Guess [8,3] level 47 at 1 out of 2

123 456 789
456 789 123
789 123 456

231 674 895
875 912 364
694 538 217

317 265 948
542 897 631
968 341 572

Done [steps:69, solution sum:1].
Run time: 65 milliseconds; steps: 69, solution sum: 1.

Order please:
run
Forward guess [8,3] level 47 at 2 out of 2

123 456 789
456 789 123
789 123 456

231 674 895
875 912 364
694 538 217

317 265 948
548 391 672
962 847 531

Done [steps:72, solution sum:2].
Run time: 71 milliseconds; steps: 72, solution sum: 2.

Order please:

 

posted on 2021-09-21 18:23  readalps  阅读(252)  评论(0编辑  收藏  举报

导航