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

SudokuSolver 1.0 用法与实现效果

SudokuSolver 是一个提供命令交互的命令行程序,提供的命令清单有:

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

Order please:

Sudoku Solver 1.0 2021/9/20 by readalps

Order List:
load-quiz <file>: load quiz from file
show: show quiz info
step: step forward
run: run till the end or a new solution met
bye: quit

Order please:

以 一道心形数独题的求解 里的那道题为例,先在一个文本文件(如:H:\s.txt)里记录这道题,约定用 0 表示待填数字,内容如下:

000 000 000
023 000 780
100 406 009

400 050 001
900 000 006
060 000 090

005 000 800
000 301 000
000 090 000

load-quiz 命令用于从文本文件中把数独题(称为一个 quiz)作为输入加载到程序内存;

show 命令用于显示 quiz 相关信息;

step 和 run 命令用于解题,step 只往前走一步;run 则一直走到求出一个解或走到末尾无解时才停步;

bye 命令结束交互退出程序。

以下是load-quiz、show 以及第一次 step 命令的运行效果:

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

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

Order please:
show
000 000 000
023 000 780
100 406 009

400 050 001
900 000 006
060 000 090

005 000 800
000 301 000
000 090 000

Order please:
step
000 000 000
023 000 780
100 406 009

400 050 001
900 000 006
060 000 090

005 000 800
000 301 000
000 090 000

Candidates:
[1,1]: 5 6 7 8
[1,2]: 4 5 7 8 9
... [2,1]: 5 6 [2,4]: 1 5 9 [2,5]: 1 [2,6]: 5 9
... [9,8]: 1 2 3 4 5 6 7 [9,9]: 2 3 4 5 7 Order please:

对比 show 显示的九宫格内容,和 step 之后显示出来的九宫格内容,会发现两者内容完全一致,即这一次 step 还没有开始填数。随后的 candidates 内容显示了各个待填数的格子里所有可能的候选值,其中 [2,5]: 1 表示第 2 行第 5 列的位置只有一个候选值 1,说明下一步这个位置会填上 1。具体看一下:

Order please:
step
000 000 000
023 010 780
100 406 009

400 050 001
900 000 006
060 000 090

005 000 800
000 301 000
000 090 000

Candidates:
[1,1]: 5 6 7 8
... [1,9]: 2 3 4 5 [2,1]: 5 6 [2,4]: 5 9 [2,6]: 5 9 [2,9]: 4 5 [3,2]: 5 7 8
... [9,9]: 2 3 4 5 7 Order please:

可以看到 [2,5] 位置如期填上了 1,但是从随后的 candidates 信息里看,剩余待填格的候选项数都大于 1,其中位置最靠前的是 :

[2,1]: 5 6

 由 一道心形数独题的求解 里的方法,可以直接确定 [2,1] = 6 以及 [2,9] = 4,但这个程序并没有实现这个方法。因此,在下一步里会尝试让 [2,1] = 5。具体看一下:

Order please:
step
Guess [2,1] level 1 at 1 out of 2

000 000 000
523 010 780
100 406 009

400 050 001
900 000 006
060 000 090

005 000 800
000 301 000
000 090 000

Candidates:
[1,1]: 6 7 8
... [1,9]: 2 3 4 5 [2,4]: 9 [2,6]: 9 [2,9]: 4 [3,2]: 7 8 [3,3]: 7 8
... [9,9]: 2 3 4 5 7 At guess level 1 [2,1] 1 Order please:

[2,1] 位置上如期填上了 5。在 step 命令之后有如下一句信息:

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

这是说,[2,1] 位置上有两个候选值,这里先尝试第一个候选值(at 1 out of 2),当前处在的猜测级数为1(level 1)。

随后的 candidates 信息里有:

[2,4]: 9
[2,6]: 9

即 [2,4] 和 [2,6] 位置的唯一候选值都为 9,因为同处第 2 行,这是不合理的。因此,下一步要做出调整:

Order please:
step
9 is in row 2 before!
Forward guess [2,1] level 1 at 2 out of 2

000 000 000
623 010 780
100 406 009

400 050 001
900 000 006
060 000 090

005 000 800
000 301 000
000 090 000

Candidates:
[1,1]: 5 7 8
... [2,4]: 5 9 [2,6]: 5 9 [2,9]: 4 5
... [9,9]: 2 3 4 5 7 At guess level 1 [2,1] 2 Order please:

[2,1] 位置如期调整为第二个候选值 6。

Forward guess [2,1] level 1 at 2 out of 2

Forward guess 是指在同一个猜测级向下一个候选值调整,即两个候选值中的第二个(at 2 out of 2)。

在随后的 candidates 信息里,剩余待填格的候选项数都大于 1,说明又要做更进一级的猜测,即猜测级数为 2。其中位置最靠前的是 :

[2,4]: 5 9

下一步会猜测 [2,4] = 5:

Order please:
step
Guess [2,4] level 2 at 1 out of 2

000 000 000
623 510 780
100 406 009

400 050 001
900 000 006
060 000 090

005 000 800
000 301 000
000 090 000

Candidates:
[1,1]: 5 7 8
... [2,6]: 9 [2,9]: 4 [3,2]: 5 7 8
... [9,9]: 2 3 4 5 7 At guess level 2 [2,4] 1 Order please:

从如下信息可以看出猜测级别进到了 level 2:

Guess [2,4] level 2 at 1 out of 2

新的候选值信息里有唯一候选值的情形,下一步会进一步填值:

Order please:
step
000 000 000
623 519 784
100 406 009

400 050 001
900 000 006
060 000 090

005 000 800
000 301 000
000 090 000

Candidates:
[1,1]: 5 7 8
... [3,3]: 7 8
... [9,9]: 2 3 5 7 At guess level 2 [2,4] 1 Order please:

新的候选值信息里没有唯一候选值的情形,说明下一步里猜测会加深一级:

Order please:
step
Guess [3,3] level 3 at 1 out of 2

000 000 000
623 519 784
107 406 009

400 050 001
900 000 006
060 000 090

005 000 800
000 301 000
000 090 000

Candidates:
[1,1]: 5 8
... [3,2]: 5 8
... [9,9]: 2 3 5 7 At guess level 3 [3,3] 1 Order please:

后面不再一步步走了,直接试一次 run 命令:

Order please:
run
Guess [1,1] level 4 at 1 out of 2
Guess [1,2] level 5 at 1 out of 2
Guess [1,9] level 6 at 1 out of 2
Guess [1,4] level 7 at 1 out of 2
Guess [1,5] level 8 at 1 out of 2
Guess [1,7] level 9 at 1 out of 2
Guess [3,7] level 10 at 1 out of 2
Guess [4,2] level 11 at 1 out of 2
7 is in row 4 before!
Forward guess [4,2] level 11 at 2 out of 2
3 is in row 4 before!
Upward guess [3,7] level 10 at 2 out of 2
Guess [4,2] level 11 at 1 out of 2
7 is in row 4 before!
Forward guess [4,2] level 11 at 2 out of 2
...
2 is in row 4 before!
Upward guess [3,7] level 9 at 2 out of 2
8 is in row 4 before!
Upward guess [2,4] level 2 at 2 out of 2
Guess [3,3] level 3 at 1 out of 2
Guess [1,1] level 4 at 1 out of 2
Guess [1,2] level 5 at 1 out of 2
Guess [1,9] level 6 at 1 out of 2
Guess [1,4] level 7 at 1 out of 2
Guess [1,5] level 8 at 1 out of 2
Guess [1,7] level 9 at 1 out of 2
Guess [3,7] level 10 at 1 out of 2
Guess [4,2] level 11 at 1 out of 2
Guess [5,3] level 12 at 1 out of 2

549 738 162
623 915 784
187 426 359

438 659 271
951 872 436
762 143 598

395 264 817
276 381 945
814 597 623

Done [steps:1058, solution sum:1].
Run time: 763 milliseconds; steps: 1058, solution sum: 1.

Order please:

这次的 run 命令求得了一个解,为求得这个解,一共走了 1058 步,耗时大约为 763 毫秒。

从这次 run 的前期输出信息看,进到 guess level 11 时,因出现冲突,而做了一次平级(forward guess)调整:

Forward guess [4,2] level 11 at 2 out of 2

随后这次平级调整同样出现冲突,且无法再做平级调整,于是做了一次降级(upward guess)调整:

Upward guess [3,7] level 10 at 2 out of 2

从这次 run 的后期输出信息看,最后一次调整是从 level 9 直降到 level 2:

Upward guess [3,7] level 9 at 2 out of 2
8 is in row 4 before!
Upward guess [2,4] level 2 at 2 out of 2

即前面 level 2 时由于 [2,4]: 5 9 猜测的 [2,4] = 5 是错误的,直到此时才调整为 [2,4] = 9,随后又依次逐级进到 level 12 才求出第一个解。

再来一次 run 命令:

...
2 is in col 3 before! Forward guess [5,2] level 10 at 2 out of 2 Guess [5,3] level 11 at 1 out of 2 5 is in col 7 before! Forward guess [5,3] level 11 at 2 out of 2 Candidates for [8,7] went wrong No more solution (solution sum is 1). 794 832 615 623 915 784 158 476 329 487 659 231 932 000 506 561 000 490 005 000 800 009 301 000 006 090 000 Invalid quiz [steps:4496] - no more solution (solution sum is 1) Run time: 1618 milliseconds; steps: 4496, solution sum: 1. Order please:

上一次 run 已经找到一个解,新的 run 会从上次得到解的位置试图做平级调整、降级调整等手段,尝试找一个新的解。但本例中,第二次 run 走到了末尾都没有找到新解,说明本例只有一个解。

Candidates for [8,7] went wrong

这句输出说明,最后得到的九宫格里 [8,7] 位置不能填 1 到 9 的任何数字(从第 8 行看,不能为 1、3、9,从第 7 列看,不能为 2、3、4、5、6、7、8)。

No more solution (solution sum is 1).

这句说明,当时所处的各级 guess level 都已是该级的最后一个选项值,出现冲突后无法做平级调整或降级调整,即所有可能有解的情形都已经走完了。

这次 run 命令耗时约 1618 毫秒。而从第一个 step 算起,包括两次 run 里面的 step 数,合计为 4496 步(steps)。

SudokuSolver 1.0 实现程序与说明

一个代码文件 sudoku.cpp 总计约 540 行代码。

Cell 结构

 1 #include <stdio.h>
 2 #include <stdint.h>
 3 #include <string>
 4 #include <vector>
 5 #include <set>
 6 #include <stack>
 7 #include <iostream>
 8 #include <fstream>
 9 #include <ctime>
10 
11 typedef unsigned char u8;
12 typedef unsigned long ulong;
13 
14 struct Cell {
15     u8 val;
16     u8 candidates[10];
17     Cell() {memset(this, 0, sizeof(Cell));}
18 };

一个 Cell 对应九宫格中的一个数字格。分量 val 为该数字格的值,初始值为 0 表示该格的数字待填;分量 candidates 记录待填格的候选值数组,下标从 1 开始(下标为 0 的单元记录候选值个数)。

Snapshot 结构

 1 struct Snapshot
 2 {
 3     Cell seqCell[81];
 4     std::set<u8> setBlank;
 5     std::vector<std::set<u8> > rowTaken;
 6     std::vector<std::set<u8> > colTaken;
 7     std::vector<std::set<u8> > blkTaken;
 8     u8 state;
 9     u8 guessLevel;
10     u8 guessPos;
11     u8 guessValPos;
12 };

一个 Snapshot 记录某一次猜测的上下文,包括:

seqCell,每个数字格的值与候选值信息;

setBlank,所有待填格(值为 0)的下标集合;

rowTaken、colTaken、blkTaken,记录每一行、每一列、每一宫已填数字的集合;

state,当时 quiz 所处状态;

guessLevel、guessPos、guessValPos,猜测级别、猜测数字格对应的下标、猜测数字格的取值在候选数列中的下标。

CQuizDealer 类

 1 class CQuizDealer
 2 {
 3 public:
 4     void loadQuiz(std::string& strAbsFile);
 5     void showQuiz();
 6     void step();
 7     void run();
 8     static CQuizDealer* instance() {
 9         return (NULL == sm_pInst ? (sm_pInst = new CQuizDealer) : sm_pInst);
10     }
11 
12 private:
13     enum {STA_UNLOADED = 0, STA_LOADED, STA_INVALID, STA_VALID, STA_DONE};
14     CQuizDealer() : m_state(STA_UNLOADED), m_guessLevel(0), m_guessPos(0), 
m_guessValPos(0), m_soluSum(0), m_steps(0) {}; 15 void parse(); 16 void adjust(); 17 void initTakens(); 18 void initTaken(std::vector<std::set<u8> >& vec); 19 bool fillTaken(u8 key, u8 val, std::vector<std::set<u8> >& vec); 20 bool adjustTakens(u8 idx, u8 val); 21 bool calcCandidates(u8 idx); 22 bool reCalcCandidates(); 23 void guess(u8 idx); 24 void pushIn(u8 idx, u8 valIdx); 25 bool shift(u8 idx, u8 valIdx); 26 void nextGuess(); 27 void backGuess(); 28 void useSnapshot(Snapshot* pSnap); 29 30 Cell m_seqCell[81]; 31 std::set<u8> m_setBlank; 32 std::vector<std::set<u8> > m_rowTaken; 33 std::vector<std::set<u8> > m_colTaken; 34 std::vector<std::set<u8> > m_blockTaken; 35 u8 m_state; 36 u8 m_guessLevel; 37 u8 m_guessPos; 38 u8 m_guessValPos; 39 ulong m_soluSum; 40 ulong m_steps; 41 42 std::stack<Snapshot*> m_stkSnap; 43 static CQuizDealer* sm_pInst; 44 };

CQuizDealer 的成员变量大多和 Snapshot 的成员相对应,m_soluSum 记录总解数,m_steps 记录求解过程中所用的总步数,m_stkSnap 是个堆栈,记录各级猜测的上下文;sm_pInst 是 CQuizDealer 类的单例指针,使用 CQuizDealer::instance() 访问。

main 函数

 1 int main()
 2 {
 3     std::string strOrder;
 4     while (!isQuitSet()) {
 5         printf("\nOrder please:\n");
 6         getline(std::cin, strOrder);
 7         dealOrder(strOrder);
 8     }
 9     return 0;
10 }

其中用到的 isQuitSet() 为:

1 static bool s_bQuit = false;
2 bool isQuitSet() {return s_bQuit;}
3 void setQuit() {s_bQuit = true;}

dealOrder 和 showOrderList 函数

 1 void showOrderList()
 2 {
 3     printf("Sudoku Solver 1.0 2021/9/20 by readalps\n\n");
 4     printf("Order List:\n");
 5     printf("load-quiz <file>: load quiz from file\n");
 6     printf("show: show quiz info\n");
 7     printf("step: step forward\n");
 8     printf("run: run till the end or a new solution met\n");
 9     printf("bye: quit\n");
10 }
11 
12 void dealOrder(std::string& strOrder)
13 {
14     std::string strEx;
15     if ("bye" == strOrder)
16         setQuit();
17     else if (matchPrefixEx(strOrder, "load-quiz", strEx))
18         CQuizDealer::instance()->loadQuiz(strEx);
19     else if ("show" == strOrder)
20         CQuizDealer::instance()->showQuiz();
21     else if ("step" == strOrder)
22         CQuizDealer::instance()->step();
23     else if ("run" == strOrder)
24         CQuizDealer::instance()->run();
25     else
26         showOrderList();
27 }

其中用到的 matchPrefixEx 函数实现为:

 1 bool matchPrefixEx(const std::string& src, const char* prefix, std::string& strEx)
 2 {
 3     if (NULL == prefix || 0 == strlen(prefix))
 4         return false;
 5     size_t len = strlen(prefix);
 6     if (0 == strncmp(src.c_str(), prefix, len)) {
 7         strEx = src.substr(len);
 8         return true;
 9     }
10     return false;
11 }

CQuizDealer 类内部实现放到下一篇。

 

上一篇:虚妄中发展的量子计算机与量子霸权


下一篇:2021年全球12个最大的数据中心