您的位置:首页 > 编程语言

数独终局生成和求解(二)代码实现和性能分析

2020-02-12 19:16 886 查看

软件工程基础个人大作业 数独终局生成和求解

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
  • 点赞
  • 收藏
  • 分享
  • 文章举报
站内首发文章 Luke___ 发布了3 篇原创文章 · 获赞 0 · 访问量 13 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: