2017BUAA软工个人项目之数独生成与求解
1.项目GitHub地址:https://github.com/ZiJiaW/Soduko
(由于一开始把sudoku看成了soduko,于是名字建错了,读起来可能有点奇怪…)
2.项目PSP表格如下:
| 
 PSP2.1  | 
 Personal Software Process Stages  | 
 预估耗时  | 
 实际耗时  | 
| 
 Planning  | 
 计划  | 
 0.5h  | 
 0.5h  | 
| 
 .Estimate  | 
 .估计这个任务需要多少时间  | 
 0.5h  | 
 0.5h  | 
| 
 Development  | 
 开发  | 
 20.5h  | 
 21.5  | 
| 
 .Analysis  | 
 .需求分析(包括学习新技术)  | 
 3h  | 
 2h  | 
| 
 .Design Spec  | 
 .生成设计文档  | 
 0h  | 
 0h  | 
| 
 .Design Review  | 
 .设计复审(和同事设计审核设计文档)  | 
 0h  | 
 0h  | 
| 
 .Coding Standard  | 
 .代码规范(为目前的开发指定合适的规范)  | 
 0h  | 
 0h  | 
| 
 .Design  | 
 .具体设计  | 
 3h  | 
 2.5h  | 
| 
 .Coding  | 
 .具体编码  | 
 8h  | 
 6h  | 
| 
 .Code Review  | 
 .代码复审  | 
 0h  | 
 0h  | 
| 
 .Test  | 
 .测试(自我测试,修改代码,提交修改)  | 
 2h  | 
 6h  | 
| 
 Reporting  | 
 报告  | 
 3h  | 
 4h  | 
| 
 .Test Reprot  | 
 .测试报告  | 
 1.5h  | 
 1h  | 
| 
 .Size Measurement  | 
 .计算工作量  | 
 0.5h  | 
 0h  | 
| 
 .Postmortem & Process Improvement Plan  | 
 .事后总结,并提出过程改进计划  | 
 1h  | 
 0.5h  | 
| 
 合计  | 
 24h  | 
 23h  | 
3.解题思路
3.1 任务需求
编写命令行程序(sudoku.exe),支持下列指令:1. sudoku.exe –c N 2. sudoku.exe -s absolute_path_of_puzzlefile;
指令1实现程序生成不重复的N个数独终局至同目录下文件sudoku.txt,数独矩阵的左上角数字为确定的(9+6)%9+1=7;指令2将绝对路径下的数独题目解出并生成答案于同目录下的sudoku.txt。
3.2 思路分析
直觉告诉我解数独比较简单,所以我先想的是怎么解数独。查了下资料发现主要就两种方法,最简单的就是递归回溯,另外一种就是Dancing Links算法,将数独转化成精确覆盖问题求解,其实也是需要回溯的,但是使用的数据结构比较方便。因为我先想的也是递归填数,而且比较简单,所以就选择第一种方法了。思路很简单,将读取到的数独题目保存在9*9的二维数组中:
- 从第一个格子开始填数,如果该格子已经填过,那么处理下一个格子;
 - 如果当前格子是空的,尝试从1-9中选择数字填入,并判断是否符合数独规则,符合则填入;
 - 若最终没有找到合适的数填入,说明在之前填的某一个数字不对,进行回溯。
 - 若填的是最后一个格子且满足规则,说明找到了数独的一个解,程序结束。
 
解决了解数独的问题,再来看怎么生成数独,其实从上面的角度很容易就想到,把初始的数独矩阵全部置零,按照解数独的方法即可生成数独了,只不过需要生成的数独数量较多,相当于求全零数独的前N个解,那么只要将解数独的算法步骤稍作修改,当生成的解是第N个,输出该解让程序结束,当生成的解不足N个时,输出该解并回溯,这样就能够保证已经生成的解不会二次生成,因为回溯后必然改变之前的某个值。至于左上角规则,只需要在预先放置好需要的数字,然后从第二个格子开始处理即可。
4.实现过程
分析需求,我设计了以下函数和类:
4.1输入
vector<int(*)[]> SodukoInput(char * filename);
读取文件中的数独题目,每一个题目为一个2维矩阵,返回各题目矩阵指针的vector;
4.2输出
void SudokuOutput(char *ret, int maxnum, char *ret2);
将得到的保存所有数独解的数组设置格式(空格和回车),然后返回之,使用fputs输出。
4.3数独求解模块
class SudokuSolve {
public:
    bool Solve(int r, int l);//递归填数
    bool check(int r, int l, int num);//测试同行同宫同列是否已有num
    void ProblemInit(int p[][]);//初始化
    int(*getSolution())[];//返回解
private:
    int problem[][];
};
每一个需要求解的数独初始化一个类,Solve函数即为对problem[r][l]的处理试填,check函数判断在problem[r][l]处填入num是否符合规则,getSolution用于在解决数独后返回填完的problem的指针,用于输出。
4.3数独生成模块
class SudokuMaker {
public:
    bool fill(int r, int l, char *ret);//递归填数
    bool check(int r, int l, int num);//判断在[r,l]处放入num是否符合数独规则
    void RequestInit(int n);//初始化需求
private:
    int maxnum;//需要生成的数独终局数
    int count;//当前已生成的数独终局数
    int M[][];//维护的数独棋盘
};
5.关键代码说明
下面给出数独求解的Solve函数进行说明:
bool SudokuSolve::Solve(int r, int l)
{
int nr = l == ? r + : r;
int nl = l == ? : l + ;
if (problem[r][l] != && nr < )//(r, l) already has a number
{
if (SudokuSolve::Solve(nr, nl))
return true;
else
return false;
}
else if (problem[r][l] != && nr >= )//problem solved
return true;
// now problem[i][j] == 0, try to fill it.
for (int k = ; k < ; ++k)
{
if (!SudokuSolve::check(r, l, k))
continue;
problem[r][l] = k;
if (r == && l == )//problem solved
return true;
if (SudokuSolve::Solve(nr, nl))
return true;
else
problem[r][l] = ;//k is bad, try k+1.
}
return false;//can't find a k.
}
实际调用的时候,首先初始化数独题目,即函数中的problem,而后调用Solve(0,0)即可将problem解出;上面的函数首先计算当前处理位置的下一位置,若当前位置已经有值,则直接处理下一个,若恰好在最后一个位置有值,则说明此时数独已经解好了,可以结束递归;在当前位置为空时,我们就要尝试填数,对每一个判断是否符合规则,找到一个合适的值后,若填的是最后一个位置,同样说明数独解决;否则填值后处理下一个位置,若下一个位置的处理失败,说明当前位置的填值不合适,尝试下一个数;在尝试所有数后,若没有合适的,说明之前位置填值有误,需要恢复当前位置的空状态并回溯。注意恢复problem[r][l]=0是必要的,否则回溯到上一个位置时会对check函数(判断同行同列同宫是否存在k)的结果有影响,导致少解。
对这个函数的单元测试函数如下:
TEST_METHOD(TestMethod1)
{
int p[][] = { {,,,,,,,,},{,,,,,,,,},{,,,,,,,,},
{,,,,,,,,},{,,,,,,,,},{,,,,,,,,},
{,,,,,,,,},{,,,,,,,,},{,,,,,,,,} };
SudokuSolve s;
s.ProblemInit(p);
s.Solve(, );
int(*q)[] = s.getSolution();
bool r = true;
for (int i = ; i < ; ++i)
{
for (int j = ; j < ; ++j)
r &= s.check(i, j, q[i][j]);
}
Assert::AreEqual(r, true);
}
使用的数独题目为号称最难的芬兰题,在实际运行中使用clock计时得到求解时间为245ms,测试中给出数独的解并测试其是否合法,测试结果如下:

相似的思路处理生成数独的问题,给出SudokuMaker::fill函数如下:
bool SudokuMaker::fill(int r, int l, char *ret)
{
int nr = l == ? r + : r;
int nl = l == ? : l + ;
for (int k = ; k < ; ++k)
{
if (!SudokuMaker::check(r, l, k))
continue;
M[r][l] = k;
if (r == && l == )//到达最后一个位置
{
count++;
if (count == maxnum)//若已生成要求数目的数独终局,则输出终局并结束递归
{
for (int i = ; i < ; ++i)
{
for (int j = ; j < ; ++j)
{
ret[ * i + j + * (count-)] = char(M[i][j] + '');
}
}
return true;
}
else
{
//生成数目不够,则输出并恢复[r,l]处的值,并试填下一个
for (int i = ; i < ; ++i)
{
for (int j = ; j < ; ++j)
{
ret[ * i + j + * (count-)] = char(M[i][j] + '');
}
}
M[r][l] = ;
continue;
}
}
else
{
if (SudokuMaker::fill(nr, nl, ret))//递归求解下一个位置
return true;
else
{
M[r][l] = ;
continue;
}
}
}
return false;
}
可以看到两个函数布局差不多,实际上和之前分析的一样,初始化数组M的左上角,从位置(0,1)开始求解,两者的区别在于前者只要得到一个可行解就可以输出结束递归,而后者需要生成maxnum个数独终局,因此在生成足够数目的数独前,函数一律在输出后继续尝试,尝试所有的值后回溯。下面给出代码覆盖率测试结果,分两块,一块是生成数独,一块是解数独:


因为一次只能设置一个命令行参数,所以都不是100%,但是可以看到两个模块分别都是覆盖率很高的。
6.性能分析和改进
由于选择的是暴力回溯,瓶颈在那里,所以数独生成的速度肯定不会很快…但是按照以上的思路编译通过后第一次性能分析,我选择的是生成1000个数独终局,运行时间达到了惊人的38秒,我瞬间有了跳崖的冲动。仔细查看性能分析结果,我发现程序98%以上的时间在做文件IO(最初版本性能测试没有截图),于是我仔细查看了我的IO函数,实际上我的IO是在每生成一个数组的时候进行输出,由于写的比较快,就直接在函数内进行文件的打开和关闭了,所以输出1000个数独需要开闭文件1000次,我将文件流传入函数,在外面开闭文件,速度就上去了。此时100000个数独需要1分钟左右,仍然很慢,下面是这时的输出函数。
void SudokuOutput(int p[][], bool flag, fstream &file)
{
if (!file.is_open())
cerr << "fail to open file!" << endl;
else
{
for (int i = ; i < ; ++i)
{
for (int j = ; j < ; ++j)
{
if (j == )
file << p[i][j] << endl;
else
file << p[i][j] << ' ';
}
}
if (flag)
file << endl;
}
}
再次进行性能分析,发现程序运行时间依然是文件IO占了大头,这是突然想到这里是直接将数字以整型输出到文件,如果我把它改成字符输出呢?当即把p[i][j]改成char(p[i][j]+’0’),发现输出快了十几秒。继续分析发现操作符<<和endl的耗时很长,查阅资料,这里endl的flush作用是不需要的,所以将endl改成file.put(‘\n’),前者同理,到这里再运行,100000级的时间是14秒。但是还是很慢。由于我选择的算法十分的暴力,所以我预期百万级的测试在1分钟内完成,十万级就耗时14秒是不行的。机缘巧合之下又看到了这篇文章,于是试用了freopen重定向和putchar的组合,运行时间优化到9秒,确实有效果。这时又看到微信群里罗老师建议保存答案到最后一起输出,于是我尝试建了一个全局字符数组,将所有终局都存进去,在最后的时候直接用fputs全部输出,这时运行100000级输出运行时间为:5.121s,百万级为:49.553s(用clock计时)。满足预期了……到这里Output函数已经面目全非,被我改成给输出的数组添加空格和回车的函数了,就不贴了,但是之前贴的关键函数都是最终版。下面是性能分析结果(-c 100000):

可以看到在优化IO后,IO占的时间很少了,现在最耗时的在于每次试填都要使用的check函数,用于判断同行同宫同列是否符合需求,我尝试过维护专门的数组来记录每行每列每宫已填数的信息,这样check函数就只需要查询这些数组了,但是实际测试下来和最简单的直接遍历行列宫相差无几,因为维护这些数组同样需要时间成本,所以最终按照原方案。
7.PSP各模块实际花费时间(略,见1)
8.感想
俗话说得好,不作死就不会死,暴力回溯生成数独终局确实是挺慢的,比不上各种取巧的方法,但是用来解数独我倒觉得是最实用也是最简单的方法,因为解数独是无法避免回溯和试填的。在写这个程序之前,说实话我没怎么用C++写过程序,计院的面向对象也还没上过,撑死了用C++解过几十道LeetCode,只能说略懂C++的语法而已,可以说是相当的菜了。前面写的优化其实只是对IO作了优化,对大佬们来说可以说是相当trivial了,但是对我来说,之前确实没有过处理这么多数据的情况,所以其实收获还是蛮大的,因为很多东西都是第一次用,包括VS和GitHub。最后,图简单暴力解题我觉得我大概要倒数了吧……
2017BUAA软工个人项目之数独生成与求解的更多相关文章
- [2017BUAA软工]个人项目:数独
		
一.项目地址 https://github.com/Slontia/Sudoku 附加作业(GUI):https://github.com/Slontia/SudokuGUI 二.开发时间 PSP2. ...
 - [2017BUAA软工]结对项目:数独扩展
		
结对项目:数独扩展 1. Github项目地址 https://github.com/Slontia/Sudoku2 2. PSP估计表格 3. 关于Information Hiding, Inter ...
 - [2017BUAA软工]个人项目
		
软工个人项目 一.Github项目地址 https://github.com/Lydia-yang/2017BUAA-SoftwareEngineering 二.解题思路 在刚开始拿到题目的时候,关于 ...
 - [2017BUAA软工]结对项目
		
软工结对项目 一. Github项目地址 https://github.com/crvz6182/sudoku_partner 二. PSP表格 Psp personal software progr ...
 - [2017BUAA软工]结对项目-数独程序扩展
		
零.github地址 GitHub地址:https://github.com/Liu-SD/SudoCmd (这个地址是命令行模式数独的仓库,包含了用作测试的BIN.DLL核心计算模块地址是:http ...
 - [2017BUAA软工]个人项目心得体会:数独
		
心得体会 回顾此次个人项目,感受比较复杂,最明显的一点是--累!代码编写.单元测试.代码覆盖.性能优化,环环相扣,有种从作业发布开始就一直在赶DDL的感觉,但是很充实,也学习到和体验了很多东西.最令人 ...
 - [2017BUAA软工助教]个人项目小结
		
2017BUAA个人项目小结 一.作业链接 http://www.cnblogs.com/jiel/p/7545780.html 二.评分细则 0.注意事项 按时间完成并提交--正常评分 晚交一周以内 ...
 - [2017BUAA软工助教]个人项目准备工作
		
BUAA软工个人项目准备工作 零.注册Github个人账号(你不会没有吧..) 这是Git的使用教程: http://www.cnblogs.com/schaepher/p/5561193.html ...
 - [2017BUAA软工助教]第0次作业小结
		
BUAA软工第0次作业小结 零.题目 作业链接: This is a hyperlink 一.评分规则 本次作业满分10分: 按时提交有分 一周内补交得0分 超过一周不交或抄袭倒扣全部分数 评分规则如 ...
 
随机推荐
- FusionCharts参数说明——3D饼图属性(Pie3D.swf )
			
animation 是否显示加载图表时的动画palette 内置的图表样式,共5个paletteColors 自定义图表元素颜色(为多个,如过过少会重复)showAboutMenuItem 右键是否显 ...
 - DAG 动态规划 巴比伦塔 B - The Tower of Babylon
			
题目:The Tower of Babylon 这是一个DAG 模型,有两种常规解法 1.记忆化搜索, 写函数,去查找上一个符合的值,不断递归 2.递推法 方法一:记忆化搜索 #include < ...
 - sed命令替换字符包含斜杠\,引号的处理方法
			
在字符替换中,可能会遇见引号,“/”等的替换,这时应该注意,sed的命令原型是: sed -i "s/oldstring/goalstring/g" file 如果一个路径是da ...
 - SSM 搭建精美实用的管理系统
			
课程介绍 SSM 框架即 SpringMVC+Spring+Mybatis,相信各位朋友在投递简历时已直观感受到它的重要性,JavaWeb 相关工作的招聘要求中基本都包括了这三项技术能力. 由于其轻量 ...
 - java.lang.NoClassDefFoundError: org/eclipse/core/resources/IContainer
			
启动eclipse报错:java.lang.NoClassDefFoundError: org/eclipse/core/resources/IContainer 解决办法: 删除以下文件.metad ...
 - nginx和php-fpm调用方式
			
一.背景: 在开发中碰到一个问题,项目以nginx+php-fpm形式访问交互,结果访问项目时报错如下图: 二.分析: 提示很明确嘛,去看error.log(在nginx.conf或者vhost里 ...
 - 理解JSON.stringify()高级用法
			
一:JSON.stringify() 该方法是把javascript对象转换成json字符串. 基本语法:JSON.stringify(value, [, replacer], [, space]) ...
 - Javascript 对象复制
			
如果对象只是一个数据集,可采用json化再反json化的方式克隆一个对象,这个过程会丢失对象的方法.效率比较低. 可以采用如下递归的方式复制一个对象. function clone(target) { ...
 - ORA-10858:在要求输入数字处找到非数字字符
			
今天在写sql语句的时候,运行报错:ORA-10858:在要求输入数字处找到非数字字符 在网上查了一下为什么会有这种错误,有一个建议是可能是写的sql语句中的日期没有处理.仔细看了一下自己写的代码 就 ...
 - TCP/IP协议--TCP协议概括和TCP连接的建立和终止
			
TCP提供一种面向连接的.可靠的字节流服务.面向连接指,发送和接收方在交换数据前必须建立一个TCP连接.顺便说下,一个TCP连接只有两方,因此广播和多播是不能应用于TCP的.字节流指,两个应用程序通过 ...