数独终局生成和求解(二)代码实现和性能分析
软件工程基础个人大作业 数独终局生成和求解
github项目地址https://github.com/lukal-new/new-potato/tree/sudoku
一、预计开发
Personal Software Process Stages | 预估耗时(分钟) |
---|---|
计划 | 60 |
估计任务所需时间 | 20 |
开发 | 1500 |
需求分析 | 30 |
生成设计文档 | 40 |
设计复审 | 40 |
代码规范 | 300 |
具体设计 | 300 |
具体代码 | 600 |
具体编码 | 600 |
代码复审 | 60 |
测试 | 300 |
报告 | 120 |
测试报告 | 120 |
计算工作量 | 30 |
事后总结并提出过程改进计划 | 120 |
合计 | 4240 |
二、解题思路描述
首先了解数独的规则, 以下是摘自百度百科的内容。
数独(shù dú)是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3)内的数字均含1-9,不重复 。数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次,所以又称“九宫格”。
根据题目意思,主要分为两部分。一是生成最多一百万个数独终局,并存入文件中,生成新文件要覆盖原文件。二是从某路径文件中读取最多一百万个数独,并求解存入文件中。以下内容主要描述算法和实现思路。
1.生成数独终局
数独的生成方法有很多种,比如随机法和行列变换法。但为了兼顾性能和输出时间,在此选择比较简单的一种算法。主要思路是随机生成第一行,然后从第二行开始,每一行是第一行右移3,6,1,4,7,2,5,8列的结果,这样就可以产生一个数独终局。由于题目要求第一行第一个数字必须是固定的,我的固定数字为9,所以只用生成第一行的后八位数字。这样可以产生的数独终局个数为8!=40320种。为了达到要求的一百万种,可以将已生成的数独做行列交换,因为第一行已经固定,所以第二行和第三行可以交换,四五六行可以互换,七八九行可以互换。所以最终可以产生2!*3!*3!8!=2903040种,远超要求的一百万种。为了简化代码,只对四五六行、七八九行进行互换即可,二三行可以不用动。这样就有3!*3!*8!=1451520种,也达到了要求。总结来说,使用的算法主要思路是,第一行的第一个数字固定为9,随机生成后8位数是一行右移3,6,1,4,7,2,5,8位依次得到剩下8行,可以互相交换四五六行、七八九行。最终输出每一行,可以得到题目要求的结果。
2.求解数独
求解数独的方法也有很多,比较典型的是回溯法,简单但效率比较低。回溯算法的主要思路是从根节点开始向下搜索,如该正在被搜索的节点有解,则向下继续搜索,如无解,则返回到该节点的父节点。对于数独求解问题,对所有空格,即为0的位置,可以依据全局找到可能填入的数字。从数字最少的空格开始,填入某一个数字,然后在数字第二少的空格填入数字,直到某一个空格无法填入位置,向上一个填入的空格返回,更换该空格填入的数字。题目要求是找到一个可行解即可,所以不用搜索完所有解空间,直到找到一个可以将所有空格填入的方法,则不用继续搜索。并且可以设计合适的剪枝条件,来提高搜索效率。
三、设计实现过程
1.数独的表示形式
显而易见数独可以定义为int[9][9]这种形式。但对于终局生成来说,最多要打印1000000个数独终局,每个终局都是81个数字,如果用1000000个iint型二维数组肯定不行。所以在输出终局时要用一个足够长的字符串,包括了终局里的空格、换行符号。在此,我定义了一个长度为20000000的字符串,用来表示输出的所有数独终局。但由于这个字符串太大了,所以就作为全局变量,放在函数外面即可。
对求解数独来说,要先读取和得到的终局格式相同的文件,空格是用0表示,但要求解空格位置,就必须把读取到的字符串转化为int[9][9]的形式,才能进行求解,所以还是要在数独类中定义int[9][9]。
2.数独类的设计
由于整个程序是用c++写的,所以可以利用面向对象的封装性,把与数独有关的所有东西都封装在Sudoku类中,在主函数中对参数进行判别,定义一个类,然后调用相应的函数。
数独类里的私有变量是一个int[9][9]和两个文件指针,一个用来指向生成数独终局的文件的指针,一个用来指向求解数独存放文件的指针。
函数有一个生成数独终局函数createSudoku()函数,在这个函数外部,定义一个全局变量char[20000000],用来存放生成的终局。
class Sudoku
{
private:
char grid[9][9]h = { 0 };
FILE *resultfile1;
FILE *resultfile2;
public:
Sudoku();
~Sudoku();
void solveSudoku(string);
void createSudoku(int);
void backtrace(int );
bool isPlace(int );
};
3.函数流程
主函数判定参数是否合法不合法报错合法创建Sudoku类判定参数类型生成数独求解数独调用createSudoku函数调用solveSudoku函数调用backrtace函数调用isPlace函数四、描述改进过程
由于软件配置的问题,导致在进行性能分析的时候,产生了一些奇怪的问题,经过网上查询也无果。改用同学的电脑得到的性能分析图如下。
由上图,可以得知消耗最大的函数如上,但由于符号文件不包含源信息,所以在界面里无法查询到具体函数。但根据上图含义,应该是createSudoku函数的fputs函数和全局变量matx[2000000000]。
char matx[200000000] = { 0 };
puts(matx, resultfile1);
在编译的时候,由于系统默认分配的栈过小,要在vs里更改系统栈的分配,但还是无法满足题目要求的生成一百万个数独终局,所以将数独字符串初始化为一个全局变量,这样就可以达到要求。
对fputs函数而言,是最后用来将生成的字符串导入sudoku.txt文本中的,这个函数比较方便,当指定参数较大时,使得要导入的字符串也比较大,所以fputs函数比较耗时。但其实并不影响整体性能。
五、代码说明
1.createSudoku函数
char matx[200000000] = { 0 }; void Sudoku::createSudoku(int num) { errno_t err; err = fopen_s(&resultfile1, "sudoku.txt", "w"); if (err != 0) { printf("生成文档失败\n"); } int count = 0; int shift[9] = { 0, 3, 6, 1, 4, 7, 2, 5, 8 }; for (int i = 0; i < 6; i++) { if (count >= num) { //matx[count * 163 - 1] = '\0'; //cout << "i="<<i << endl; break; } if (i) { next_permutation(shift + 3, shift + 6); //shift[6] = 2, shift[7] = 5, shift[8] = 8; //cout <<"sh1"<< shift << endl; } for (int j = 0; j < 6; j++) { if (count >= num) { //cout << "j="<<j << endl; break; } if (j) { next_permutation(shift + 6, shift + 9); //cout <<"sh2"<< shift << endl; } char row[10] = "912345678"; for (int k = 0; k < 40320; k++) { if (count >= num) break; if (k) { next_permutation(row + 1, row + 9); //cout << "sh3" << row << endl; } int m = 0; for (int t = 0; t < 9; t++) { for (int y = 0; y < 9; y++) { matx[m + count * 163] = row[(y + shift[t]) % 9]; matx[m + 1 + count * 163] = ' '; //cout<<matx[m + count * 163] << endl; m += 2; } //cout << matx << endl; matx[(m - 1) + 163 * count] = '\n'; } matx[162 + 163 * count] = '\n'; //cout << matx <<" 1"<< endl; //cout << m << " 2" << endl; count++; //cout << count <<" 3"<< endl; } } } matx[count * 163 - 1] = '\0'; fputs(matx, resultfile1);}
createSudoku函数首先打开并写入“sudoku.txt”文本,当存在这个文本时,覆盖掉,当不存在时,创建并写入。在之前的设计中,得知要产生大于一百万个数独,第一行右移3,6,1,4,7,2,5,8位依次得到剩下8行,可以互相交换四五六行、七八九行,共有3!*3!*8!=1451520种。则必须有一个3!3!8!即6640320的循环嵌套,通过next_permutation函数获得全排列组合。
2.solveSudoku函数
ifstream problemfile(path); errno_t err; err = fopen_s(&resultfile2, "sudoku.txt", "w+"); if (problemfile) { int total = 0; string temp[9]; string str; int line = 0; bool exc = false; while (total < 1000000 && getline(problemfile, str)) { temp[line] = str; line++; if (line == 9) { for (int i = 0; i < 9; i++) for (int j = 0; j < 9; j++) { grid[i][j] = temp[i][2 * j]; if (grid[i][j] < '0' || grid[i][j] > '9') { exc = true; break; } } getline(problemfile, str); line = 0; if (exc) { exc = false; cout << "Error!" << endl; continue; } total++; // solve sudoku long count = 0; backtrace(0); } } //resultfile.close(); } else cout << "Can't find such file:" << string(path) << endl;
solveSudoku函数首先打开指定路径的文件,若打开失败则报错。使用getline函数从文件中一行一行读取字符串,每9行为一个单位,进行循环。循环中,将读取到的字符串存入grid[9][9]中,并且判断存入的字符是否合法。若不合法,则报错。每读取完9行,需要将一个换行符吃掉。每一个grid[9][9]调用一个回溯函数,从0层开始回溯,在回溯函数中会调用判定函数。
六、实际花费时间
Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟 |
---|---|---|
计划 | 60 | 50 |
估计任务所需时间 | 20 | 20 |
开发 | 1500 | 480 |
需求分析 | 30 | 30 |
生成设计文档 | 40 | 60 |
设计复审 | 40 | 20 |
代码规范 | 300 | 60 |
具体设计 | 300 | 120 |
具体代码 | 600 | 480 |
具体编码 | 600 | 60 |
代码复审 | 60 | 60 |
测试 | 300 | 300 |
报告 | 120 | 150 |
测试报告 | 120 | 150 |
计算工作量 | 30 | 40 |
事后总结并提出过程改进计划 | 120 | 90 |
合计 | 4240 | 2170 |
- 点赞
- 收藏
- 分享
- 文章举报
- 数独终局生成和求解(三)测试和代码改进
- 数独终局生成与求解python实现
- 软件工程基础个人项目——数独终局生成&求解
- 最大子序列的求解分析(java代码实现)
- 性能测试数据生成——java代码实现
- C++链表AT&T代码,通过Ubuntu实现生成(Linux内核分析笔记)
- 数独终局游戏(数独终局生成,数独问题求解,数独题目生成)
- 【软件工程基础】个人项目-数独-代码质量分析及性能测试
- 递归求解并生成哈夫曼编码的代码实现
- 计算机图形学——直线生成算法(原理分析及代码实现)
- 几个排序算法性能分析及其实现代码
- IL分析及本地代码提前生成性能分析
- 数独终局生成及残局求解
- ASP.NET 2.0 HttpHandler实现生成图片验证码(示例代码下载)
- ASP.NET 2.0 HttpHandler实现生成图片验证码(示例代码下载)
- 生成可执行BPEL代码所需必要信息分析和归纳
- C#分析数据库结构,使用XSL模板自动生成代码
- (转)用.net实现远程获取其他网站页面内容!(核心代码分析)
- ASP.NET 2.0 HttpHandler实现生成图片验证码(示例代码下载)
- IIS6.0日志文件分析代码_1生成访问字段记录到数组中