软件工程基础个人项目——生成数独终局以及求解数独
本次个人项目的需求可以分为两个主要部分:生成指定数量的不重复的数独终局以及读取文件内的数独终局,求解并将结果输出到文件。
整体项目的流程图分析如下:
一. 项目的GitHub地址
https://github.com/MAJIUWANG/-Software-engineering-project-mjw.git
二. 填写表格中的预计时间
PSP2.1 | Personal Software Process Stages | 预估耗时 (分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 100 | |
Estimate | 估计这个任务需要多少时间 | 2400 | |
Development | 开发 | 1200 | |
Analysis | 需求分析(包括学习新技术) | 300 | |
Design Spec | 生成设计文档 | 100 | |
Design Review | 设计复审(和同事审核设计文档) | 0 | |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 20 | |
Design | 具体设计 | 300 | |
Coding | 具体编码 | 300 | |
Code Review | 代码复审 | 0 | |
Test | 测试(自我测试,修改代码,提交修改) | 300 | |
Reporting | 报告 | 300 | |
Test Report | 测试报告 | 100 | |
Size Measurement | 计算工作量 | 20 | |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 60 | |
Total | 合计 | 3100 |
三. 解题思路描述
1. 生成数独终局模块解题思路
首先对第一个问题进行分析,通过阅读优快云博客我了解到,数独终局的形成有自己的内在规律,可以通过应用该规律大大降低生成数独终局的时间复杂度。
在生成数独矩阵时,左上角第一个数为(学号后两位相加)%9 + 1,这为此题目唯一的一个硬性约束条件。对于以下数独终局,其形成中,可以从第二行开始,每行分别是第一行右移3、6、1、4、7、2、5、8列的结果。(其中左上角第一个数为我的学号BIT-1120161752后两位相加模9+1,即为8):
8 9 1 2 3 4 5 6 7
5 6 7 8 9 1 2 3 4
2 3 4 5 6 7 8 9 1
7 8 9 1 2 3 4 5 6
4 5 6 7 8 9 1 2 3
1 2 3 4 5 6 7 8 9
6 7 8 9 1 2 3 4 5
4 5 6 7 8 9 1 2 3
9 1 2 3 4 5 6 7 8
按照同样的规律,可以获得8!即40320种不同的终局。同时,如果任意交换2-3行、4-6行或7-9行三行的位置,可以使每一种原先的终局扩展为72个终局,得到最多2903040种不同的终局情况,即可以满足题目中1000000种情况的要求。
为什么这个规律可以成功扩展生成的数独终局呢?可以看出,其每三行内部之间平移的距离差都是3或者6,即保证了三行内每一行与前一行之间没有重复的可能性且一定包含1-9的全部数字,三乘三方格的规律被满足。同时,每行之间数值也都相互错开,即3、6、1、4、7、2、5、8序列中没有重复的数值,保证了每一列中数字的不重复。每一行的数字本身也为1~9不重复,故数独终局的全部条件都被满足。
据此分析后,就可以设计实现生成数独终局需求的代码了。
2. 求解数独模块解题思路
在求解数独终局这个问题上,我想到的是使用回溯的方法,在一个给定的题目中,根据空结点的排列按自左至右、自上至下的顺序遍历寻找数独的解。如果当前空格可以放下某一个数字,就标记当前空格为已搜索,存入当前数字至该空格处,标记该数字在矩阵中已访问,并在当前情况下继续进行下一个空结点的搜索。如果当前空格不能放下任何一个数字,说明之前的空格中填入的数字有误,回溯至上一级将原操作撤销,并继续寻找新的数字进行填入,在新的可行情况下继续进行搜索,直至9x9的数独矩阵中没有空格时函数终止,即找到了该数独的一个解。
四. 设计实现过程
1. 需求分析
实现一个能够生成数独终局并求解数独终局的控制台程序。
-
生成数独终局:在控制台输入指令“sudoku.exe -c 有效数字n”,可以输出n个数独终局到当前目录下sudoku.txt文件中。
-
求解数独终局:在控制台输入指令“sudoku.exe -s 文件路径”,求解该文件路径对应文件中的数独题目,并将结果输出到当前目录下sudoku.txt文件中。
-
另:为生成求解数独终局所需题目,需要编写挖空函数实现对生成数独终局的一次随机挖空。
由此可见,该项目可以分为分析输入、生成数独终局、求解数独终局三个模块。其中分析输入模块在主函数中进行判断即可,生成数独终局和求解数独终局需要分别调用相应的函数。
2. 建模分析
功能建模
3. 生成数独终局模块设计实现过程
在生成数独终局模块,主函数部分的伪代码如下:
根据输入分析得到需要生成的数独终局个数n
通过n判断需要生成几轮随机数(当n大于72时round=n/72+1)
while(round)
{
Initial函数初始化数独的第一行
计算出当前轮数需要生成的终局个数demand(如果不是最后一轮则为72,为最后一轮则为n%72)
在当前数独第一行确定的情况下调用Produce_Sudoku()函数生成所需要的数独终局并逐个输出
--round;
}
在我的设计实现过程中,用到的函数有:
- void transform(double* temp1) 编码函数,采用浮点数随机序列生成方法(rand()/(double)RAND_MAX)*((upper)-(lowwer))+(lowwer),生成-30到30之间的8个随机浮点数,并通过编码函数对其从小到大排序,按其大小顺序得到一个整型随机数序列,加上左上角要求的学号后二位组成的数字,构成一个1~9的数独第一行随机数序列。
- void Initial()数独第一行初始化函数,生成浮点数随机序列并调用transform函数对其进行编码。
- void Print_Sudoku(int tag),为打印数独终局函数,其中由于打印生成的数独终局以及打印求解的数独终局使用同一个函数,在这里tag为1时为打印生成数独终局中的结果,tag为2时为打印求解数独终局中的结果。
- void Produce_Sudoku(int DEMAND),为该模块核心函数,其根据变换顺序的不同组合,一个数独终局可以延伸成为72个相同数值但不同组合结构的数独终局,并在其中调用Print_Sudoku函数进行输出。其流程图如下:
4. 求解数独模块设计实现过程
在求解数独模块,主函数部分的伪代码如下:
while(一行一行地读入含有数独题目的文件)
{
将文件中的数独题目逐行预存入生成的数独数组中
if(读入行数为9即读入了一个完整的数独题目后)
{
置标记找到答案的布尔值变量为false
Solve_Sudoku() 回溯求解数独
Print_Sudoku() 将求解结果打印
重置标记结点是否被访问的vis数组以及行计数变量
}
}
在最初我编写函数时,发现如何标记数独矩阵中一个点被访问过了很复杂,需要记录其在行、列以及九宫格中的出现情况。通过在网上搜索资料,我了解到,使用vis[3][10][10]数组来标记十分方便。其中第一维中0表示行、1表示列、2表示九宫格,第二维中表示在第几个行、列或九宫格中,而第三维表示其中的某个数字,如果该数字被填入了,vis值置1,否则置0。
在我的设计实现过程中,用到的函数有:
- void Set_Vis(int r, int c,int num),其置数独矩阵中r行c列数字num的位置为1。
- void Reset_Vis(int r, int c, int num),其置数独矩阵中r行c列数字num的位置为0。
- bool Check_Vis(int r, int c, int num),其检查数独矩阵中的r行c列的位置是否可以填入一个值为num的数字。
- void Print_Sudoku(int tag),为打印数独终局函数,在这里tag置2。
- void Solve_Sudoku(int r, int c),其为求解数独模块的核心函数,利用回溯算法对数独题目进行求解,其中调用Set_Vis、Reset_Vis以及Check_Vis函数,以及迭代调用自身。
5. GUI界面的生成
在生成GUI界面时,我原本采用MFC控制程序进行设计,然而由于时间只剩下一天多一点了,又感到不是很容易操作,就改为使用之前小学期熟悉过的Windows窗体程序进行GUI界面的设计。
将GUI界面设计分成填充数独题目以及判断提交界面两个主要部分。在填充数独题目时,从文件sudokuQuestions_1000.txt按顺序读取数独题目,在有数字时置终局棋盘为相应数字,无数字时置空。读取完数独题目后,仅需要判断填入的数独是否满足其适应规则即可,如果正确则弹出“你是真滴厉害,做对了喔!”,填错了就弹出“你做错了喔!”,如果在还没有完成数独题目即棋盘中有空格时就点击了提交按钮,就弹出“你还没有做完喔!”。
界面如下:
作答正确显示:
作答错误显示:
作答没有完成显示:
五. 程序改进
1. 应用代码分析工具消除警告
在编写完程序的首个版本后,应用vs自带的代码分析工具分析得到的警告如下:
解决方案为如下:
警告一:数值类型不匹配。程序中得到生成数独终局的个数时需要将字符串数据转换成整型数字,原本使用了pow函数,其导致了数值类型的不匹配问题。故采用原始的for循环进行字符串向整型数字的转化消除了警告。
警告二:输出文件目录未添加斜杠,而是让系统自动补全了。在项目属性中设定输出文件目录BIN时在其后补上斜杠即可。
应用解决方案后如下图可见,程序警告得到了消除。
2. 程序性能改进
改进一:代码块复用
在函数设计过程中,我把需要多次调用的代码块写成了函数。例如transform函数、Print_Sudoku函数等,同时将打印数独终局结果的函数和打印求解数独结果的函数合二为一,通过参数标记的不同调用函数的不同部分,使得函数的功能更加清晰,代码的整体结构更为简洁。
改进二:改机随机数生成方法
在生成数独终局模块中,生成数独第一行的随机序列时,我最先使用的是最为基础的rand()%9+1的方式生成1-9的随机数,然而这种方式如果要保证每一次生成的随机序列1-9的数字不重复,在生成随机数种子上将会浪费过长的时间。我采用的改进方法为一次性生成一串-30到30之间的浮点型序列,然后调用transform函数对其进行编码,将其大小排列顺序得到的整型序列作为数独第一行的随机序列。由于排序中已经可以保证数字相异,故有效地解决了之前的问题。
改进三:改进输出
在生成数独终局模块,最初我的程序输出所用的时间很长,满足不了项目的正确性需求,这让我很苦恼。之后,我通过查阅资料发现,可以将输出模块由一个一个地打印数独矩阵中的整型数字改为将一个数独终局存入一个字符型数组中,按字符串输出的方式输出一整个数独终局。改进了输出模块后,我的程序速度提升了很多。
3. 应用性能分析工具
应用性能分析工具进行分析后发现,程序中主函数占用CPU最多,其次是读写文件的函数,剩余的函数占用CPU比例较小。
六.代码说明
1.生成数独终局模块代码说明
核心函数代码如下:
void Produce_Sudoku(int DEMAND)
{
int count = 0;
for (int i = 0; i < 2; i++) //共可以生成72种不同的排序方式
{
for (int j = 0; j < 6; j++)
{
for (int k = 0; k < 6; k++)
{
char s1[15], s2[15];
strcpy(s1, move1[i]);
strcpy(s2, move2[j]);
strcpy(move_boss, strcat(strcat(s1, s2), move3[k])); //排列组合出的字符型序列
To_int(); //将字符型序列转换为int型
for (int q = 2; q <= 9; q++) //逐行生成一个数独矩阵
{
for (int w = 1; w <= 9; w++)
{
int m = (w - move_boss_int[q - 1] + 9) % 9;
if (m == 0) //对应最后一列的情况需要特殊处理
m = 9;
a[q][w] = a[1][m];
}
}
++count;
Print_Sudoku(1);
if (count == DEMAND)//如果满足了这一轮的需求,就退出
return;
}
}
}
}
其中,move1、move2、move3数组的定义如下:
char move1[10][5]={"036","063"}; //移动规则
char move2[10][5]={"258","285","528","582","852","825"};
char move3[10][5]={"147","174","417","471","714","741"};
2.求解数独模块代码说明
核心函数代码如下:
void Solve_Sudoku(int r, int c)
{
while (a[r][c] != '0') //找到一个空的数独位置
{
if (c < 8)
c++;
else //再来一轮
{
c = 0;
r++;
}
if (r == 9) //找到了一个答案即9x9数独中没有0,那就是找到了一个解答
{
Found_Ans = true;
return; //走喽
}
}
bool Can_Search=false; //标记回溯算法中当前结点是否可以搜索
for(int i=1;i<=9;i++)
{
if(Check_Vis(r,c,i))
{
Can_Search=true; //标记可以搜索
Set_Vis(r,c,i); //当前结点搜索过
a[r][c]=i+'0';
Solve_Sudoku(r,c);
if(Found_Ans) //剪枝
return;
Can_Search=false;
Reset_Vis(r,c,i);
a[r][c]='0';
}
}
}
七. 填写表格中的实际时间
PSP2.1 | Personal Software Process Stages | 预估耗时 (分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 100 | 80 |
Estimate | 估计这个任务需要多少时间 | 2400 | 2400 |
Development | 开发 | 1200 | 800 |
Analysis | 需求分析(包括学习新技术) | 300 | 200 |
Design Spec | 生成设计文档 | 100 | 120 |
Design Review | 设计复审(和同事审核设计文档) | 0 | 0 |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 20 | 20 |
Design | 具体设计 | 300 | 240 |
Coding | 具体编码 | 300 | 420 |
Code Review | 代码复审 | 0 | 0 |
Test | 测试(自我测试,修改代码,提交修改) | 300 | 200 |
Reporting | 报告 | 300 | 350 |
Test Report | 测试报告 | 100 | 60 |
Size Measurement | 计算工作量 | 20 | 20 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 60 | 30 |
Total | 合计 | 3100 | 2540 |
八. 实验总结
在完成这个实验的过程中,我还是有许多的收获的。我熟悉并掌握了控制台程序的编写方法,掌握了生成数独终局以及求解数独终局的算法,同时也熟悉了使用GitHub管理项目代码的方法以及如何撰写优快云博客。
但是还是明显地感觉到了自己的不足,我全篇代码都是使用C语言面向过程进行编程的,没有实现其向C++的转化,而在该问题中使用面向对象编程可以更方便地进行测试以及代码结构的管理。