数独项目github地址:https://github.com/Issac-Newton/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到9这九个数字不重复填充。首先想到的就是暴力解法,在一个空的矩阵中填入随机数,之后往旁边能够填的位置上继续随机填入能够填入的数字。但是,构成数独的规则比较复杂,填完当前位置之后往下个位置填的时候填入的数字很可能不满足之前的数字或者是这个空格没有可以填的数字(最开始做得时候没有想到这点,一开始就暴力,结果GG),所以,还要加入回溯。(复杂度)。 后来,在网上搜索“生成数独终局的算法”,看到这样一个帖子 矩阵转换法生成数独,就想用一个现存的矩阵通过变换来生成新的矩阵。后来跟舍友一起讨论,感觉可以用一种映射来对数字进行位置转换。不如就用排列数来对矩阵中的最开始数字进行映射,比如:
第一行的数字 【6,7,8,9,1,2,3,4,5】 ,生成的数字排列为 【1,7,9,8,2,3,5,4】 (因为最开始的数字6是不能动的,所以生成的排列没有6),可以看到:生成的数字排列中第二个位置是7,所以,第一行的数字2就变成7,如果矩阵中数字比第一个数字大,就找比它小1的位置(7-->3)。然而,这样生成最多也才是8!= 40320组,所以,那篇帖子中的方法就派上了用场,行变换就可以解决这个问题。
另外,因为还要考虑到不能由重复的矩阵,有两点就需要注意一下:这种映射只能作用于最开始的矩阵;进行行变换的时候不能动第一行(同时也是为了保证首数字不变)。
设计实现:
因为我们的作业中要实现的功能有两个:生成数独终局和求解数独。所以,很自然我写了两个类来做这两件事 “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 ......
本来就以为这次作业耗时会比较长,看来预估是正确的(开始写的时候,犯了一个错误,导致战线拉长)。。