目录
一、GitHub的网址
在博客的最前面附上的github上的网址:https://github.com/lyyyrx/sudoku
二、PSP表格和每项的预估时间
PSP2.1 | 个人软件过程阶段 | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 60 |
|
Estimate | 估计这个任务需要多少时间 | 10 |
|
Development | 开发 |
|
|
Analysis | 需求分析(包括学习新技术) | 300 |
|
Design Spec | 生成设计文档 | 120 |
|
Design Review | 设计复审(和同事审核实际文档) | 30 |
|
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 10 |
|
Design | 具体设计 | 60 |
|
Coding | 具体编码 | 900 |
|
Code Review | 代码复审 | 120 |
|
Test | 测试(自我测试,修改代码,提交修改) | 180 |
|
Reporting | 报告 | 180 |
|
Test Report | 测试报告 | 120 |
|
Size Measurment | 计算工作量 | 10 |
|
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 30 |
|
| 合计 | 2130 |
|
三、解题思路
由于11月月末有两门考试,考完之后又急急忙忙地补上了其他科目的作业,加上周六又提前安排了别的事情,所以在12月2号才开始着手完成老师留的个人项目(实话说大作业提交截止时间填的是12月25号的我心里还是有些忐忑的,因为12月24号还有一门OS考试)。
首先是完成相应的准备工作。由于自己是个小白,因而之前并没有注册的github上账号。首先先注册了账号(中间还发生了一点意外),同时翻出了半年前注册的完全空空如也的没有任何内容CSDN的博客账号。然后便开始着手做个人项目了。
刚开始拿到这个问题的时候,感觉自己的注意力全部都放在了开发的要求上,内心的慌张确实让我有些找不到头绪。最终我决定静下心好好分析一下这个问题。
数独想必大家都玩过,规则也很简单,就是每一行、每一列、每一个九宫格(3*3)内的数字均含不重复的数字1-9(九宫格的定义见下图,图片来源自百度百科)。
清楚了规则之后来看个人项目,个人项目必须要完成的目标由两部分构成,其中第一部分是生成终局,这部分比较重要的点有如下几点:
- 生成无误的数独终局
- 输入格式为:-c N(N为需要生成的终局的数目)
- N大于等于1并且小于1000000(上限较高)
- 生成的数独终局要用TXT文件保存
- 新生成的TXT文件要覆盖旧版的TXT文件
- 数字与数字空一格(行末无空格),终局和终局空一行
- 能处理异常输出
- 第一行的第一个数字为学号末两位相加模9之后再加1,我的学号末两位为32,所以这个数字应该为6。
输入输出的格式判断通过和同学讨论以及查阅资料很好地就解决了(https://blog.csdn.net/dcrmg/article/details/51987413)。输出到txt文本的函数也决定好用fopen函数(后来由于c++判定不安全,又改成了fopen_s)。
最终剩下的比较棘手的点就是如何生成数独。最开始想随机生成,但是不仅生成函数自己没有头绪,而且感觉每次都要判定是否重复,感觉会浪费很多时间。后来看了看要求中没有说不允许每次输出的顺序都一样,因此后来想了一种方法,具体如下:
首先将1到9随机排列。(由于我的左上角数字为6,因此例子中我将6放在了首位;同时最后用的next_permutation函数是按特定顺序排列的,因此也就不是随机了)
例如:6 1 2 3 4 5 7 8 9
我想的方法是:剩下八行都由第一行的排列变换而来,除去旧的排列的首位和末位数字以及新的排列的首位和末位数字,其他数字的左右两边数字不变。换言之,就是将前N(N = 1,2,3,4,5,6,7,8)个数字按顺序移至排列最后,由于两条限制条件:(1)一个九宫格内1〜9各应出现一次;(2)各行各数字应出现一次;(3)各列各数字应出现一次因此总能找到至少一种排列能满足以上限制条件。
例如:第一行:6 1 2 3 4 5 7 8 9
此时,第二行和第三行的排列已经确定(其他情况无法满足限制条件(1)),即分别将前三位和前六位移至末尾:
3 4 5 7 8 9 6 1 2
7 8 9 6 1 2 3 4 5
第456行和789行也可以看成一组(三个一轮换),为保证满足限制条件(3),剩下所有6行(第4~9行)应当分别将前1,2,4,5,7,8个数字移至末尾(相对于第一行),同时“1,4,7”一组“2,5,8”一组。
由此可以得到满足要求的最终的数独矩阵之一:
6 1 2 3 4 5 7 8 9
3 4 5 7 8 9 6 1 2
7 8 9 6 1 2 3 4 5
1 2 3 4 5 7 8 9 6
4 5 7 8 9 6 1 2 3
8 9 6 1 2 3 4 5 7
2 3 4 5 7 8 9 6 1
5 7 8 9 6 1 2 3 4
9 6 1 2 3 4 5 7 8
由此可以至少生成8!= 40320种。同时456行之间可以互换,789行之间可以互换,23行之间可以互换(由于左上角数字被固定所以只能23行互换) ,同时456行和789行可以整体互换(即456行和789行同时换到对方的位置上)。而此时的总的种类已经达到了40320 * 2!* 3!×3!×2 = 5806080种(实际上只换456三行和789三行就已经满足要求了),超过了百万种。因此,此方法是可行的。
至于求解数独的问题,要求有如下几点:
- 输入格式为:-s读入文件的路径
- 要将结果输入到sudoku.txt中
- 输入的数独的空格用0表示
- 解出完整的数独
在求解这里还是纠结了一阵子,最后用的还是回溯法。
开始自己觉得回溯法可能不是最好的方法,于是上网查阅了相关的资料看到了网上有种很神奇的解法 —— 舞蹈链(Dancing Links)算法。但是看了一阵子感觉都没有太看明白,加之后来看了这位博主(http://www.cnblogs.com/grenet/p/3163550.html)做了大量实验之后得出的结论是:数独的舞蹈链算法(博主优化了多次的算法)和暴力破解法相比,在简单的数独问题上,时间和空间都不占优势,在高难度的数独问题上,数独的舞蹈链算法还是在时间上占有一点优势的博主最后还指出:舞蹈链算法本质上也是暴力破解法,只是利用巧妙的数据结构实现了高效的缓存和回溯结合博主的结论,由于害怕自己所剩的时间不足以实现,我最终没有使用舞蹈链算法,而是采用了比较直接的回溯法。
四、设计实现过程
按个人的想法,代码主要包括两个主要的函数:一个是负责生成终局的函数,一个是负责求解的代码此外还有一个主函数。由于个人对于面向对象的方法掌握的还不算很熟悉,最终没有设计类。
简单地做了一个功能建模图和数独求解的函数流程图。
当用户发出指令后,由主函数判断用户的意图(想生成终局或数独求解)如果是想生成终局,则进入生成终局函数;如果是想进行函数求解,则进入数独求解函数。
单元测试希望能够针对所有函数进行判定覆盖测试。(后续补充:学习单元测试着实花费了我一番精力,并且后来写完之后发现自己代码实在是太不规范,模块化程度也不高,不太好做单元测试。因此最后在进行单元测试的时候反而浪费了很多时间在优化代码结构上,这是很出乎我意料的,侧面也说明了使用面向对象的优越性,也让我意识到:结对项目的时候还是写类吧……)
最后对自己拆分的三个函数(不包括主函数)进行了单元测试,具体的单元测试代码已经提交至github上,代码较长这里便不单独展示了。
五、程序性能改进
开始的时候自己尝试运行了一下输出1000000个数独终局,结果发现耗时达到了23秒,其中输出部分占据了较大的比例。后来经过网上查找后发现如果将所有的终局全部存到一个大数组中,最后再将大数组一次性输出,效率会提高很多。经过改进后的程序输出1000000个数独终局所花费的时间不到2秒,程序中消耗最大的函数还是输出函数,不过所占比例降到了60%。性能分析图如下:
此外数独求解问题也在和同学讨论和查阅资料后采取了预剪枝的策略,用数组分别存放第几行、第几列以及从左上往右下数第几个九宫格内某个数是否出现过,出现过则将其置为1,没出现过则将其置为0。这样也极大地减少了搜索的次数,缩短了求解的时间。
六、代码说明
1.生成终局函数
采用对第一行进行全排列,然后通过变换位置生成后八行,同时进行与行之间的交换。
int sudokuproducer(int p)
{
while (next_permutation(sortline, sortline + 8))
{
for (int x = 0;x < 2;x++)
{
for (int y = 0;y < 6;y++)
{
for (int z = 0;z < 6;z++)//三重循环分别对应three1(前三行)、three2(中间三行)、three3(最后三行)
{
for (int i = 0;i < 3;i++) //生成前三行
{
store[cnt++] = sortline[(8 + three1[x][i]) % 9];
for (int j = 1;j < 17;j++) {
store[cnt++] = ' ';
j++;
store[cnt++] = sortline[((16 - j) / 2 + three1[x][i]) % 9];
}
store[cnt++] = '\n';
}
for (int i = 0;i < 3;i++) //生成中间三行
{
store[cnt++] = sortline[(8 + three2[y][i]) % 9];
for (int j = 1;j < 17;j++) {
store[cnt++] = ' ';
j++;
store[cnt++] = sortline[((16 - j) / 2 + three2[y][i]) % 9];
}
store[cnt++] = '\n';
}
for (int i = 0;i < 3;i++) //生成最后三行
{
store[cnt++] = sortline[(8 + three3[z][i]) % 9];
for (int j = 1;j < 17;j++) {
store[cnt++] = ' ';
j++;
store[cnt++] = sortline[((16 - j) / 2 + three3[z][i]) % 9];
}
store[cnt++] = '\n';
}
cmp++;
if (cmp == p)//满足数量
{
return 1;
}
else store[cnt++] = '\n';
}
}
}
}
return 0;
}
2.数独求解函数
前面已通过流程图详细解释过,这里便不赘述了。
int sudokusolver(int row, int column)
{
bool searchflag = false;
while (flagma[row][column] != '0')
{
if (column < 8) {
column++;
}
else {
column = 0;
row++;
}
if (row == 9) {
findflag = true;
return 1;
}
}
for (int i = 1;i <= 9;i++)
{
//如果这个数在行列九宫格中都没有被填过
if (judge[0][row][i] == 0 && judge[1][column][i] == 0 && judge[2][row / 3 * 3 + column / 3][i] == 0)
{
searchflag = true;
judge[0][row][i] = 1;
judge[1][column][i] = 1;
judge[2][row / 3 * 3 + column / 3][i] = 1;
flagma[row][column] = i + '0';
sudokusolver(row, column);//递归
}
if (searchflag == true)//是否在递归中
{
searchflag = false;
if (findflag == false)//回溯并重置当前值
{
judge[0][row][i] = 0;
judge[1][column][i] = 0;
judge[2][row / 3 * 3 + column / 3][i] = 0;
flagma[row][column] = '0';
}
else//已经找完所有空格
{
return 1;
}
}
}
return 0;
}
七、总结反思
从12月2号一直持续断断续续地做到现在,总算是差不多完工了。作为一个小白,这期间真的很辛苦,但是也真的学习了很多新的知识,最关键的是也将以前学过的很多理论知识运用到了实践之中。
由于自己在编程方面并没有很多的经历,刚上手的时候其实心里是十分没底的。但是当自己真正去做了才发现,这回的个人项目在编程方面其实并没有想象中那么难,虽然也在几个地方卡了壳(包括输入判别,输出以及回溯),但是总体来说还是比较流畅的完成了编码。期间查阅了很多资料,从中也受益颇多。真正浪费了较多时间的地方反而是单元测试和代码优化,那两天整个人因为花费了很多精力在这上面......不过这也说明了我在设计环节做的还是不够好,同时在编码规范上和规划代码结构上都存在着很大问题。所以以后还是用面向对象的方法好好写类吧,要不以后工作还这么差劲怕是很快就会被炒。
这回没有采用面向对象的方法去实现软件开发,还是只停留在面向过程,算是挺大的一个遗憾。此外数独求解最终没有采用Dancing Links算法,感觉自己也没做到精益求精。在今后的双人项目中,希望自己能够更进一步、越来越好。
大概是只有经历过痛苦才能成长吧,总之,这回的个人项目辛苦归辛苦,不过感觉自己的能力还是得到了一定的提升。虽然自己做的不够完美,但是能清楚地感受到自己还是在成长的,这种感觉还是挺不错的,也算是能理解老师留个人项目的用意吧。
八、实际用时
实际用时还是比计划用时长了不少,在代码规范和具体设计这两个环节做的不够完美,间接导致了我后续环节所消耗的时间大幅增加,实在是有些得不偿失。
PSP2.1 | 个人软件过程阶段 | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 60 | 60 |
Estimate | 估计这个任务需要多少时间 | 10 | 5 |
Development | 开发 |
|
|
Analysis | 需求分析(包括学习新技术) | 300 | 600 |
Design Spec | 生成设计文档 | 120 | 120 |
Design Review | 设计复审(和同事审核实际文档) | 30 | 10 |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 10 | 10 |
Design | 具体设计 | 60 | 120 |
Coding | 具体编码 | 900 | 960 |
Code Review | 代码复审 | 120 | 40 |
Test | 测试(自我测试,修改代码,提交修改) | 180 | 600 |
Reporting | 报告 | 180 | 240 |
Test Report | 测试报告 | 120 | 20 |
Size Measurment | 计算工作量 | 10 | 5 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 30 | 20 |
| 合计 | 2130 | 2810 |
最后附上几个对我帮助较大,内容是讲解相关基本操作的博客网址:
1.性能分析:https://www.cnblogs.com/aarond/archive/2013/04/19/performance-enhancement.html
2.单元测试:https://www.cnblogs.com/puddingcat/p/8620310.html
3.github桌面版:https://blog.csdn.net/java155/article/details/78723113