个人项目-Sudoku

个人项目-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 ......
本来就以为这次作业耗时会比较长,看来预估是正确的(开始写的时候,犯了一个错误,导致战线拉长)。。

数独项目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 ......
本来就以为这次作业耗时会比较长,看来预估是正确的(开始写的时候,犯了一个错误,导致战线拉长)。。

猜你喜欢

转载自www.cnblogs.com/bitcyx/p/8909423.html