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