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前端界面的美工: 陈翔: ...
随机推荐
- 以ActiveMQ为例JAVA消息中间件学习【1】
前言 在慢慢的接触大型的javaweb的项目就会接触到很多的中间件系统. 其中消息中间件在很多场景下会被运用. 这里主要就对最近所学习到的消息中间件知识做一个笔记,为以后的实际运用打下一个良好的基础. ...
- A brief introduction to per-cpu variables
墙外通道:http://thinkiii.blogspot.com/2014/05/a-brief-introduction-to-per-cpu.html per-cpu variables are ...
- 前端回顾:2016年 JavaScript 之星
JavasScript社区在创新的道路上开足了马力,曾经流行过的也许一个月之后就过时了.2016已经结束了.你可能会想你是否错过一些重要的东西?不用担心,让我们来回顾2016年前端有哪些主流.通过比较 ...
- JavaWeb学习 (六)————Servlet(二)
一.ServletConfig讲解 1.1.配置Servlet初始化参数 在Servlet的配置文件web.xml中,可以使用一个或多个<init-param>标签为servlet配置一些 ...
- POJ 1860 Currency Exchange(如何Bellman-Ford算法判断图中是否存在正环)
题目链接: https://cn.vjudge.net/problem/POJ-1860 Several currency exchange points are working in our cit ...
- WebForm 【上传图片--添加水印】
对图片添加水印,上传 <div> <asp:FileUpload ID="FileUpload1" runat="server" /> ...
- Git实战手册(三): stash解惑与妙用
0. 介绍 教程所示图片使用的是 github 仓库图片,网速过慢的朋友请移步原文地址 有空就来看看个人技术小站, 我一直都在 在实际项目开发中,总会遇到代码写到一半(没法去打commit),去开启新 ...
- module.exports和exports.md
推荐写法 具体解释可以往后看. 'use strict' let app = { // 注册全局对象 ... } ... // 封装工具箱 exports = module.exports = app ...
- JavaScript中的递归
译者按: 程序员应该知道递归,但是你真的知道是怎么回事么? 原文: All About Recursion, PTC, TCO and STC in JavaScript 译者: Fundebug 为 ...
- Fundebug是这样备份数据的
摘要: 数据还是要备份的,万一删库了呢? 本文代码仓库: fundebug-mongodb-backup 引言 今年8月,腾讯云竟然把客户前沿数据的数据弄没了,Fundebug在第一时间进行了一些简单 ...