[软件工程基础]个人项目——数独
目录
一、Github项目地址
二、PSP估计
三、解题思路描述
3.1、 生成终局
3.1.1、暴力搜索——回溯法
3.1.2、模板法
3.2、求解数独
3.2.1、暴力搜索——回溯法
3.2.2、回溯+扫描法
3.3、数独挖空
四、设计实现过程
4.1、代码规范
4.2、函数设计
4.2.1、程序基本流程图
4.2.2、函数关系图
4.3、单元测试
五、改进性能
5.1、算法优化
5.1.1、生成终局部分
5.1.2、求解数独部分
5.2、输入输出优化
六、代码说明
6.1、生成终局部分
6.2、求解数独部分
七、PSP实际
八、心路历程与收获
一、Github项目地址
项目地址:
https://github.com/XCyclone/PersonalProject-Sudoku
二、PSP估计
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | |
·Estimate | ·估计这个任务需要多少时间 | 60 | |
Development | 开发 | 1560 | |
·Analysis | ·需求分析(包括学习新技术) | 120 | |
·Design Spec | ·生成设计文档 | 60 | |
·Design Review | ·设计复审(和同事审核设计文档) | 60 | |
·Coding Standard | ·代码规范(为目前的开发制定合适的规范) | 40 | |
·Design | ·具体设计 | 60 | |
·Coding | ·具体编码 | 900 | |
·Code Review | ·代码复审 | 120 | |
·Test | ·测试(自我测试,修改代码,提交修改) | 200 | |
Reporting | 报告 | 380 | |
·Test Report | ·测试报告 | 120 | |
·Size Measurement | ·计算工作量 | 60 | |
·Postmortem & Process Improvement Plan | ·事后总结,并提出过程改进计划 | 200 | |
合计 | 2000 |
三、解题思路描述
本项目可以分为三大块:
- 确定终局生成、数独求解的算法
- 进行代码测试、性能分析
- 进行版本管理、项目规范
3.1、 生成终局
生成终局部分主要有如下几个条件:
- 生成的数独终局不重复
- 生成个数在1到1e6之间
- 文件内的格式:数与数之间由空格分开,终局与终局之间空一行,行末无空格
3.1.1、暴力搜索——回溯法
对于生成不重复的满足所在行、所在列、所在3*3方格内均无重复数字的终局,首先想到的肯定是暴力搜索,对于每一个空尝试填入符合规则的数,然后递归求解,这样可以保证生成有效终局并且不重复,但经过测试这样在生成1e6的条件下速度较慢。
3.1.2、模板法
由于暴力搜索速度较慢,所以想尝试其他方法,经过在网上的相关算法的搜索,发现了一种较为简单便捷的生成方法——模板法。参考链接:数独终盘生成的几种方法、数独终盘模板
设置一个固定的字母模板,通过对模板中不同字母的赋值,可以生成不同的终局。由于终局的第一位为(2+1)%9+1 = 4(学号后两位相加%9+1)剩下的八位可以通过递归在1~3、5~9之间顺序赋值,这样可以产生8!=40320个终局。采用模板如下:
i | g | h | c | a | b | f | d | e |
c | a | b | f | d | e | i | g | h |
f | d | e | i | g | h | c | a | b |
g | h | i | a | b | c | d | e | f |
a | b | c | d | e | f | g | h | i |
d | e | f | g | h | i | a | b | c |
h | i | g | b | c | a | e | f | d |
b | c | a | e | f | d | h | i | g |
e | f | d | h | i | g | b | c | a |
这样距离1e6的数量级还差一部分,但是我们发现在1~3、4~6、7~9之间交换任意两行或两列可以生成符合规则的与之前不同的终局,由于第一行、第一列不能用作交换,所以有2!×3!×3!×2!×3!×3!= 5184种,5184×40320 = 209018880为1e8数量级,完全可以满足1e6的数量级。
3.2、求解数独
3.2.1、暴力搜索——回溯法
跟生成终局一样,求解也可以使用回溯法进行求解,但对于1e6的数量级求解较慢。
3.2.2、回溯+扫描法
经过个人进行的数独游戏,我发现在人做数独时经常是对一个空查找能否有唯一可填的数字,如果有就填入,如果没有就对其他空进行这样的尝试,最后遇到的无法确定的空有可能存在不唯一解,此时再进行推理或者以先填入一个数进行尝试。
由此,可以结合人在做数独时的想法,先对数独盘进行扫描,如果有可以确定的空马上填入,如果扫描整个数独盘都没有可以再填入的就说明可能出现了不唯一的解,之后再用回溯法,这样回溯的空间较小,效率较高。
3.3、数独挖空
这是为了生成数独盘的部分,由于需要对于求解数独的速度进行测试,所以设计算法将终局挖空形成不同的数独盘,遵照了整个数独盘上的挖空不少于30个,不多于60个,且每个3*3小数独盘中挖空不少于两个。
最初由于使用随机的方法发现生成了一些整个行都被挖空的情况,这对于求解存在很大的难度,后来改为对不同位置的3*3小数独盘挖不同数量的空格,比如第一排第一个挖少量的2~4个,第二排第二个挖较多的5~8个,按照如下挖取:
少 | 多 | 少 |
多 | 少 | 多 |
少 | 多 | 少 |
四、设计实现过程
4.1、代码规范
本项目采用c++程序设计语言。
4.2、函数设计
本程序共实现了两个类,分别为Generator和Solution,Generator为终局生成的类,Solution为数独求解的类,分别通过构造函数初始化传递参数。此外设计了一个通过挖空生成数独局的类Blank用于为数独求解进行测试,用-b参数进行生成,所提交github部分只保留了函数未进行引用。
4.2.1、程序基本流程图
4.2.2、函数关系图
4.3、单元测试
对Generator和Solution函数进行单元测试:
输入参数 | -c 1 | -c 20 | -c 1000 | -c -10 | -c 0 | -c abc |
期望结果 | 正确 | 正确 | 正确 | 错误 | 错误 | 错误 |
输入参数 | -s .\\block_1.txt | -s .\\block_20.txt | -s .\\block_1e3.txt | -s .\\block123.txt |
-s C:\\Users\\yaoxx\\Desktop\\ Sudoku2\\Debug\\block_1.txt |
|
期望结果 | 正确 | 正确 | 正确 | 错误 | 正确 |
测试结果如下:
其使用插件测得覆盖率如下:
其中的generator类由于wrap函数因只用生成1e6数量级终局而并未使用全部,所以覆盖率较低。
五、改进性能
5.1、算法优化
对于生成终局部分首先采用了回溯法,在1e6的条件下生成速度较慢,后改用模板法,此过程不包含输出到文件。对于求解数独部分首先采用了递归求解法,但考虑到当空过多时求解空间过大,所以尝试用扫描+递归法进行改进。
5.1.1、生成终局部分
1)回溯法
总执行时间为9.284s,其中只有深度搜索函数generate_dfs(),独占率到达99.9%。
2)模板法
总执行时间为2.685s,其中包括首行递归生成generate_firts_line(int )、模板生成change_to_num()、交换行列wrap(int, int )等部分。其中独占时最久的为生成首行函数generate_firts_line。
改为模板法后,生成时间明显缩短,其中最为费时的仍未递归的首行生成部分。
5.1.2、求解数独部分
1)回溯法
从上图性能分析报告可以看出,其消耗最大的函数为递归填充空格函数full_dfs(),这是数独求解部分的核心函数,通过不断尝试所能填充的数字求解数独。
2)扫描+回溯法
由于上述方法求解较慢,故尝试通过扫描+回溯的方法求解,其主要想法是,只有在每个空格都没有可以确定填入的数的时候再对于整个数独盘采用回溯法求解,这样可以有效减小求解空间。
由上图发现,消耗较大的函数由原来的递归填充空格函数full_dfs()变为了扫描法填充单个空格函数full_block(),但遗憾的是,总体上效率并未有什么提升。
5.2、输入输出优化
经过测试发现,输入输出的速度对于整体性能有很大的影响,最开始我采用了C++头文件fstream中的函数进行文件读写,但发现在生成1e6数量级终局时就出现了200s+的情况,之后通过上网搜索和询问同学得知了C++的文件流读写较慢,之后对于写文件改用了fprintf(),fputc(),最后选用了fwirte()以文件流的方式写入文件,在本机上到达了3s左右的效果。
以下是对于生成终局部分1e6数量级的测试部分:
ofstream:
此次终局生成花费超过4分钟,其中重载运算符<<消耗最大,输出函数ouptut()总体耗用CPU到达99%以上。
fprintf():
此次终局生成耗时大约2分钟,与上一次相比有了明显的提升,消耗最大的仍是输出函数fprintf()。
fputc():
改用fputc()后,生成效率有了明显提升,生成1e6数量级的终局只需35s,但是可以看到,消耗最多的函数仍为output()。
fwrite():
fwrite()是向制定文件写入数据块的函数,通过将每个终局以数据块的方式写入文件,可以有效提高速度。
此次生成终局在4s之内,虽然output仍是消耗最大的函数,但相比之前已经有了明显的降低(从99%降到87%)。
六、代码说明
6.1、生成终局部分
6.1.1、递归生成终局第一行
void Generator::generate_first_line(int pos) //递归生成第一行
{if (pos == 9){for (int i = 1; i < 9; i++)if (temp[i] >= 4) temp[i]++;change_to_num(); //根据模板生成终局wrap(2); //对终局的行、列进行交换,由于只需要最大1e6种终局,顾不交换7、8、9行,如需生成超过8709120个终局可改为wrap(1)for (int i = 1; i < 9; i++)if (temp[i] >= 4) temp[i]--;return;}for (int i = 1; i < 9; i++){if (mark[i] == 0){temp[pos] = i;mark[i] = 1;generate_first_line(pos + 1);temp[pos] = 0;mark[i] = 0;}}
}
6.1.2、按模板生成终局其余部分
void Generator::change_to_num() //根据模板和第一行生成数列转换为终局
{for (int i = 0; i < 9; i++){for (int j = 0; j < 9; j++){if (model[i][j] == 'i') box[i][j] = temp[0];else if (model[i][j] == 'g') box[i][j] = temp[1]; else if (model[i][j] == 'h') box[i][j] = temp[2];else if (model[i][j] == 'c') box[i][j] = temp[3];else if (model[i][j] == 'a') box[i][j] = temp[4];else if (model[i][j] == 'b') box[i][j] = temp[5];else if (model[i][j] == 'f') box[i][j] = temp[6];else if (model[i][j] == 'd') box[i][j] = temp[7];else if (model[i][j] == 'e') box[i][j] = temp[8];}}
}
6.1.3、交换不同行、不同列
void Generator::wrap(int l) //将9行(列)分为三组,分别为1、2、3,4、5、6,7、8、9,(由于第一个数字不可改变)在4、5、6和7、8、9中都可随机交换两列使得新终局任满足要求//根据递归求解第一行按模板可生成8!=40320终局,在此基础上交换,共可以有8!*3!*3!*3!*3!=52254720种终局
{if (l == 4){if (now == num) exit(0);output();wrap_col(6, 7);output();wrap_col(7, 8);output();wrap_col(6, 8);output();wrap_col(6, 7);output();wrap_col(7, 8);output();wrap_col(6, 8);return;}if (l == 3){wrap(l + 1);wrap_col(3, 4);wrap(l + 1);wrap_col(4, 5);wrap(l + 1);wrap_col(3, 5);wrap(l + 1);wrap_col(3, 4);wrap(l + 1);wrap_col(4, 5);wrap(l + 1);wrap_col(3, 5);}if (l == 2){wrap(l + 1);wrap_row(3, 4);wrap(l + 1);wrap_row(4, 5);wrap(l + 1);wrap_row(3, 5);wrap(l + 1);wrap_row(3, 4);wrap(l + 1);wrap_row(4, 5);wrap(l + 1);wrap_row(3, 5);}if (l == 1){wrap(l + 1);wrap_row(6, 7);wrap(l + 1);wrap_row(7, 8);wrap(l + 1);wrap_row(6, 8);wrap(l + 1);wrap_row(6, 7);wrap(l + 1);wrap_row(7, 8);wrap(l + 1);wrap_row(6, 8);}
}
6.2、求解数独部分
6.2.1、扫描法对每个空格填充
void Solution::full() //填充空格:通过扫描查看每个空目前可以填充的数字,如果唯一则填入
{flag = 1;int block_num = 0;while (flag) //只要每次扫描都有新的可填入位置,则不断扫描,知道一次扫描不再可以填入新数字{flag2 = 0;for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++){if (box[i][j] == 0) full_block(i, j);}}flag = flag2;}int block_m, block_n, f = 0;for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++){if (box[i][j] == 0) {block_num++;if (f == 0) {block_m = i;block_n = j;f = 1;}}}}if (block_num) full_dfs(block_m, block_n); //当扫描法结束后仍然有空位存在,则解不唯一,进行递归求解output();
}
6.2.2、对单个空格进行判断填充
void Solution::full_block(int m, int n) //填充某空格:通过对行、列、3*3矩阵的扫描确定可以填入的数组,如果唯一则马上填入,不唯一则等待下次扫描
{int check[10] = { 0 };int k = 9 * m + n;int f = 0;for (int i = 0; i < 9; i++) if(box[i][n] != 0) check[box[i][n]] = 1;for (int i = 0; i < 9; i++) if (box[m][i] != 0) check[box[m][i]] = 1;for (int i = 3 * (m / 3); i < 3 * (m / 3) + 3; i++) {for (int j = 3 * (n / 3); j < 3 * (n / 3) + 3; j++){if (box[i][j] != 0) check[box[i][j]] = 1;}}for (int j = 1; j < 10; j++){if (check[j] == 0 && f != 0) f = -1;else if (check[j] == 0) f = j;}if (f > 0){box[m][n] = f;flag2 = 1;}
}
6.2.3、对剩余空格进行递归填充
bool Solution::full_dfs(int row, int col) //递归求解剩余空格(此时求解空格较少,速度较快)
{int i, j, n;int next_row, next_col;n = 0;while (1) {next_num:n++;if (n >= 10) break;for (j = 0; j < 9; j++) { // 判断行是否有重复if (box[row][j] == n) {goto next_num;}}for (i = 0; i < 9; i++) { // 判断列是否重复if (box[i][col] == n) {goto next_num;}}for (i = 3 * (row / 3); i < 3 * (row / 3) + 3; i++) { //判断所在3*3矩阵是否有重复for (j = 3 * (col / 3); j < 3 * (col / 3) + 3; j++) {if (box[i][j] == n) {goto next_num;}}}box[row][col] = n; //当该单元可以填充if (!find_next_block(row, &next_row, &next_col)) { //填满,则找到可行解return true;}if (!full_dfs(next_row, next_col)) { //否则继续填下一个未填充的空box[row][col] = 0;continue;}elsereturn true;}return false;
}
七、PSP实际
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
·Estimate | ·估计这个任务需要多少时间 | 60 | 60 |
Development | 开发 | 1560 | 1680 |
·Analysis | ·需求分析(包括学习新技术) | 120 | 120 |
·Design Spec | ·生成设计文档 | 60 | 120 |
·Design Review | ·设计复审(和同事审核技术文档) | 60 | 60 |
·Coding Standard | ·代码规范(为目前的开发制定合适的规范) | 40 | 60 |
·Design | ·具体设计 | 60 | 100 |
·Coding | ·具体编码 | 900 | 800 |
·Code Review | ·代码复审 | 120 | 120 |
·Test | ·测试(自我测试,修改代码,提交修改) | 200 | 300 |
Reporting | 报告 | 380 | 350 |
·Test Report | ·测试报告 | 120 | 120 |
·Size Measurement | ·计算工作量 | 60 | 30 |
·Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 200 | 200 |
合计 | 2000 | 2090 |
八、心路历程与收获
这是我第一次进行流程完整的软件开发,从前做的项目只是完成了一些功能,但其需求、效果等方面往往都不在考虑范围内,但这一次经过从最开始的需求分析阶段到目前为止最后的代码测试阶段,我才终于完整的体验了开发软件的过程。
从12月1号的分析开始,到现在已经接近一个月的时间,我在不断摸索中前行,磕磕绊绊,说起来也遇到了不少问题。
这次项目我遇到的第一个问题就是系统的问题,我之前使用的是MAC系统,这首先就不符合这次项目Windows 10的开发环境,为此我自装了Windows系统并在同学的帮助和无数教程的指引下入门了Visual Studio,并再次期间对题目进行了一定的分析,大致构建了此次项目的实施方式。
此后才在12月8日开始了代码的开发部分,但由于之前已经有了很系统的思考,所以代码的编写部分只花了不到两天就几乎完成了。但经过测试,发现其性能太差,生成1e6终局需要200s左右,才在网上进行生成数独终局的相关搜索,改进了方法。本以为会有很大的提升,但是发现其效率并没有过大改变,之后才发现是读写文件的问题,再经过fprintf()、fgetc()、fwrite()、三次改正之后,才将生成速度提升到3s左右。关于求解部分虽然早早写完,但是并没有写挖空函数,所以一直没有对其求解速度进行了解,写了挖空函数之后,发现求解速度也有待提升,所以想到扫描法结合回溯法的方法,按理来说应该会有较大的提升,但是实际测试时并没有,经过不断查找问题还是没有得到解决,所以最后也只有回溯法的效果,不过我决定在此后有时间时再进行改进,争取找到原因。
之后的附加题虽然也写了挖空函数,但是由于对C++的GUI部分和Visual Studio都不够熟悉,并在初次尝试了MFC,开发过程中遇到了一点问题在网上资源有限的情况下一直无法解决,之后由于时间原因被搁置了下来,所以决定在学期结束后在此方面进行学习。
关于单元测试部分,由于VS社区版无法测得覆盖率,所以删了很多软件空出内存去下载了企业版,但在使用途中出现了一些问题,且经过各种搜索都没有解决,最后只好用插件测得。
通过此次个人项目,我收获良多,并且也逐步体会到软件开发的乐趣,通过不断将任务细化,每天完成一部分,看着自己的项目不断有所进展,自己也学习到更多,心里有了一种满足感。
[软件工程基础]个人项目——数独相关推荐
- 软件工程基础-个人项目-数独游戏
软件工程基础-个人项目-数独游戏 ----------------------------------------------------------------------------------- ...
- 软件工程基础个人项目——数独(5)
软件工程基础个人项目--数独 点击这里可看github上的具体代码 本次个人项目关于数独的生成与求解 PSP表格 PSP2.1 Personal Software Process Stages 预估耗 ...
- 软件工程基础——个人项目——数独(1)
软件工程基础--个人项目--数独(1) 一.实现目标 1.生成数独终局 命令行输入如下: sudoku.exe -c 20 sudoku.exe为最终实现程序,-c确定活动为生成数独终局,20为生成结 ...
- 软件工程基础-个人项目-数独
个人项目–数独 目录) 个人项目--数独 1 项目地址 2 PSP表格 3 思路描述 3.1 数独终局生成 3.1.1. 暴力法 3.1.2. 全排列及行变换 3.2 功能实现思路 3.2.1. 数独 ...
- [软件工程基础]结对项目 数独程序扩展
(1)在文章开头给出Github项目地址.(1') 项目地址:https://github.com/JerryYouxin/sudoku (2)在开始实现程序之前,在下述PSP表格记录下你估计将在程序 ...
- 软件工程基础大项目——数独问题
Github项目地址:https://github.com/WX78yyj/sudoku(由于自己有不爱命名的坏习惯,所以忘记命名了,结果提交的文件名称是未命名3,发现还改不了,郁闷) PSP2.1 ...
- 数独问题流程图_软件工程基础大项目——数独问题
Github项目地址:https://github.com/WX78yyj/sudoku(由于自己有不爱命名的坏习惯,所以忘记命名了,结果提交的文件名称是未命名3,发现还改不了,郁闷) PSP2.1 ...
- 软件工程基础个人项目——数独终局生成求解
目录 1.源代码的GitHub链接: 2.PSP表格(预估): 3.题目要求: 4.解题思路: 1)数独游戏规则 2)生成数独终局 2)求解数独 5.设计实现过程: 第一部分:sudoku类的构建 第 ...
- 软件工程基础课-个人项目-数独
一.项目地址 二.PSP 三.解题思路 四.设计实现过程 4.1 代码风格规范 4.2 函数关系图 五.程序性能分析及改进 六.代码说明 七.单元测试与代码覆盖率分析 八.项目总结 8.1 个人的提升 ...
最新文章
- 专访NIPS主席:如何保证论⽂评审的公平性?| 人物志
- RocketMQ的原理与实践
- python和什么一起学_[lvog1]和小菜一起学python(零基础开始学习)
- CMake基础 第5节 安装项目
- java线程池(ThreadPool)
- 农民约翰是一个惊人的会计_我的朋友约翰在CSS Grid中犯了一个错误。 不要像约翰-这样做。
- Vue性能优化:图片与组件,如何实现按需加载?
- Bootstrap3 滚动监听插件的调用方式
- 准备创业或刚创业的朋友必读
- 运算放大器相关参数基本知识(一)
- 如何配置Sql Server 2005之ODBC数据源连接
- Java实现POS打印机无驱打印(转)
- 微信小程序 import文件大小限制
- 云剪贴板:以备不时之需
- win7计算机还原点建立,win7系统每次启动自动创建还原点的处理技巧
- web界面测试用例(shelley_shu)
- 软件测试初学者精华(一)
- STM32F4主板硬件设计与接口
- h3c查在线计算机,H3C 交换机查看所有端口状态的命令
- 阿里彻底去中台了,你真以为中台不行了?
热门文章
- python表情包语言_我是斗图王之python爬取表情包
- 一些不错的 SCI 科研工具
- HTTP之长连接、短连接、持久连接
- 【理论恒叨】【立体匹配系列】经典AD-Census: (1)代价计算
- CCRC认证和ISO27001认证有哪些不同?企业该如何申请?
- 谈计算机知识对学生的作用,浅谈学习计算机基础知识对中专学生的重要性
- PC-Dmis 二次开发 |(二):VB基本语法
- mysql数据库设计汉字转拼音函数
- 利用Backtrack劫持cookie
- 在Ubuntu 18.04 LTS安装ROS Melodic版机器人操作系统(2019年10月更新MoveIt! 1.0 ROS 2.0 Dashing)