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的更多相关文章

  1. 17秋 软件工程 团队第五次作业 Alpha

    题目:团队作业--Alpha冲刺 17秋 软件工程 团队第五次作业 Alpha 12次Scrum 第一次Scrum 第二次Scrum 第三次Scrum 第四次Scrum 第五次Scrum 第六次Scr ...

  2. 17秋 软件工程 团队第五次作业 Alpha Scrum1

    题目:团队作业--Alpha冲刺 17秋 软件工程 团队第五次作业 Alpha Scrum1 各个成员在 Alpha 阶段认领的任务 伟航:督促和监督团队进度,协调组内合作 港晨:APP前端页面编写: ...

  3. 17秋 软件工程 团队第五次作业 Alpha Scrum2

    17秋 软件工程 团队第五次作业 Alpha Scrum2 今日完成的任务 杰麟:Java后端的学习: 世强:登录和注册接口编写: 港晨:完成数据库表的设计: 树民.陈翔:完成超级管理员后端框架. 其 ...

  4. 17秋 软件工程 团队第五次作业 Alpha Scrum3

    17秋 软件工程 团队第五次作业 Alpha Scrum3 今日完成的任务 杰麟:java后端学习: 世强:Android的部门基础信息模块的信息显示和对接后台: 港晨:后台管理登陆界面ui设计: 树 ...

  5. 17秋 软件工程 团队第五次作业 Alpha Scrum4

    17秋 软件工程 团队第五次作业 Alpha Scrum4 今日完成的任务 世强:部门基础信息模块数据更新.部门审核提交: 港晨:设计编写登录界面的一部分: 树民:学习python基本语法.flask ...

  6. 17秋 软件工程 团队第五次作业 Alpha Scrum5

    17秋 软件工程 团队第五次作业 Alpha Scrum5 今日完成的任务 世强:消息通知管理列表页界面编写,下拉加载效果: 港晨:编写登录界面: 树民: 伟航:学习了flask_restful框架的 ...

  7. 17秋 软件工程 团队第五次作业 Alpha Scrum6

    17秋 软件工程 团队第五次作业 Alpha Scrum6 今日完成的任务 世强:APP内通知消息发送; 港晨:APP前端登陆界面编写: 树民:Web后端数据库访问模块代码实现: 伟航:Web后端Re ...

  8. 17秋 软件工程 团队第五次作业 Alpha Scrum7

    17秋 软件工程 团队第五次作业 Alpha Scrum7 今日完成的任务 世强:部员详情列表的编写与数据交互,完善APP通知模块: 港晨:完成前端登陆界面编写: 树民:完善Web后端数据库访问模块: ...

  9. 17秋 软件工程 团队第五次作业 Alpha Scrum8

    17秋 软件工程 团队第五次作业 Alpha Scrum8 今日完成的任务 世强:部门人员管理界面设计编写: 港晨:设计主页面: 树民:web后端框架与前端对接: 伟航:app前端界面的美工: 陈翔: ...

随机推荐

  1. 以ActiveMQ为例JAVA消息中间件学习【1】

    前言 在慢慢的接触大型的javaweb的项目就会接触到很多的中间件系统. 其中消息中间件在很多场景下会被运用. 这里主要就对最近所学习到的消息中间件知识做一个笔记,为以后的实际运用打下一个良好的基础. ...

  2. A brief introduction to per-cpu variables

    墙外通道:http://thinkiii.blogspot.com/2014/05/a-brief-introduction-to-per-cpu.html per-cpu variables are ...

  3. 前端回顾:2016年 JavaScript 之星

    JavasScript社区在创新的道路上开足了马力,曾经流行过的也许一个月之后就过时了.2016已经结束了.你可能会想你是否错过一些重要的东西?不用担心,让我们来回顾2016年前端有哪些主流.通过比较 ...

  4. JavaWeb学习 (六)————Servlet(二)

    一.ServletConfig讲解 1.1.配置Servlet初始化参数 在Servlet的配置文件web.xml中,可以使用一个或多个<init-param>标签为servlet配置一些 ...

  5. POJ 1860 Currency Exchange(如何Bellman-Ford算法判断图中是否存在正环)

    题目链接: https://cn.vjudge.net/problem/POJ-1860 Several currency exchange points are working in our cit ...

  6. WebForm 【上传图片--添加水印】

      对图片添加水印,上传 <div> <asp:FileUpload ID="FileUpload1" runat="server" /> ...

  7. Git实战手册(三): stash解惑与妙用

    0. 介绍 教程所示图片使用的是 github 仓库图片,网速过慢的朋友请移步原文地址 有空就来看看个人技术小站, 我一直都在 在实际项目开发中,总会遇到代码写到一半(没法去打commit),去开启新 ...

  8. module.exports和exports.md

    推荐写法 具体解释可以往后看. 'use strict' let app = { // 注册全局对象 ... } ... // 封装工具箱 exports = module.exports = app ...

  9. JavaScript中的递归

    译者按: 程序员应该知道递归,但是你真的知道是怎么回事么? 原文: All About Recursion, PTC, TCO and STC in JavaScript 译者: Fundebug 为 ...

  10. Fundebug是这样备份数据的

    摘要: 数据还是要备份的,万一删库了呢? 本文代码仓库: fundebug-mongodb-backup 引言 今年8月,腾讯云竟然把客户前沿数据的数据弄没了,Fundebug在第一时间进行了一些简单 ...