一、对各个模块所需的时间预计以及实际耗时
PSP2.1 | Personal Software Process Stages | 估计耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | ||
Estimate | 估计这个任务需要多久 | 30 | 30 |
Development | 开发 | ||
Analysis | 需求分析 | 30 | 60 |
Design Spe/c | 生成设计文档 | 40 | 40 |
Design Review | 设计复审 | 30 | 40 |
Coding Standard | 代码规范 | 20 | 30 |
Design | 具体设计 | 60 | 120 |
Coding | 具体编码 | 720 | 900 |
Code Review | 代码复审 | 30 | 40 |
Test | 测试 | 30 | 40 |
Reporting | 报告 | ||
Test Report | 测试报告 | 30 | 30 |
Size Measurement | 计算工作量 | 30 | 30 |
Postmortem & Process Improvement Plan |
事后总结,并提出过程改进计划 | 60 | 40 |
合计 | 1110 | 1400 |
二、思路描述
这个项目主要有两个问题:生成数独终局以及解数独
生成:
对于数独终局,我们首先知道,在9*9的方格内,每一行,每一列以及每一个九宫格都是由1到9组成,只要满足这个条件就是一个合法的数独终局。
总共的数独终局大约有6.67×10的21次方种,虽然项目只要求1e6种,但一开始还是没什么头绪,后来在网上查阅了相关资料以及大牛的博客后发现可以用全排列来做出该数量的终局,具体做法如下:
先从九宫格的第一行来考虑,第一行的可能种数为9!=362880,但因为第一位固定,所以只有8!=40320种。
在确定了第一行以后,余下八行都可以由第一行分别右移(或左移) 3、6、1、4、7、2、5、8位得到,示例如下
9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
3 | 2 | 1 | 9 | 8 | 7 | 6 | 5 | 4 |
6 | 5 | 4 | 3 | 2 | 1 | 9 | 8 | 7 |
1 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 |
4 | 3 | 2 | 1 | 9 | 8 | 7 | 6 | 5 |
7 | 6 | 5 | 4 | 3 | 2 | 1 | 9 | 8 |
2 | 1 | 9 | 8 | 7 | 6 | 5 | 4 | 3 |
5 | 4 | 3 | 2 | 1 | 9 | 8 | 7 | 6 |
8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 9 |
由每一个第一行都可以按这种方式生成一个终局。
进一步延伸,位移序列中的{3、6},{1,4,7},{2,5,8}都可在组内任意交换位置,即每一个头行都可产生2!*3!*3!种终局,所以这种方法总共可以产生的终局数为
8!*2!*3!*3!=2903040>1e6,满足要求。
求解:
对于求解,我们首先很容易想到暴力搜索这种可行的方法,但效率很低下。
在网上查找资料后发现针对这个问题其实有很多优秀的解法,我从中选择了一种最简单的。
这种方法可以称之为回溯法。大致思路就是,依次扫描每一个待填数字的空格:
1. 在第一个空格里面填上“1”,检查这个数字是否合法(其所在的行、列,以及3X3的子区域里不存在重复的数字)。如果合法,则前进到第二个格子。否则,在这个格子里继续试2,3,… ,直到合法为止。
2. 在第二个格子里面继续填数字,还是从“1”开始试起,直到找到一个合法的数字,继续进到下一格。
3. 如果在某个格子里,从“1”到“9”都不合法,这说明前面某个格子填错了。这时就回退到上一格,从还没试的数字里面继续试。例如,如果上一格已填的数字是3,就继续试4,5,6,… 是否合法。如果找到一个合法的数字,则又前进到下一格。如果找不到,说明前面还有格子也填错了,则继续回退到更前面一格,… 如此反复。
4. 如果这个数独是有解的,我们总会遇到“每个格子都碰巧填对了”的情况。如果这个数独是没有解的,那么我们会遇到“第一个格子试了1~9所有的数字都不行”的情况。
三、设计实现过程
代码组织过程我主要将其分为解数独和生成终局两部分,这两部分在不同的命令行输入时分别起作用。
生成部分由两个函数实现其功能,分别是create(),recursion()。其执行过程是在main()函数根据命令行输入判断为合法生成数独终局输入时,调用create(),create()函数在内部再调用recursion()通过递归求出头行,然后create()再根据所求出的头行以及要求的生成数目来生成终局。
解数独部分由两个函数实现其功能,分别是solve(),solve2()。其执行过程是在main()函数根据命令行输入判断为合法解数独输入时,调用solve(),solve()的作用是先从目标文件处读取数独题目,然后调用solve2()来通过回溯法实现求解,再在solve()内将题解输出到目标文件。
直观流程如下:
单元测试:
对于生成数独部分的测试主要在性能上,在确定了程序输出的数独终局的正确性后,我分别对与1到1e6之间的输入进行了时间消耗的检测,输入为1e6时消耗约为17s,还有很大改进空间。
对于解数独部分的测试分别在性能与正确性上。在生成的终局里随机抽取一部分并设置一些空白格后作为数独题目以检测其正确性。性能测试上,解1000组输入耗时约0.1s。
四、改进过程
对于代码的改进主要分为两个部分,算法部分:在生成终局的函数中主要的耗时就是在于产生全排列,后来我了解到可以使用C++自带的函数std::next_permutation(, )来实现,可以让速度就有所提升。在解数独部分主要时间就是消耗在回溯的时候对于每个数字的判重上,减掉一些不必要的判断后时间应该会减少一些。还有一个部分就是对于数据的读入与输出部分,在各种测试后发现使用fputc以及fgetc比较快。
性能分析:
对生成终局部分代码的分析:在输入为 -c 10000时
发现消耗最大的函数是create(),但是fputc的消耗比它还大,说明生成部分的瓶颈在于数据的输出。
对于解数独部分的分析:在输入为 -s 以及1000组数独题目的地址时
发现这个函数中消耗最大的是solve2(),虽然fputc和fgetc部分也有不少消耗,但问题的瓶颈还是在对于数独的回溯求解上
五、代码说明
生成终局部分的关键代码,方法主要就是通过调用recursion()来得到头行的所以排列。接下来依次对每一个头行按不同的位移序列生成终局。
1 void create() 2 { 3 recursion(2);//生成头行 4 FILE* fp = fopen("sudoku.txt", "w"); 5 for (int m = 0; m < 6; m++) 6 { 7 if (m) 8 std::next_permutation(shift + 3, shift + 6); 9 for (int h = 0; h < 6; h++) 10 {//两个for循环确定一种位移方式 11 if (h) 12 std::next_permutation(shift + 6, shift + 9); 13 for (int i = 0; i < maxlong; i++) 14 {//选定头行 15 for (int s = 0; s < 9; s++) 16 { 17 for (int j = 0; j <= 8; j++) 18 { 19 if (j) 20 fputc(' ', fp); 21 c = '0' + row[i][(j + shift[s]) % 9 + 1];//通过s和j来确定位移后的的位置 22 fputc(c, fp); 23 } 24 if (s < 8) 25 fputc(' ', fp); 26 } 27 sum++; 28 if (sum == n) 29 { 30 fclose(fp); 31 return; 32 } 33 fputc(' ', fp); 34 fputc(' ', fp); 35 } 36 } 37 } 38 }
解数独部分的关键代码,方法就是回溯的判断当前空格处能填入的数字,不能进行下去时就退回上一步。直到遇见每一个空格都能填入数字的时候就结束。
void solve2(int i,int j)//i,j表示当前到达的位置 { if (i == 9 && j == 0) { flag = 1; return; } if (sodu[i][j] != 0)//已经有数字的时候直接进入下一个格子 { anssodu[i][j] = sodu[i][j]; if (j < 8) solve2(i, j + 1); else solve2(i + 1, 0); if (flag) return; } else { for (int k = 1,x,y; k <= 9; k++) { for (x = 0; x <= 8; x++) if (sodu[x][j] == k) break;//对这一列进行去重判断 for (y = 0; y <= 8; y++) if (sodu[i][y] == k) break;//对这一行进行去重判断 if (y == 9 && x == 9) { for (x = (i / 3) * 3; x <= (i / 3) * 3 + 2; x++) { for (y = (j / 3) * 3; y <= (j / 3) * 3 + 2; y++) { if (sodu[x][y] == k) y = x = 100;//在当前所处的3*3区域进行判断 } } if (x == (i / 3) * 3 + 3 && y == (j / 3) * 3 + 3) { sodu[i][j] = k; anssodu[i][j] = sodu[i][j]; if (j < 8) solve2(i, j + 1); else solve2(i + 1, 0);//在当前空格处填入k后,进入下一空格 if (flag) return; sodu[i][j] = 0; } } } } }
六、实际用时记录
见第二部分。