项目地址:https://github.com/MrFasl/1120172188
PSP表格
PSP2.1 | Personal Software Process Stages |
预估耗时 (分钟) |
实际耗时 (分钟) |
Planning | 计划 | 300 | |
Estimate | 估计这个任务需要多少时间 | 4000 | |
Development | 开发 | 1200 | |
Analysis | 需求分析(学习新技术) | 700 | |
DesignSpec | 生成设计文档 | 200 | |
DesignReview | 设计复审 | 60 | |
CodingStandard | 代码规范 | 60 | |
Design | 具体设计 | 200 | |
Coding | 具体编码 | 900 | |
CodeReview | 代码复审 | 60 | |
Test | 测试 | 360 | |
Reporting | 报告 | 60 | |
TestReport | 测试报告 | 60 | |
SizeMeasurement | 计算工作量 | 15 | |
合计 | 8175 |
![](/qrcode.jpg)
程序需求
生成不重复的数独终局到文件
读取文件内的数独问题,求解并将结果输出到文件
功能建模:
顶层图
一层图
程序实现
数独的生成:
看到这个题目的时候起初感觉暴力做就OK了,但是回头看见一个1e6发现事情并不简单,数量是可以达到的,
每个数独终局包含9*9个数字,第一个字母为(8+8)%9+1 = 8,即每个数独终局第一个数字均为8
数量要求为1<=N<=1000000 = 1e6 在首个数字固定的情况下,第一行可用的组合有8!= 40320种 <1e6。
现在考虑如何扩充数量,由于第一行不能改变,所以考虑456行和789行的行内交换,每个三行全排列个数为3! = 6种 则每个终局由后六行的排序而增加的数量为 6*6种,于是总的组合由6*6*8!>1e6种。数独终局数量问题解决。可以把第一行向右平移一定的位数,只要每一行平移的次数和之前的任意一行不一样,那么每一列也不会有重复的数字。接下来,考虑九宫格的不重复问题,我们只需要将每三行看成一组,组内向右移动都移动相差3的倍数个单位时就可以保证每宫内没有重复的数字。
生成终局
首先以8开头生成第一行,使用全排列函数,然后对每个第一行进行右移操作,生成一个基础终局,随后对这个基础终局进行行变换操作。
主要函数如下:
1 void BuildMove(int N) 2 { 3 char rule1[10][5] = { "036","063" }; 4 char rule2[10][5] = { "258","285","528","582","825","852" }; 5 char rule3[10][5] = { "147","174","417","471","714","741" }; //变换规则 6 while (1) 7 { 8 if (next_permutation(origin, origin + 8)) 9 { 10 origin[8] = 8; 11 for (int i = 0; i < 2; ++i) 12 { 13 for (int j = 0; j < 6; ++j) 14 { 15 for (int t = 0; t < 6; ++t) 16 { 17 BuildSudoku(rule1[i], rule2[j], rule3[t]); 18 countnum++; 19 if (countnum == N) 20 return; 21 } 22 } 23 } 24 } 25 else 26 break; 27 } 28 }
函数中调用的BuilSudoku()函数即为行变换操作。
行变换关键代码
void ChangeMap(char *rule) //进行变换 { for (int i = 0; i < 3; ++i) { output[datacount++] = origin[(8 + rule[i] - '0') % 9] + '0'; for (int j = 1; j < 17; ++j) { output[datacount++] = ' '; j++; output[datacount++] = origin[((16 - j) / 2 + rule[i] - '0') % 9] + '0'; } output[datacount++] = '\n'; } }
数独问题的解决:
一开始我真的不知道怎么解数独,我都没玩过数独,所以就老老实实地去看博客找资料了。找了不少博客,决定使用回溯法解决。
采用暴力回溯方法,对于每个数独问题,进行回溯方法解决,参考了好几个博客。
参考博客:https://blog.csdn.net/u012787710/article/details/77372897
https://blog.csdn.net/qq_32216775/article/details/79710137
https://blog.csdn.net/fdaixu/article/details/86307180
还有一个很有用的知乎专栏:https://zhuanlan.zhihu.com/p/31865810?utm_source=qq&utm_medium=social
回溯算法
void TraceBack(int n) { if (suc == 1) return; if (n > 80) //代表解完当前数独 { prt(); suc = 1; return; } if (res[n / 9][n % 9] != 0) //当前格子有数字,跳到下一格 { TraceBack(n + 1); } else { for (int i = 1; i <= 9; i++) { int f = CheckCanVis(n / 9, n % 9, i); if (f == 0) continue; else { res[n / 9][n % 9] = i; SetVis(n / 9, n % 9, res[n / 9][n % 9]); TraceBack(n + 1); ResetVis(n / 9, n % 9, res[n / 9][n % 9]); res[n / 9][n % 9] = 0; } } } }
单元测试
对内部函数使用了白盒测试,主要是对一些判定函数的正确性和对一些返回值较多的函数进行了测试,发现在判定函数中存在一个错误的返回值,改进后通过测试。
性能分析
生成数独终局
在最开始的版本中,终局的生成速度简直慢的令人发指,我在VS中做了100000的测试,结果等了三四分钟都没有结果,然后就手动结束了。
于是上网搜索改进方法,发现好多学长的博客中都提到了文件操作和大缓存区。然后我就对代码进行了改进。
首先是文件输出,这是不可替代项,所以要尽量减少使用次数,和大缓存区结合起来,我开了一个巨大的缓存区,储存得到的数独终局,为了少点空间开成了一维。
当数独终局生成生成执行完毕后,调用ofstream输出数独终局,结果快了很多倍。
可以看出时间的大头仍然是输出函数printf1和生成函数BuildMove但是总时间降到了最低的1.03秒。
求解数独
这部分的时间主要是算法的设计,吸取了数独终局生成的教训,又开了一个比较大的缓存区来记录结果,随后一并输出。剪枝的操作比较麻烦。
我在网上找到了数独残局1000个作为测试用例,性能分析如下
可以看出文件操作还是占了一定的时间,回溯算法TraceBack是算法时间的大部分。
代码质量分析
visual studio2017无错误和警告
注:会遇到预编译(表现为fopen_s报错)问题,解决方案是打开项目属性页,在C/C++选项中选择预处理器,并在右边的预处理器定义中加上 _CRT_SECURE_NO_WARNINGS (注意用分号隔开前面的定义)
PSP表格
PSP2.1 | Personal Software Process Stages |
预估耗时 (分钟) |
实际耗时 (分钟) |
Planning | 计划 | 300 | 200 |
Estimate | 估计这个任务需要多少时间 | 4000 | 2000 |
Development | 开发 | 1200 | 100 |
Analysis | 需求分析(学习新技术) | 700 | 800 |
DesignSpec | 生成设计文档 | 200 | 300 |
DesignReview | 设计复审 | 60 | 30 |
CodingStandard | 代码规范 | 60 | 30 |
Design | 具体设计 | 200 | 120 |
Coding | 具体编码 | 900 | 700 |
CodeReview | 代码复审 | 60 | 120 |
Test | 测试 | 360 | 300 |
Reporting | 报告 | 60 | 120 |
TestReport | 测试报告 | 60 | 60 |
SizeMeasurement | 计算工作量 | 15 | 10 |
合计 | 8175 | 4890 |
项目感受:
1.开发一款软件绝不是简单的写写代码就OK的,需要分析需求,建立模型等一系列步骤。程序文档和程序一样重要。
2.写函数一定要把所有分支都写好,否则会遇到很大麻烦。
3.程序运行时间不仅取决于算法的设计,还有工程的设计。
4.软件开发和算法设计是不同的两回事。
5.开发时间要尽量紧凑一些。