软件工程基础个人个人项目 数独终局声称与解数独问题的控制台程序
Github项目
https://github.com/YZqiangGithub/SolveSudokuPromblem
时间预估:
从项目的描述来看,项目的需求比较单一,通过命令行参数来控制当前输出要求数量的数独的终局还是给出前所给文件路径下的数独问题的一个可行解。
模块划分:
- 命令行参数类型和合法判断还有参数处理
从命令行得到命令行参数后,先判断命令行给出的命令类型,是输出要求数量的终局还是解一个数独问题,接着判断下一个参数的合法性,如要求生成的终局数是否为一个1~1e6的整数。以上检查完毕则调用相应的模块。
- 生成数独终局
生成命令行中输入的指定数量的终局,并按照指定的格式输入文件suduku.txt。
- 解决数独问题
从指定的路径中的到需要解决的数度问题,一个可行解按照要求的格式输入到文件sudoku.txt。
功能建模 :
通过数据流图来进行功能建模。
顶层图:
第一层图:
行为建模:
状态转换图:
生成终局算法思路:
我在一开始是想通过随机的办法来解决生成不同的解,但是每一次生成的终局都要将前面的计算过程全部走一遍,效率太低,所以我就参考了xxrxxr的博客, 以一个已有的终局为模板,通过以下的两种方式生成剩下的终局:
1.数字的交换
因为当前的终局已经满足数独条件,且数字的交换并不会破坏数字间的位置关系,因此可以通过数字的交换来生成其他终局。
第 1 行第 1 个数字固定为学号末两位模 9 加 1 ,因此只能交换剩下 8 个数字,可以生成8! = 40,320
种终局
2.行的交换
数独终局 1-3、4-6、7-9 行之间可以交换,且不破坏数独条件,因此可以通过行的交换生成其 他终局。
第 1 行第 1 个数字固定为学号末两位模 9 加 1 ,因此只能交换 2-3、4-6、7-9行,可以生成2! * 3! * 3! = 72
种终局
两种方式相结合将有8! * 2! * 3! * 3! = 2,903,040
种终局,大于所要求的1e6。
求解数独思路:
求解的主要思路设计参考了暴力算法之美:如何在1毫秒内解决数独问题?| 暴力枚举法+深度优先搜索 POJ 2982,即通过暴力枚举和深度优先搜索,但是采用的这两种办法所消耗的计算较大,求解的数独数目最高可达到1000000个,且空白的数目很多,因此需要对算法进行优化。
优化算法主要通过将数独中的空白按照可选填数字由大到小的顺序进行排序,因此每个空白格可选填的数字个数就是(9 - max(所在行已填入数字个数,所在列已填入数字个数))。
更加精准的优化方式即通过每个 3 x 3 的小宫格来优化,优化后每个空白格填入的数字个数就为(9- max(所在行已填入数字的格数,所在列已填入数字的格子的格数,所在3 x 3 的小宫格已填入数字的格数))。
按照需求分析中功能建模的设计,控制台程序主要分为三个部分:
- 命令行参数判断
- 解决数独问题
- 生成要求数量的数独终局
判断命令行参数集成到主函数中。除了主函数外,还有两个主要的函数:
解决数独问题函数:SudukuSolved(string path);
生成指定数目的数独终局:SudokuGenerate(int Num)
交换空白格的次序 Swap(int * a, int * b)
得到行,列和宫格已填入格数最大值GetMax(int a, int b ,int c);
DFS搜索数独问题的解:DFS(int depth)
将数据写入文件:WriteIntoFile()
记录已填入格子:SetMark(int row,int col, int block ,int flag)
将数独解重置:Reset()
得到宫格是第几个宫格:GetBlockNum(int row, int col)
基本函数调用关系如下:
单元测试
设计了以下功能的单元测试代码:
(1)测试生成数独终局算法是否能够生成正确且不重复的数独终局。
(2)测试解决数独问题算法能否产生正确的数独结果。
在编写程序的过程中我通过注释主函数并重写的方式调用函数WriteIntoFile(),Swap()等辅助函数测试其效果。
性能优化:
第一个版本生成1e6个数独,在Visual Studio2019下sudokuGenerate运行时间为4分37秒。
以下为Visual Studio2019 的性能分析工具进行分析结果:
可见占有CPU使用最多的是写入文件部分,有
std::operator<<<std::char_traits<char> >
std::basic_filebuf<char,std::char_traits<char> >::_Unlock
std::basic_filebuf<char,std::char_traits<char> >::_Lock
std::basic_filebuf<char,std::char_traits<char> >::_overflow
最初版本的I/O方式为在生成要求数量的数独终局前首先打开一个文件,每生成以个元素则将其写入,查询cppreference.com ,结合代码分析出性能瓶颈主要为:
每生成一个数独元素即将其写入文件导致std::basic_filebuf 为了维护文件位置会对指针进行频繁的操作, 同时在生成数独终局过程中文件始终保持着打开状态,sync耗费资源保持文件同步。
对于以上问题,可通过改进I/O方法优化程序性能:
通过一个大数组 output 按要求形式存放所有生成终局结果,类似于缓存区的作用,在最后一次性写入文 件。
经过改进,Visual Studio 2019上运行时间优化到了2.378s。
在Visual Studio 2019的性能分析结果如下:
可见,经过对文件写入的优化,整体性能得到大幅提升,此时系统中最占用CPU资源的是排列组合函数next_permutation。
代码说明:
代码结构比较简单,主要有两个算法:指定数量数独终局生成算法与数独问题求解算法
- 指定数量数独终局生成算法
- 数独问题求解算法
实际时间花费:
PSP2.1 | Personal Software Process Stage | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 20 | 10 |
·Estimate | ·估计这个任我要花多少时间 | 10 | 10 |
Development | 开发 | 30 | 25 |
·Analyse | ·需求分析(包括学习新技术) | 150 | 150 |
·Design Spec | 生成设计文档 | 30 | 40 |
Design Review | ·设计复审(和同事审核设计文档) | 60 | 60 |
·Coding Standard | ·代码规范(为目前的开发制定合适的规范) | 30 | 20 |
·Design | ·具体设计 | 90 | 60 |
·Coding | ·具体编码 | 500 | 600 |
·Code Review | ·代码复审 | 300 | 300 |
·Test | ·测试(自我测试,修改代码,提交修改) | 200 | 300 |
Reporting | 报告 | 90 | 110 |
·Test Report | ·测试报告 | 90 | 90 |
·Size Measurement | ·计算工作量 | 10 | 10 |
·Postmortem & Process Improment Plan | ·事后总结,并提交过程改进计划 | 30 | 40 |
合计 | 1640 | 1825 |
总结:
首先,我想先谈谈我的收获,在整个项目完成过程中,我感受最深的有两点:
第一,通过Visual Studio 对于代码的性能分析结果让我了解到了对文件的IO操作会极大地影响整个程序的性能,通过建立一个数据结构作为输出数据的缓存区,在数独生成或数独问题求解实现后将最终结果以此性写入文件,根据性能分析的优化前后比对,一次性IO操作与每当产生一个数据就写入文件比较,程序性能得到非常大的提升。
第二,通过学习,我对软件工程的项目从建立到完成有了比较完整的认识,虽然我认为我对PSP的每个步骤认识不是很完整,甚至在项目的整个完成过程中还是会出现许多纰漏,但是我仍然透过此次的个人项目窥见了一个软件项目的完成过程,获益良多,我也相信此次的实践将会给我将来参与的项目提供宝贵的经验。
其次,我的项目还是存在许多需要改进的地方:
第一,对于单元测试部分我的了解不多,所以我只对生成的终局和数独问题的解的正确性还有生成的终局是否与其他的数独生成终局不同做了测试,对于其他的辅助函数,比如返回行,列和小宫格中已填入格数最多我则没有使用程序进行测试,而只是通过调试时修改一下函数参数进行测试。同时,测试生成的终局是否与其他的数独生成终局不同,我通过一个结构体数组来存储每个生成的终局,每生成一个终局检查其与前面的终局是否不一样,这样使得创建的结构体数组浪费了大量空间,改进的方案我觉得可以通过hash值来代表不同的数独终局,这样可以同时减少空间复杂度和时间复杂度。
第二,具体设计还是要做好,首先我在对项目进行具体设计的时候没有充分考虑,使得一些变量的设计不够充分,有时在后续的具体编写过程中发现一些部分设计得过于抽象,于是要将这部分的设计重新再进行一遍。