个人项目-数独

1.项目的Github地址

  https://github.com/crvz6182/sudoku

2.开发预估耗时:

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 10  
· Estimate · 估计这个任务需要多少时间 10  
Development 开发 890  
· Analysis · 需求分析 (包括学习新技术) 30  
· Design Spec · 生成设计文档 5  
· Design Review · 设计复审 (和同事审核设计文档) 0  
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 5  
· Design · 具体设计 20  
· Coding · 具体编码 500  
· Code Review · 代码复审 30  
· Test · 测试(自我测试,修改代码,提交修改) 300  
Reporting 报告 95  
· Test Report · 测试报告 60  
· Size Measurement · 计算工作量 5  
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30  
  合计 995  

3.解题思路

  这次的项目要求既能生成不重复的数独又能解数独。

  生成数独算法的灵感来源于《编程之美》中有关数独的一部分论述,书中提到可以通过生成好的3x3矩阵扩展成合法的9x9数独。由于不能重复,随机生成再查重要消耗相当多的时间,而且当时也不知道其他顺序生成的方法,就觉得这个主意不错。

  书中的算法虽然不能满足1000000个的需求,但是给出了如何增加变化的提示。可以通过部分行列的对换生成更多种类的数独。在这次题目限制的左上角数字不变的情况下,只要确定了一个3x3矩阵,就可以生成2*6*6*2*6*6=5184个数独。3x3矩阵可以视为生成数独的一个“种子”。接下来就要确保不同的种子生成的数独中没有重复了。

  种子和最后生成的数独中的每个3x3区域都是一一对应的。将最后生成的数独中左上角的3x3区域中的格子编号:(左上角固定为6)

6 X Y
x a b
y c d

  由于6不能变换位置,因此行列变换只会涉及第2,3行和2,3列。剩下的数字只能从1,2,3,4,5,7,8,9里选。

  以如下顺序填充这个区域:先取一组数X,Y,再取一组数x,y,然后把剩下的数字由大到小填入abcd。只要两个数独中X,Y,x,y这四个位置的数字不完全相同就不会重复。假如数字相同但位置不同,进行行列变换后虽然可以使位置相同,但会影响abcd四个数的位置,因此还是不同情况。在这个规则下生成的种子可以有8*7*6*5=1680个。总共可以生成1680*5184=8709120个,满足1000000个的需求。

  解数独采用了比较简单的回溯法,依次往未填入数字的格子中填数,如果无法继续进行则回溯到上一次填数的位置。直到全部填完,或是回溯到开头后发现无解。

4.设计思路

  没有使用类,在main函数中判断要解还是生成,以及生成种子。

  关于生成的函数:

    void transform(int sudoku[][9], int x, int y, int X, int Y, int* others, int num, char *result, int &r_tag)

      用于将种子扩展为数独并进行变换,变换后结果输出到result

  关于求解的函数:

    bool in_area(int sudoku[][9], int x, int y, int i)

      判断某个数是否已经在一个3x3区域内

    void solve(int sudoku_s[][9], char* result, int &r_tag)

      求解一个数独并将结果输出到result

  调用关系:

    main调用solve,transform

    solve调用in_area

  关于单元测试:

    单元测试针对上述三个函数,给定不同输入情况并判断是否正常输出

    (教程中给的单元测试操作方法没有生成.exe,但是覆盖率分析插件只能针对.exe,我尝试了很久也没能一起使用)

    

5.程序改进

   这次的项目可以说有一半的时间都花在了改进上。改进过程中没有改动算法,主要是针对细节问题进行优化。

      在第一版完成后,生成的运行速度特别慢。后来通过性能分析发现字符串的“+=”拼接操作占用了大量时间,于是改用字符数组存储最后结果,性能得到了显著提升。在自己的电脑上测试时生成1000000个的所需时间从一分多钟降到了6s左右。

   在求解算法中我一开始使用多位数保存所有可能选择,性能分析后发现由于需要多次除法和取余数运算导致效率很低,后来改用了数组进行保存,用时减少了近6/7。

   

  时间还是主要花在对字符的操作上

6.代码展示

for (x = 1; x <= 7; x++)
            for (y = x + 1; y <= 8; y++)
                for (X = 1; X <= 7; X++)
                    for (Y = X + 1; Y <= 8; Y++)
                        if (X != x&&X != y&&Y != x&&Y != y) {
                            for (int i = 1; i < 9; i++) {
                                if (X != i&&x != i&&Y != i&&y != i) {
                                    if (i == 6)
                                        others[j++] = 9;
                                    else
                                        others[j++] = i;
                                }
                                if (j == 4) {
                                    j = 0;
                                    break;
                                }
                            }
                            if (X == 6)X = 9; if (Y == 6)Y = 9; if (x == 6)x = 9; if (y == 6)y = 9;

遍历所有种子,用x,y,X,Y保存关键区分元素,others数组保存其他元素

for (x1 = 0; x1 < 2; x1++) {
        for (x2 = 0; x2 < 6; x2++) {
            for (x3 = 0; x3 < 6; x3++) {
                for (y1 = 0; y1 < 2; y1++) {
                    for (y2 = 0; y2 < 6; y2++) {
                        for (y3 = 0; y3 < 6; y3++) {
                                                ......
                                                }

遍历所有行列变换的情况,循环内部为根据相应情况进行变换

int x = 0, y = 0, i = 0, j = 0, m = 0, n = 0;
    for (int p = 0; p < 9; p++)
        for (int q = 0; q < 9; q++) {
            if (sudoku[p][q] == 0)
                list_tag[p][q] = -1;
            else
                list_tag[p][q] = -2;
            for (int r = 0; r < 9; r++) {
                list[p][q][r] = 0;
            }
        }
    while (x != 9 && y != -1) {//遍历
        if (0 <= x&&x <= 8 && 0 <= y&&y <= 8 && sudoku[x][y] == 0) {
            for (i = 1; i <= 9; i++) {
                if (in_area(sudoku, x, y, i))continue;
                for (j = 0; j < 9; j++) {
                    if (sudoku[x][j] == i) break;
                    if (sudoku[j][y] == i) break;
                }
                if (j != 9)continue;
                list[x][y][++list_tag[x][y]] = i;
            }
            if (list_tag[x][y] == -1) {
                st = 1;
                y--;
                if (y == -1) {
                    x--; y = 8;
                }
            }
            else {
                sudoku[x][y] = list[x][y][list_tag[x][y]];
                st = 0;
                y++;
                if (y == 9) {
                    x++; y = 0;
                }
            }
        }
        else {
            if (list_tag[x][y] == -2) {
                switch (st) {
                case 0:
                    y++;
                    if (y == 9) {
                        x++; y = 0;
                    }break;
                case 1:
                    y--;
                    if (y == -1) {
                        x--; y = 8;
                    }
                    break;
                }
            }
            else {
                if (list_tag[x][y] == 0) {
                    sudoku[x][y] = 0;
                    list[x][y][0] = 0;
                    list_tag[x][y] = -1;
                    st = 1;
                    y--;
                    if (y == -1) {
                        x--; y = 8;
                    }
                }
                else {
                    list[x][y][list_tag[x][y]] = 0;
                    sudoku[x][y] = list[x][y][--list_tag[x][y]];
                    st = 0;
                    y++;
                    if (y == 9) {
                        x++; y = 0;
                    }
                }
            }
        }
        if (y == -1) {
            cout << "无解\n"; exit(1);
        }
    }
    for (int i = 0; i < 9; i++) {
        for (int j = 0; j < 9; j++) {
            result[r_tag++] = char(sudoku[i][j] + '0');
            if (j == 8)
                result[r_tag++] = '\n';
            else
                result[r_tag++] = ' ';
        }
    }
    result[r_tag++] = '\n';

求解数独时的回溯过程,用数组保存每个位置可以填的所有数字

7.完成后的PSP

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 10  10
· Estimate · 估计这个任务需要多少时间 10  20
Development 开发 890  1400
· Analysis · 需求分析 (包括学习新技术) 30  80
· Design Spec · 生成设计文档 5  5
· Design Review · 设计复审 (和同事审核设计文档) 0  0
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 5  5
· Design · 具体设计 20  20
· Coding · 具体编码 500  500
· Code Review · 代码复审 30  30
· Test · 测试(自我测试,修改代码,提交修改) 300  600
Reporting 报告 95  120
· Test Report · 测试报告 60  100
· Size Measurement · 计算工作量 5  10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30  10
  合计 995  1470

  对我来说,这次作业的压力不亚于当初让我忙了好几天的OO出租车作业。要想在时限内尽量完美的完成任务对我来说很难,光是在Debug上我就花了好几个小时,这几天几乎一直都在做相关的事情。所以我最后没有写附加题,没有用DLX算法,编码质量也很差……就个人能力上来说我还很弱,可能选择这门课的目的就是为了锻炼一下自己吧……

posted @ 2017-09-26 00:37 crvz6182 阅读(...) 评论(...) 编辑 收藏