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
17秋 软件工程 第二次作业 sudoku的更多相关文章
- 17秋 软件工程 团队第五次作业 Alpha
题目:团队作业--Alpha冲刺 17秋 软件工程 团队第五次作业 Alpha 12次Scrum 第一次Scrum 第二次Scrum 第三次Scrum 第四次Scrum 第五次Scrum 第六次Scr ...
- 17秋 软件工程 团队第五次作业 Alpha Scrum1
题目:团队作业--Alpha冲刺 17秋 软件工程 团队第五次作业 Alpha Scrum1 各个成员在 Alpha 阶段认领的任务 伟航:督促和监督团队进度,协调组内合作 港晨:APP前端页面编写: ...
- 17秋 软件工程 团队第五次作业 Alpha Scrum2
17秋 软件工程 团队第五次作业 Alpha Scrum2 今日完成的任务 杰麟:Java后端的学习: 世强:登录和注册接口编写: 港晨:完成数据库表的设计: 树民.陈翔:完成超级管理员后端框架. 其 ...
- 17秋 软件工程 团队第五次作业 Alpha Scrum3
17秋 软件工程 团队第五次作业 Alpha Scrum3 今日完成的任务 杰麟:java后端学习: 世强:Android的部门基础信息模块的信息显示和对接后台: 港晨:后台管理登陆界面ui设计: 树 ...
- 17秋 软件工程 团队第五次作业 Alpha Scrum4
17秋 软件工程 团队第五次作业 Alpha Scrum4 今日完成的任务 世强:部门基础信息模块数据更新.部门审核提交: 港晨:设计编写登录界面的一部分: 树民:学习python基本语法.flask ...
- 17秋 软件工程 团队第五次作业 Alpha Scrum5
17秋 软件工程 团队第五次作业 Alpha Scrum5 今日完成的任务 世强:消息通知管理列表页界面编写,下拉加载效果: 港晨:编写登录界面: 树民: 伟航:学习了flask_restful框架的 ...
- 17秋 软件工程 团队第五次作业 Alpha Scrum6
17秋 软件工程 团队第五次作业 Alpha Scrum6 今日完成的任务 世强:APP内通知消息发送; 港晨:APP前端登陆界面编写: 树民:Web后端数据库访问模块代码实现: 伟航:Web后端Re ...
- 17秋 软件工程 团队第五次作业 Alpha Scrum7
17秋 软件工程 团队第五次作业 Alpha Scrum7 今日完成的任务 世强:部员详情列表的编写与数据交互,完善APP通知模块: 港晨:完成前端登陆界面编写: 树民:完善Web后端数据库访问模块: ...
- 17秋 软件工程 团队第五次作业 Alpha Scrum8
17秋 软件工程 团队第五次作业 Alpha Scrum8 今日完成的任务 世强:部门人员管理界面设计编写: 港晨:设计主页面: 树民:web后端框架与前端对接: 伟航:app前端界面的美工: 陈翔: ...
随机推荐
- SQL 必知必会·笔记<15>创建和操纵表
创建表的两种办法: 使用DBMS 提供的交互式创建和管理数据库表的工具: 直接用SQL 语句创建. 表创建基础 创建表示例: CREATE TABLE Products ( prod_id ) NOT ...
- Java可以像Python一样方便爬去世间万物
前言: 之前在大二的时候,接触到了Python语言,主要是接触Python爬虫那一块 比如我们常用的requests,re,beautifulsoup库等等 当时为了清理数据和效率,还专门学了正则表达 ...
- [EZOJ1007] 神奇的三角形
Description 求 \(\sum\limits_{i=0}^{n-1}\sum\limits_{j=0}^{i}C(i,j)\times (j+1)^m\operatorname{mod}99 ...
- JQuery遍历,find()和each()方法
find()方法 jquery选择器非常强大,利用css的命名规约,可以更快更方便的找出想要的元素. 比如: $("#id") $("#"+"id&q ...
- soapUI工具使用方法、简介、接口测试
一.soapUI简介 SOAP: WebService通过Http协议发送请求和接收结果时,发送的请求内容和结果内容都采用XML格式封装,并增加了一些特定的HTTP消息头,以说明HTTP消息头的内容格 ...
- c#连接访问数据库(菜鸡篇)
C#如何访问数据库(小白篇) 刚入坑不久学习的路上还是遇到了不小的问题,昨天学习C#的时候需要连接数据库获取数据. 网上有很多这样的文章,说实话对于我这样的小白还真是有点难理解,经过一番周折总算是了解 ...
- angularjs学习第五天笔记(第二篇:表单验证升级篇)
您好,我是一名后端开发工程师,由于工作需要,现在系统的从0开始学习前端js框架之angular,每天把学习的一些心得分享出来,如果有什么说的不对的地方,请多多指正,多多包涵我这个前端菜鸟,欢迎大家的点 ...
- MEF 插件式开发之 小试牛刀
MEF 简介 Managed Extensibility Framework 即 MEF 是用于创建轻量.可扩展应用程序的库. 它让应用程序开发人员得以发现和使用扩展且无需配置. 它还让扩展开发人员得 ...
- 本地navicate for mysql怎么修改密码?
1.以前在本地设置sql库密码,就是在本地新建数据库的时候就输入,怎么也链接不上,原来是新建数据库的时候不能输入密码,需要在内部修改. 2. 打开mysql user表 3. 打开mysql user ...
- 10个最佳 Javascript+HTML5 演示文稿框架
JavaScript 与 HTML5 框架在创建基于现代浏览器的演示文稿时发挥了重要作用.他们把展示插入网页,为演示信息提供了一个有效方式.一般来说,手工插入花费大量事件和精力,它很复杂,以至于新手们 ...