个人项目
个人项目的的基本要求很简单,就是生成一个数独终局与解数独。
首先我的GitHub地址为:https://github.com/BIT1120161942/PersonalProject
项目预估用时与实际用时
|
PSP |
Personal Software Process |
预估耗时(分钟) |
实际耗时(分钟) |
|
Planning |
计划 |
||
|
Estimate |
估计任务用时 |
45 |
50 |
|
Development |
开发 |
||
|
Analysis |
需求分析(包括学习新技术) |
75 |
100 |
|
Design Spec |
生成设计文档 |
45 |
30 |
|
Design Review |
设计复审 |
20 |
30 |
|
Coding Standard |
代码规范 |
10 |
20 |
|
Design |
具体设计 |
20 |
45 |
|
Coding |
具体编码 |
1000 |
1200 |
|
Code Review |
代码复审 |
120 |
100 |
|
Test |
测试 |
180 |
240 |
|
Reporting |
报告 |
||
|
Test Report |
测试报告 |
60 |
45 |
|
Size Measurement |
计算工作量 |
20 |
20 |
|
Postmortem & Process Improvement Plan |
总结并题出改进计划 |
30 |
45 |
|
Total |
合计 |
1625 |
1925 |
解题思路
刚拿到题目的时候,其实有点懵,因为我没玩过数独,对数独的规则并不是完全清楚,所以我先去搜了数独的规则。
数独的棋盘是一个9×9的格图,每3×3又是一个9宫格。
数独的要求是每行、每列、每个9宫格中,1~9这9个数字必须出现且仅出现一次。
然后我想到实际上解数独与生成数独终局是同一个问题,区别在于生成数独终局需要填入的格子更多,与解数独只需要生成一次数独终局,而解数独需要生成多次。因为他们的问题性质基本一致,所以我打算使用相同的方式来解决这两个问题。
首先我的第一想法是按照回溯法的方式来做,每个格子依次试探,碰到完全不可能的回溯,当试探到最后一个格子时,就生成了一个数独终局。
但是之后又看到1e6的数量级,感觉生成数独终局从头开始回溯话费的时间会相当的久,所以又仔细的观察了数独的格局。
数独有九个宫,而根据数独的规则一部分宫之间互相没有限制,比如第1宫,第6宫与第8宫,他们没有任一数据在同一行与同一列上,所以我可以先将他们填上,之后再进行回溯,这样每个格子的可能性大大减小。例如对第一的要填的各自来说直接进行有9种可能,而先填完后根据填的情况有3到6种可能性。
设计实现过程
有了思路之后开始着手设计与编写。
首先对于生成数独终局部分,我要先填入三个宫。

我编写了一个函数使用随机数某一宫填满1-9,之后调用它将1,6,8三个宫填满。这样就得到了一个经过处理初始数独。在最开始的时候考虑过如果生成的终局的数量过多,多次调用这个函数导致重复会如何,但在经过仔细思考与实际测试最终打消了顾虑。首先虽然对于数独的第一个数字做出了要求,但是出去第一个还有剩下的26个格子,总共可能性为8!*9!*9!,大概为5e15中可能,就概率来说极小。之后经过测试发现,即使生成1e6个数独终局实际这个函数被调用的次数也大概只在4次左右。因为还存在有可能,所以之后又在生成一个数独终局后做了简单的行列交换,打消了这个顾虑。
在得到一个经过处理的数独起始状态后就可以开始数独终局的生成。另外这样一个经过处理数独的起始状态也可以将其视为一个数独谜题,生成数独终局也是解数独的过程。

其中用num参数来控制数独的生成数量,每生成一个就-1,当num数量为0时就表明生成完毕,不在进行。解数独部分将num数量控制为1,得到可行解就不在进行。
同时考虑到一个经过处理的起始数独可以生成许多个数独终局,所以将目标文件也作为参数调用,生成一个后就直接输出到文件中。
在填入每个数字时需要先考虑数字是否符合要求,将对应个所在行所在列所在宫均扫描一遍,去除那些已经出现过的数字。
当进行到第8行,第8列时表明已经进行到最后一个格子,进行下一步操作。
考虑到回溯时十分花费时间的,以及生成数独不重复,在输出到文件之前先对这个生成的数独终局进行变换
观察数独其实不难发现一些规律。首先对于一个数独终局,对它关于某条对称轴进行对称操作得到的也肯定是数独终局。同时对于1-3行,4-6行7-9行内部进行行交换也成立,个部分整体之间交换也成立。对列同理。所以由一个数独终局可以生成多个不同的数独终局。
为了方便,以及保证第一数字的固定,我只做了数独关于主对角线对称,以及4-6行,7-9行内部的变换。
linorder表示当前数独终局各行的逻辑顺序。因为直接交换数独行十分麻烦,所以采取只交换他们的逻辑排列,而不交换物理排列。

用type来控制数独是否是对称后的,type为1表示数独是关于主对角线对称后的情况。
用num来控制数量,每输出一个将num-1,直至其为0。
到这里生成数独终局部分就已经做完,同时也已经基本完成解数独部分,只需要在添加文件的读入并且在调用时将num置为1即可。
性能优化
在完成生成数独终局部分后,对于生成1000000个数独的运行结果将近500s,一开始我认为是回溯部分耗费的时间过多,所以在变换数独部分又增加了了列变换,发现对于结果基本没什么改变。
在运行了一遍性能分析后发现最后输出的部分所占用的时间最多,最开始我是用c++风格的输出流将数独数字单个进行输出,考虑到c风格的输出所需时间更短,将其改为fprintf进行输出,结果时间平均在150s左右。之后又考虑到直接输出字符应该更快,改用fputc,用单个字符输出,时间大概为20s。最后在同学的启发下,将数独终局压缩为一个字符串在进行整体输出,最终将时间控制在3s左右。

我生成数独终局的函数是GetFinalSudoku与ChangeFinalSudoku,他们WriteIntoFile函数存在调用关系,最终还是WriteIntoFile花费的时间最久,所以要优化还需优化输出函数。
小结
这次个人项目的作业虽然在做之前感觉十分麻烦,但正式开始做之后也还是顺畅的做了下来,同时学习到了很多东西,对于这种小项目的编写也稍微积累了点经验。

浙公网安备 33010602011771号