文章目录
一. github项目地址
https://github.com/laopangpyy/shuduku
二. PSP表格
psp2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 90 |
Estimate | 估计这个任务需要多少时间 | 2460 | 3215 |
Development | 开发 | 1350 | 1920 |
Anylasis | 需求分析(包括学习新技术) | 240 | 300 |
Design Spec | 生成设计文档 | 40 | 60 |
Design Rewiew | 设计复审(和同事审核设计文档) | 30 | 40 |
Coding Stantard | 代码规范(为目前的开发制定合适的规范) | 30 | 30 |
Design | 具体设计 | 180 | 240 |
Coding | 具体编码 | 1200 | 1800 |
Code Review | 代码复审 | 150 | 120 |
Test | 测试(自我测试、修改代码、提交修改) | 240 | 300 |
Reporting | 报告 | 120 | 100 |
Test Report | 测试报告 | 100 | 60 |
Size Measurement | 计算工作量 | 30 | 30 |
Postmortem Process Improvement Plan | 事后总结,并提出改进计划 | 40 | 45 |
合计 | 2460 | 3215 |
三. 解题思路
1. 生成数独终局
题中要求生成数量为1至1e6的数独终局,故首先确定生成大量终局的算法。通过浏览相关博客,我了解到了可先确定一个终局模板行,然后通过它的移位变换(分别移0,3,6,1,4,7,2,5,8)产生终局中的所有行。因有8!个模板行,故此方法能产生8!个终局(因本题中要求数独第一行第一列处数字固定为学号);分别将以上得到的终局的第4、5、6行和第7、8、9行进行行交换又可在原终局基础上得到3!*3!个新终局。此时总共可得到8!*3!*3!个终局,大于题目所要求的数量。
2. 求解数独
首先将待解数独从文件中读入,记录其中为0的数字的行和列。然后采用回溯法依次搜索每个数值为0的点可放的数字(1-9),若无可放数字即回溯到上一个搜索的点更换其值再重新搜索。直到所有数值为0的点都更新了数字1-9即求解结束。
四. 设计实现过程
1.总体设计
设置了ShuDu类,将生成终局作为其成员函数CreateShuDu(),将求解数独作为其成员函数SolveShudu(),通过在主函数里建立ShuDu类的对象来获取相应方法。另外两个成员函数InputShuDu()和Solve()在SolveShudu中被调用。
2. 生成终局(CreateShuDu())部分
- 无子函数,主要由形成终局的三层循环和嵌套在循环中的输出部分构成。
- 设置一个移位数组source,存放各行相对模板行的移位数{0,3,6,1,4,7,2,5,8}。例如source[1]=3即第二行右移3位,以此类推。
- 设置一个原始模板行数组row,第一个元素必为学号后两位和,其他元素可为剩余数字的任意排序
- 用三层循环实现终局的生成,第一层循环为第4,5,6行的交换(即source[3]、source[4]、source[5]的全排列),第二层循环为第7,8,9行的交换(即source[6]、source[7]、source[8]的全排列),第三层循环为8!个模板行的生成以及相应模板行和移位数条件下的对应终局生成(即row[1]至row[8]的全排列)。其中全排列均用next_permutation得到。
- 最后的输出部分采用将全部数独存在一个巨大的动态字符数组中一次性输出到文件的方法,大幅度减少了输入输出的时间开销。采用动态数组的原因是让生成数组的数目和时间开销能够成线性关系,否则直接开一个巨大的静态数组将影响小数目数独生成的效率。这个方法是在性能分析中发现的问题进而研究出的解决方法,完全拯救了整个生成终局方法。
3. 求解数独(SolveShudu())部分
- 设置了set_security三维数组判断放置的数字是否合法(即所在行,列,块无相同数字),其中第一维代表判断的是所在行(值为0)、所在列(值为1)还是所在块(值为2);第二维代表是行或列或块中的第几个;第三维代表对数字 i 的判断,数组元素的值代表是否在此行或列或块中已经有了数字 i ,若有则为1;若无则为0。(此方法可大幅提高判断数字合法性的效率,是在某大佬博客中发现的)
- 包括–SolveShuDu()主体以及其调用的InputShuDu()和Solve()成员函数。
- SolveShuDu() 首先读入文件中的待解数独数据字符串,经过InputShuDu转换为二维的数独字符数组;再调用Solve进行回溯求解的实现,最后将求解后的二维数组转换为规范格式的一维数组从而一次性输出整个数独。以上过程循环进行,直到读到文件末尾停止。
- 在Solve()中首先按行顺序更新当前空白点坐标,判断该点可填的合法数字(采用1-9的循环实现),若找到合法数字则递归调用Solve()继续处理下一个空白点;若无合法数字则return回溯到上一节点(需要还原进入下一级前改变的变量)继续判断其他数字。直到所有空白点搜索完毕(即当前点的行数=9),求解数独完成。
- InputShuDu()中将从文件中读入的待求解数独字符串转化为带有坐标信息的二维数组,供接下来的求解过程适用。
4. 单元测试的设计
针对SolveShuDu()、InputShuDu()、Solve()、CreateShuDu()、main()设计了10个测试用例。
-
SolveShuDu()单元测试:对输入文件的打开状态进行测试,进行了已存在文件和不存在文件打开状态对比,确认了不存在文件不能被打开(由于本机中存在同目录下的文件test.txt,故在其他机器上运行时没有text.txt会运行失败);同时在测试执行后,手动检查求解文件内容的正确性.
-
InputShuDu()单元测试:对 从一维数组转换到二维数组后的数独数据 及 判断放置是否合法的set_security数组 进行正确性检查,进行了两组输入参数的测试。
-
Solve()单元测试:输入一个待解数独map[9][9],(map已被定义为ShuDu类的属性)通过调用Solve()求解得到新的map[][],再判断map的第一行是否与标准答案吻合。其中分别使用Solve()的正确和错误参数分别测试,并与错误答案进行比对
-
CreateShuDu()单元测试:传入合法和不合法的生成数目分别进行测试,用State_Create判断是否识别出了不合法的输入参数;同时手动检查合法输入参数的结果,判断输出的文件内容是否符合要求。
五. 性能改进
1. 生成终局的改进
在生成终局中我起初是生成一行就输出到文件一次,通过性能分析工具的查看,发现输出到文件语句耗费的cpu时间甚至占了80%以上,于是想办法使这个语句各频率地执行。先是改成了每8个数独输出一次,后来突然意识到也可以彻底地用空间换时间,也就是将要输出的全部数独用超大字符数组一次性地输出到文件,尝试了1000000个数独存储在大小为2e8的数组中,又发现小数目数独生成的时间较长。为了使运行时间能够和生成数独数目有线性呈现,改为使用动态数组,即要生成多少数独就分配多大的数组,效果符合预期,达到1.6s生成1e6个终局的效果,且小数目生成时间也较快。
调用生成终局方法时性能分析图如下所示:
由下图可见优化之后输出到文件依旧是花费巨多时间
2. 求解数独的改进
在求解数独中,最初使用的判断放置数字合法性是通过挨个比较相应行、列、块是否有重复的数字,判断过程需要三个循环,且每判断一次就要执行这三个循环,通过性能分析工具的查看,果然这部分的开销占了78%左右。因此苦思冥想,在浏览他人博客时发现了一个博主非常机智的算法,即上文提到过的 set_security 三维数组,存储每个位置的所在行、列和块已放置数字情况,从而通过一个 if 语句和三个判断条件即可快速判断是否可放置某数字,大大提升了求解数独的效率,甚至达到了提升数十倍速度的效果。甚是感谢这个机智的大佬。
求解1000个数独时性能分析如下图:
由下图可见花费最多的是Solve迭代的过程以及判放置数字是否合理的 if 语句
六. 代码说明
1. 生成终局关键代码
三层循环进行每个终局每行的生成,source存储模板行移位数量,以生成新行;最外层交换第3、4、5行,中间层层交换第6、7、8行,最里层全排列模板行,生成行的同时存入输出大数组output中。
2. 求解终局关键代码
每有一个数字被三维数组set_security判为合理即更新它的相应set_security的值,注意在未找到合法数字从下级返回后要将已经改变的上级set_security值还原。
七. 关于GUI
由于时间问题,最终没能实现GUI,只是编写了生成待解数独的代码。
题目要求每33小块中挖空至少2个,所有挖空不少于30个,不多于60个。因此我首先利用已经写好的生成终局的代码生成原始终局,再在其中挖空。每个33的小块我手动固定使两个位置为0,剩下的12-42个挖空采用随机数生成总数目以及其具体横纵坐标,具体实现代码如下:
八. 总结
通过本次个人项目学习到了很多以前没有尝试过的新技术,使我拓展了技术思维。第一次以作者的身份来到熟悉的CSDN,第一次拥有自己的github仓库,也尝试了测试用例的编写。在这个过程中切身体会到了工程开发中的有趣和难度,未来要更多地学习和体验新知识、新技术。