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 类内部实现放到下一篇。
SudokuSolver 1.0:用C++实现的数独解题程序 【一】的更多相关文章
- SudokuSolver 1.0:用C++实现的数独解题程序 【二】
本篇是 SudokuSolver 1.0:用C++实现的数独解题程序 [一] 的续篇. CQuizDealer::loadQuiz 接口实现 1 CQuizDealer* CQuizDealer::s ...
- SudokuSolver 2.0:用C++实现的数独解题程序 【一】
SudokuSolver 2.0 实现效果 H:\Read\num\Release>sudoku.exe Order please: Sudoku Solver 2.0 2021/10/2 by ...
- 用C++实现的数独解题程序 SudokuSolver 2.3 及实例分析
SudokuSolver 2.3 程序实现 用C++实现的数独解题程序 SudokuSolver 2.2 及实例分析 里新发现了一处可以改进 grp 算法的地方,本次版本实现了对应的改进 grp 算法 ...
- 用C++实现的数独解题程序 SudokuSolver 2.2 及实例分析
SudokuSolver 2.2 程序实现 根据 用C++实现的数独解题程序 SudokuSolver 2.1 及实例分析 里分析,对 2.1 版做了一些改进和尝试. CQuizDealer 类声明部 ...
- 用C++实现的数独解题程序 SudokuSolver 2.1 及实例分析
SudokuSolver 2.1 程序实现 在 2.0 版的基础上,2.1 版在输出信息上做了一些改进,并增加了 runtil <steps> 命令,方便做实例分析. CQuizDeale ...
- 用C++实现的数独解题程序 SudokuSolver 2.4 及实例分析
SudokuSolver 2.4 程序实现 本次版本实现了 用C++实现的数独解题程序 SudokuSolver 2.3 及实例分析 里发现的第三个不完全收缩 grp 算法 thirdGreenWor ...
- 用C++实现的数独解题程序 SudokuSolver 2.6 的新功能及相关分析
SudokuSolver 2.6 的新功能及相关分析 SudokuSolver 2.6 的命令清单如下: H:\Read\num\Release>sudoku.exe Order please: ...
- 用C++实现的数独解题程序 SudokuSolver 2.7 及实例分析
引言:一个 bug 的发现 在 MobaXterm 上看到有内置的 Sudoku 游戏,于是拿 SudokuSolver 求解,随机出题,一上来是个 medium 级别的题: 073 000 060 ...
- 数独GUI程序项目实现
数独GUI程序项目实现 导语:最近玩上了数独这个游戏,但是找到的几个PC端数独游戏都有点老了...我就想自己做一个数独小游戏,也是一个不错的选择. 前期我在网上简单地查看了一些数独游戏的界面,代码.好 ...
随机推荐
- Hutool中那些常用的工具类和方法
Hutool中那些常用的工具类和方法 Hutool是一个Java工具包,它帮助我们简化每一行代码,避免重复造轮子.如果你有需要用到某些工具方法的时候,不妨在Hutool里面找找,可能就有.本文将对Hu ...
- python json demo
值得注意的一点是,list类型的数据可以用[2,3]的方式定义,如"b" import json jsonData = '{"a":1,"b" ...
- tensorflow summary demo with linear-model
tf.summary + tensorboard 用来把graph图中的相关信息,如结构图.学习率.准确率.Loss等数据,写入到本地硬盘,并通过浏览器可视化之. 整理的代码如下: import te ...
- golang map实现原理
这篇文章主要讲 map 的赋值.删除.查询.扩容的具体执行过程,仍然是从底层的角度展开.结合源码,看完本文一定会彻底明白 map 底层原理. 我要说明的是,这里对 map 的基本用法涉及比较少,我相信 ...
- IOS 集成 Bilibili IJKPlayer播放器,播放rtmp视频流
因为公司项目需要,我一个连iPhone都没用过的人竟然跑去开发iOS APP.近一段时间一直忙于赶项目,到今天差不多了,所以记录一下当时遇到的各种坑,先从ios 集成 ijkplayer播放器说起! ...
- Spring5(六)——AspectJ(xml)
一.AspectJ 1.介绍 AspectJ是一个面向切面的框架,它扩展了Java语言.AspectJ定义了AOP语法,也可以说 AspectJ 是一个基于 Java 语言的 AOP 框架.通常我们在 ...
- go中导入包的几种方式
标准方式: import ( "fmt" )使用:fmt.Println() 点(.)方式 import ( . "fmt" ) 使用时候,可以胜率fmt前缀P ...
- 日期SQL 脚本
一个月第一天的 SELECT DATEADD(mm, DATEDIFF(mm,0,getdate()), 0)本周的星期一 SELECT DATEADD(wk, DATEDIFF(wk,0,g ...
- tar解压缩问题
gzip: stdin: unexpected end of filetar: 归档文件中异常的 EOFtar: 归档文件中异常的 EOFtar: Error is not recoverable: ...
- CodeForce-812C Sagheer and Nubian Market(二分)
Sagheer and Nubian Market CodeForces - 812C 题意:n个货物,每个货物基础价格是ai. 当你一共购买k个货物时,每个货物的价格为a[i]+k*i. 每个货物只 ...