数独终局游戏(终局生成、题目生成和解题)
项目简介
Github链接:https://github.com/fagen/sudoku
1、项目要求
- 能够生成1-1e6个不一样的数独终局并输出到文件 (命令:sudoku.exe -c abc )
- 能够从文件里读取数独问题并将求解结果输出到文件 (命令:sudoku.exe -s path)
- 附加要求:为数独游戏生成一个GUI界面,能够生成任意数量的数独题目并依次显示,棋盘挖空数目30<=n<=60。用户可以在界面上点击或者输入完成数独题目,用户完成数独题目后可以得到反馈,知道自己的题目是否做对
2、PSP表格
3、解题思路及过程
看到题目发现是数独的时候觉得不会太难,只要解决了生成合格的数独终局这个问题,解数独和生成数独问题都是基于相同的规则下产生的。关于游戏界面的添加则是另外一个问题,只是可能会繁琐且耗时一点。
3.1 数独终局生成解决过程
1、暴力法
在没有查询资料的前提下,只了解到一个完整的数独终局规则:在一个99的数字表格中,每一行,每一列,每一个33的9个数字中,都必须包括1-9且不能重复。可以通过随机数逐步产生数字一边检查一遍生成终局。但这个方法由于生成一个终局的过程中每产生一个随机数的同时,都必须检查一遍目前终局的合法性。即使通过一定方式尽量避免检查这个问题,检查次数还是不少。在需要生成1e6个终局情况下还需要进行反复查重,虽然生成终局的质量很高,但是非常浪费时间。
2、全排列+行变换
通过网上查询关于数独终局的生成方法,发现一种较为简便的方式。
对于一个数独终局,在第一行固定了的情况下,从第二行开始,每行在第一行左移3、6、1、4、7、2、5、8的情况下,就可以生成合格的一个数独终局。由于数独终局的第一个数字已经固定为5,所以通过对第一行进行全排列的方式就可以产生8!= 40320种终局。在每一个终局的基础上,通过任意交换2 3行,4 5 6行,7,8,9行就可以产生新的终局。由此总共可以产生40320*2!*3!*3!= 2903040种不同的终局。已经远远超过了题目要求。
代码思路
为了生成足够多的数独终局并且保证不能重复,第一步就是全排列。全排列本可以通过递归生成,但突然想起C++有一个函数next_permutation() 可以完成这件事,因此解决了全排列的问题。
对于每一个全排列,都只确定了第一行,但因为后面8行都是通过第一行平移产生的,因此,一个全排列就确定了一个数独终局。在一个终局的基础上,其他终局的产生只是调换第一个终局行的顺序的产物。为了省去行交换的时间,新的终局的生成只需调换行的输出顺序就可以了。
由于整个过程的代码量不多,并且终局的生成和输出是结合在一起的,于是一个函数就可以解决数独终局的生成问题
同样也是为了效率,整个编码过程基本是使用C语言完成
关于输出以及改进过程
- 二维数组存储81个数字,终局生成之后,采用freopen() 结合putchar() 对字符进行一个一个输出,在每个数字之后输出一个’\0’或者’\n’确保输出格式符合题目要求。通过这样的方式,一个数独终局需要输出163次,生成1e6个数独终局的时间为28s
- 通过对性能分析图的观察,发现整个数独生成过程中,全排列消耗的时间不到1%,而整个函数也没有其他多余的部分,说明,95%以上的时间消耗都是在输出过程中。于是,在输出之前,通过字符连接运算将每一个数字字符和空格和回车提前连接成一个字符串,于是一行只需要输出一次,使用freopen()和puts(),一个数独终局只需要输出10次,虽然运算代价稍有增加,程序运行时间仍然大大减少,从28s减少到了8s。性能得到了很大的优化。
- 继续在输出上做文章,前面的字符串连接主要是空格,但实际上可以把空格提前放入运算的数组中。一开始一位会很麻烦,但只要对代码进行微调就可以了,把循环变量的“i++”改成“i+=2”,同时其余细节进行稍微调整。最终采用freopen()和puts()一次输出一行,不需要进行字符串连接,最终时间是5.5s左右。
- 通过不断尝试,发现使用fopen()函数和fputs()会使得输出的时间稍微有所降低,但是效果不是很明显。
- 终局生成之后,在输出之前将整个熟读终局的所有字符连接成一个长的字符串,于是一个数独终局只需要输出一次。运行时间减少到了3.5s,当电脑状态好的情况下,可以跑进3s
- 在整个改进过程中,发现一个现象,当数组的每一行的所有字符都有效即每一行的末尾都没有’\0’字符的情况下,当用fputs()进行输出时,一次就可以将整个数独终局全部输出。因此有一个设想:使用得当的情况下,不需要进行将163个字符连接成一个长的字符串操作就可以直接输出,预计时间可以节省0.5s左右。但由于代码结构的原因,必须进行较大改动才能实现,故没有尝试。
- 开一个全局数组用于输出,将所有的终局都存进去,在最后需要输出的时候直接一次输出,最终生成1e6终局的时间在2.5s
整个改进过程大概花了5天的时间,平均一天2小时。
关键代码
void sudoku_generate(int n)
{
//char str[30];
char line[9] = {
'5','1','2','3','4','6','7','8','9' };
char line1[19] = {
'5',' ','1', ' ' ,'2', ' ','3',' ','4', ' ','6',' ','7',' ','8',' ','9','\n','\0' };
int shift[9] = {
0,6,12,2,8,14,4,10,16 };
int pos1[6][3] = {
{
3,4,5 },{
3,5,4 },{
4,5,3 },{
4,3,5 },{
5,4,3 },{
5,3,4 } };
int pos2[6][3] = {
{
6,7,8 },{
6,8,7 },{
7,6,8 },{
7,8,6 },{
8,6,7 },{
8,7,6