17秋 软件工程 第二次作业 sudoku

2017年秋季 软件工程 作业2:个人项目 sudoku

Github Project

Github Project at Wasdns/sudoku.

PSP Table

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

解题思路

需求:

  • 1.利用程序随机构造出N个已解答的数独棋盘;
  • 2.在生成数独矩阵时,左上角的第一个数为:(学号后两位相加)% 9 + 1;

上述需求可以分解为以下几点:

  • 1.构造出合法的数独解,即满足 行/列/格 的约束条件;
  • 2.根据用户的输入进行IO处理;
  • 3.数独矩阵第一个数为(0+9)%9+1=1。

那么很容易就想到以下的基本求解方法:

1.初始化一个数独棋盘(以二维数组表示),初始位置为sudoku[0][0],填入1,进入步骤2;

2.遍历下一个格子,进入步骤3;

3.在满足依赖条件的前提下选出所有可能的数字,如果有进入步骤4,没有返回步骤1;

4.随机选取一个满足条件的数字,填入该空格,进入步骤5;

5.如果该空格是最后一个空格,则终止程序返回数独解;如果不是最后一个空格,返回步骤2。

实现也很简单(brach: legacy),但是很快就发现了问题:上述求解过程中,步骤3的情况是很容易出现的(15格=>20格之间),即遍历到某一个空格时,发现1-9所有的数字都不满足数独解的合法约束(行/列/格)。在上述的算法中,很简单的就进行了处理(重新开始),但是计算出单个解的时间是无法估计的。

于是发现了一个规律:当遍历至一个单元格时,如果发现无解,则说明前一个单元格所选择的数字不符合要求。那么只要重新回到上一步,将上一次选择的数字标记为非法,再进行选择试探即可。那么最终生成的算法为:

1.初始化一个数独棋盘(以二维数组表示),以及一个用于记录当前单元格所有潜在数字的缓存数组,初始位置为sudoku[0][0],填入1,进入步骤2;

2.遍历下一个格子,进入步骤3;

3.在满足依赖条件的前提下选出所有可能的数字,如果有进入步骤4;没有,清空当前单元格的缓存数组,将上一个单元格选择的数字从上一个单元格的缓存数组中剔除,返回步骤2;

4.随机选取一个满足条件的数字,填入该空格,进入步骤5;

5.如果该空格是最后一个空格,则终止程序返回数独解;如果不是最后一个空格,返回步骤2。

设计实现

最开始时,确定了完成该项目所需要的类:

  • SudokuJudger: 用于测试生成的数独解是否合法(满足行/列/格的约束限制);
  • SudokuGenerator: 用于生成数独的解;
  • SudokuIOer: 用于接收来自用户的输入(命令行形式,解析参数);
  • SudokuExceptionInspector: 用于异常处理,如输入非法字符;
  • SudokuPrinter: 打印数独解。

其中,SudokuJudger包含以下方法:

bool SudokuisSolved(int input[9][9]);

说明:将数独解作为输入,判断是否是正确解答,返回True|False。

SudokuGenerator包含以下数据结构:

int solution[9][9];

说明:用于存放数独解。

SudokuGenerator包含以下方法:

int generateNumber(int inputAvailable[10], int index);

说明:将当前单元格的缓存数组、当前单元格的格号(或者说相对(0, 0)的偏移量)作为输入,根据程序依赖条件得出所有潜在数字,若不存在返回-1,若存在则基于随机数选举出一个数字,并返回。

void increaseRandomSeed();

说明:修改随机数种子,保证随机性。

bool Generator();

说明:生成合理数独解,如果正常执行,将解存放在solution[9][9]中,返回True;若发生异常,则返回False。

SudokuIOer包括以下方法:

void outputFile(int solution[9][9], ofstream& sudokuFile);

说明:输入数独的解、文件流,将数独的解输出到该文件流中。

SudokuExceptionInspector包括以下方法:

bool isNumber(char number[]);

说明:将用户输入作为该函数的输入,判断输入的数字的每一位是否在0-9之间,返回True|False。

int parser(char number[]) throw(ParserException);

说明:将用户输入作为该函数的输入,判断输入合法性,若非法抛出异常,若合法返回对应的整数值。

SudokuPrinter包括以下方法:

void Printer(int solution[9][9]);

说明:将输入的数独解打印出来。

依赖关系:

表述为:方法名 => 被依赖方法名。

Class SudokuGenerator:

  • Generator => generateNumber;
  • Generator => increaseRandomSeed;

Class SudokuExceptionInspector:

  • parser => isNumber.

关键代码说明

函数main():

int main(int argc, char *argv[]) {
    // 判断用户输入参数个数,若小于3则报错
    if (argc < 3) {
        cout << "Error occurs when parsing arguments." << endl;
        cout << "Usage: sudoku.exe -c [N: a number]" << endl;
        return 1;
    }
    
    // 解析用户输入的参数,判断是否输入异常,出现异常进行异常处理
    int solutionNumber;
    SudokuExceptionInspector sudokuExceptionInspector;
    try {
        solutionNumber = sudokuExceptionInspector.parser(argv[2]);
    } catch(ParserException) {
        cout << "Error occurs when parsing arguments." << endl;
        cout << "Usage: sudoku.exe -c [N: a number]" << endl;
        cout << "Please check your input number." << endl;
        return 1;
    }

    SudokuGenerator sudokuGenerator;
    SudokuIOer sudokuIOer;

    // 打开文件 sudoku.txt
    ofstream sudokuFile("sudoku.txt", ios::out | ios::ate);

    // 求解N个数独解,并将其输入到sudoku.txt中
    bool signal = false;
    for (int i = 0; i < solutionNumber; i++) {
        signal = sudokuGenerator.Generator();
        if (signal) {
            sudokuGenerator.increaseRandomSeed();
            sudokuIOer.outputFile(sudokuGenerator.solution, sudokuFile);
        } else {
            cout << "Error occurs when applying sudokuGenerator." << endl;
            return 1;
        }
    }

    // 关闭文件 sudoku.txt
    sudokuFile.close();

    return 0;
}

函数Generator():

bool SudokuGenerator::Generator() {
    // 初始化数独解棋盘
    memset(solution, 0, sizeof(solution));
    solution[0][0] = (0+9)%9+1;

    // 判断当前单元格的合法数字
    // available[格位置][数字] = 0: 数字合法;
    // available[格位置][数字] = 1: 数字非法;
    int available[82][10];
    memset(available, 0, sizeof(available));

    // 记录先前单元格选择的数字
    int traverseRecorder[82];
    memset(traverseRecorder, 0, sizeof(traverseRecorder));

    traverseRecorder[0] = 1;
    
    // 指向当前单元格的指针
    int currentPlacePointer = 1;

    int i = 1;

    // 遍历所有81个单元格
    while (i < 81) {
        // 生成当前单元格的数字
        int getNumber = generateNumber(available[i], i);

        // 如果当前单元格出现无解的情况
        if (getNumber == -1) {
            // 指向当前单元格的指针回退到上一个单元格
            currentPlacePointer--;

            // 将上一次选择的数字在缓存数组中标记为非法
            int lastChosenNumber = traverseRecorder[currentPlacePointer];
            available[currentPlacePointer][lastChosenNumber] = 1;

            // 在记录先前选择数字的数组中,清除上一个单元格选择的数字
            traverseRecorder[currentPlacePointer] = 0;
            
            i--; // 回到上一个单元格

            // 在棋盘中,清除上一个单元格选择的数字
            int lineIndex = i/9, columnIndex = i%9; 
            solution[lineIndex][columnIndex] = 0;

            // 将出现无解的单元格处的缓存数组清空
            memset(available[currentPlacePointer+1], 0, sizeof(available[currentPlacePointer+1]));
        } else {
            // 有解,将生成的数字更新到解决方案中,进入下一个单元格
            int lineIndex = i/9, columnIndex = i%9; 
            solution[lineIndex][columnIndex] = getNumber;
            i++;

            // 更新指向当前单元格的指针,和记录先前选择数字的数组
            traverseRecorder[currentPlacePointer] = getNumber;
            currentPlacePointer++;
        }
    }

    return true;
}

测试运行

代码执行时间测试:

1.使用命令:

$ time ./main -c 10/100/1000/10000/100000

2.执行时间:

(1) 10:
real    0m0.032s
user    0m0.010s
sys 0m0.004s

(2) 100:
real    0m0.055s
user    0m0.048s
sys 0m0.004s

(3) 1000:
real    0m0.424s
user    0m0.405s
sys 0m0.017s

(4) 10000:
real    0m4.504s
user    0m4.310s
sys 0m0.169s

(5) 100000:
real    0m48.359s
user    0m46.014s
sys 0m2.029s

代码覆盖率测试(Linux下使用gcov):here

输入检测:

正确运行结果(部分):

项目改进

在Windows环境下做测试时,发现程序的花费时间非常长,与Linux环境下的测试结果不相符。在检查之后发现,原有程序中,IO是在sudokuIOer类中的outputFile函数的for循环里面完成的,在原有main函数中调用了N次outputFile函数,因此造成了极大的overhead。

原有程序:

    SudokuGenerator sudokuGenerator;
    SudokuIOer sudokuIOer;

    bool signal = false;

    for (int i = 0; i < solutionNumber; i++) {
        signal = sudokuGenerator.Generator();
        if (signal) {
            sudokuGenerator.increaseRandomSeed();
            // 在程序中执行IO,造成了极大的性能损耗
            sudokuIOer.outputFile(sudokuGenerator.solution, "sudoku.txt"); 
        } else {
            cout << "Error occurs when applying sudokuGenerator." << endl;
            return 1;
        }
}

改进方法是将文件IO放在main函数中处理,往outputFile方法中传入文件流,将原有的N次IO处理缩减为1次,极大缩短了程序的运行时间。

改进后程序:

    SudokuGenerator sudokuGenerator;
    SudokuIOer sudokuIOer;
    // SudokuPrinter sudokuPrinter;

    // 在main函数中打开文件
    ofstream sudokuFile("sudoku.txt", ios::out | ios::ate);

    bool signal = false;

    for (int i = 0; i < solutionNumber; i++) {
        signal = sudokuGenerator.Generator();
        if (signal) {
            sudokuGenerator.increaseRandomSeed();
            // 传入文件流,将结果输出到文件
            sudokuIOer.outputFile(sudokuGenerator.solution, sudokuFile);
        } else {
            cout << "Error occurs when applying sudokuGenerator." << endl;
            return 1;
        }
    }

    // 关闭文件
    sudokuFile.close();

2017.9

posted @ 2017-09-06 21:28 Wasdns 阅读(...) 评论(...) 编辑 收藏