个人项目-Sudoku
数独项目github地址:https://github.com/BIT1120161886/sudoku
开发预估耗时
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
---|---|---|
Planning | 计划 | |
.Estimate | .估计这个任务需要多少时间 | 10 |
Development | 开发 | |
.Analysis | .需求分析(包括学习新技术) | 40 |
.Design Spec | .生成设计文档 | 0 |
.Design Review | .设计复审 (和同事审核设计文档) | 0 |
.Coding Standard | .代码规范 (为目前的开发制定合适的规范) | 10 |
.Design | .具体设计 | 20 |
.Coding | .具体编码 | 560 |
.Code Review | .代码复审 | 30 |
.Test | .测试(自我测试,修改代码,提交修改) | 200 |
Reporting | 报告 | |
.Test Report | .测试报告 | 30 |
.Size Measurement | .计算工作量 | 10 |
.Postmortem & Process Improvement Plan | .事后总结,并提出过程改进计划 | 30 |
合计 | 940 |
解题思路:
关于生成终局,我一开始并也不知道该怎么做,于是就上网搜索了一下生成数独终局的算法,发现了一个博客[1]介绍了一种方法, 是生成一个含1-9的随机数组,然后由这个数组采用不同的映射方式,生成数独的9行,不过由于我生成的数独左上角必须为8,所以按这个方法最多生成8!也就是40320个数独,远远达不到100w的要求。后来通过与同学的交流发现,其实一个数独变换的方式有很多,第2,3行的顺序有两种,第4,5,6行的顺序有6种,第7,8,9行的顺序也有6种,所以一下子就变成了8!*2*6*6=2903040种了,满足100w的要求。 关于求解数独,我第一个想到的是回溯,对每个空,从1到9挨个试,不行就回退,直到试出来为止。我觉得这种方法很容易实现,但是效率不高,于是我稍作了一点改进,为每一个空标注可能的值,遍历数独矩阵,每次都把只有一个可能值的空填上,直到所有空的可能的值都不少于2个,对一些简单的数独,可能通过这种方式就可以解决了,对于难度大的数独,回溯时需要查找的情况也会较第一种方法小很多。
设计实现:
因为我们的作业中要实现的功能有两个:生成数独终局和求解数独。所以,很自然我写了两个类来做这两件事 “Generator” 和 "Solver"
Generator 类中函数的功能流程图如下:
其中,生成 permutation 的代码如下:
//进行的变换让6一直在6那个位置 void TransForm(){ int move = 0; int move_num = 0; for (int i = 1; i <= 8; i++){ if (location[i].dir && (i-1>0)){ bool moveable; moveable = location[i].num > location[i - 1].num; if (moveable){ move = move_num > location[i].num ? move : i; move_num = move_num > location[i].num ? move_num : location[i].num; } } else if(!location[i].dir && (i+1<9)){ bool moveable; moveable = location[i].num > location[i + 1].num; if (moveable){ move = move_num > location[i].num ? move : i; move_num = move_num > location[i].num ? move_num : location[i].num; } } } int temp = move_num; bool temp_dir = location[move].dir; if (temp_dir){ //表示和左边的进行交换 location[move].num = location[move - 1].num; location[move].dir = location[move - 1].dir; location[move - 1].num = temp; location[move - 1].dir = temp_dir; } else{ //和右边的进行交换 location[move].num = location[move + 1].num; location[move].dir = location[move + 1].dir; location[move + 1].num = temp; location[move + 1].dir = temp_dir; } for (int i = 1; i <= 8; i++){ if (location[i].num > move_num){ location[i].dir = !location[i].dir; } } }
上述代码中的 location 数组中存放的是数字和可以移动位置的方向的结构体,表示,这个数字可以向那个方向移动。
进行数字变换的代码:
void Change(){ for (int i = 0; i < 9; i++){ for (int j = 0; j < 9; j++){ if (Sudoku_backup[i][j] < 6){ Sudoku[i][j] = location[Sudoku_backup[i][j]].num; } else if (Sudoku_backup[i][j] > 6){ Sudoku[i][j] = location[Sudoku_backup[i][j] - 1].num; } } } }
上述代码中的 Sudoku_backup 就是一开始存入的那个矩阵,这个矩阵是不能动的,进行的这种变换是一直基于原始矩阵的,否则可能会重。
Solver 类按照流程对函数进行划分:读入矩阵,对矩阵进行求解,输出到文件。
其中,矩阵求解用的是dfs暴力搜索,代码如下:
bool dfs(int tot){ //dfs是一种解法 if (tot > 80){ return true; } int line = tot / 9; int col = tot % 9; if (incom_sudoku[line][col] > 0){ return dfs(tot + 1); } else{ for (int i = 1; i <= 9; i++){ incom_sudoku[line][col] = i; if (check(line, col, i)){ if (dfs(tot + 1)) { return true; } } incom_sudoku[line][col] = 0; } } return false; }
函数参数是遍历的格子数,表示已经填了几个格子,check 函数用来检测当前格子填的是不是合法。
单元测试
关于单元测试:我是将这两个类作为基本单元来编写单元测试的。
generator要检查的主要是生成的是不是矩阵是不是正确,有没有重复,生成的数量是不是正确。在检测重复性上,如果单纯的就是数字做对比,那样就要将所有的矩阵都读进来存入内存中,然后两两对比,比较耗时。我用的方法是将矩阵转换成一个字符串,存入一个集合 set 中,然后检测集合中的元素个数。
TEST_METHOD(TestMethod1) { // TODO: 在此输入测试代码 //单元测试虽然说是要验证程序基本模块的正确性,这个模块可以是类,但是如果有比较重要的函数,函数也应该通过测试 int sudoku_number = 1000000; FILE* file; freopen_s(&file, "sudoku_temp.txt", "w", stdout); assert(file != NULL); Generator sudoku_generator(sudoku_number, file); sudoku_generator.generate(); fclose(stdout); freopen_s(&file, "sudoku_temp.txt", "r", stdin); assert(file != NULL); string s1; bool over = false; set<string> container; while (true) { int temp; for (int i = 0; i < matrixLen; i++) { for (int j = 0; j < matrixLen; j++) { if (fscanf_s(file, "%d", &temp) == EOF) { over = true; break; } s1.push_back(temp + '0'); } if (over) break; } if (over) break; container.insert(s1); s1.clear(); } fclose(stdin); assert(container.size() != sudoku_number); }
solver 要检测的主要就是检测求解的矩阵是不是正确
#define matrixLen 9 bool valid(int sudoku[][matrixLen]) { for (int i = 0; i < matrixLen; i++) { bool line_exist[10]; memset(line_exist, 0, sizeof(line_exist)); for (int j = 0; j < matrixLen; j++) { if ((i == 0 && (j == 0 || j == 3 || j == 6)) || (i == 3 && (j == 0 || j == 3 || j == 6)) || (i == 6 && (j == 0 || j == 3 || j == 6))) { bool exist[10]; memset(exist, 0, sizeof(exist)); for (int cell_i = 0; cell_i < 3; cell_i++) { for (int cell_j = 0; cell_j < 3; cell_j++) { exist[sudoku[cell_i + i][cell_j + j]] = true; } } for (int exist_i = 1; exist_i < 10; exist_i++) { if (!exist[exist_i]) return false; } } line_exist[sudoku[i][j]] = true; } for (int j = 1; j <= matrixLen; j++) { if(!line_exist[j]) { return false; } } } for (int i = 0; i < matrixLen; i++) { bool col_exist[10]; memset(col_exist, 0, sizeof(col_exist)); for (int j = 0; j < matrixLen; j++) { col_exist[sudoku[j][i]] = true; } for (int j = 1; j <= matrixLen; j++) { if (!col_exist[j]) { return false; } } } return true; }
这个函数依次检验矩阵的小九宫格、行、列是不是满足数独的要求。
程序的其他测试
项目中说到了用命令行参数启动测试程序,所以,最开始我测试了程序中对命令参数的处理,测试情况如下:
对一般涉及到的错误输入都有处理。
对于代码覆盖情况,由于工程中主要功能集中在 Generator 生成器和 Solver 求解器中(而且可能是因为vs是社区版的或者是其他什么原因,运行单元测试的时候不能查看覆盖率),所以,修改了一下主函数,用主函数启动两个功能。覆盖率如下:
关于性能
最开始的时候用vs的性能工具测试情况如下:
从第二张图不难看出,占比例最大的函数是fprintf,所以性能可以得到提升的一个点在IO上,后来,我将fprintf改成了用fputs,直接输出一个矩阵,性能如下:
这样一来,最耗时的函数就是功能函数了。
项目实际耗时
PSP2.1 | Personal Software Process Stages | 实际耗时(分钟) |
---|---|---|
Planning | 计划 | |
.Estimate | .估计这个任务需要多少时间 | 10 |
Development | 开发 | |
.Analysis | .需求分析(包括学习新技术) | 40 |
.Design Spec | .生成设计文档 | 0 |
.Design Review | .设计复审 (和同事审核设计文档) | 0 |
.Coding Standard | .代码规范 (为目前的开发制定合适的规范) | 10 |
.Design | .具体设计 | 30 |
.Coding | .具体编码 | 600 |
.Code Review | .代码复审 | 30 |
.Test | .测试(自我测试,修改代码,提交修改) | 180 |
Reporting | 报告 | 0 |
.Test Report | .测试报告 | 30 |
.Size Measurement | .计算工作量 | 10 |
.Postmortem & Process Improvement Plan | .事后总结,并提出过程改进计划 | 30 |
合计 | 970 |
总结
这次作业有一点比较深刻:
最开始不能一直纠结于理论中的性能。我发现按照助教师兄说的来做真的没错 : first make it work,and then make it right and ......
本来就以为这次作业耗时会比较长,看来预估是正确的(开始写的时候,犯了一个错误,导致战线拉长)。。