Github项目地址:https://github.com/2016bits/sudoku.git
附加题Github地址:https://github.com/2016bits/interface_of_sudoku.git
一、题目描述:
实现一个能够生成数独终局并且能求解数独问题的控制台程序。
二、要求描述:
实现一个命令行程序:
- 生成不重复的数独终局至文件;
- 读取文件中的数独问题,求解并将结果输出到文件
生成终局:
- 在命令行中使用-c参数加数字N(1<=N<=1000000)控制生成数独终局的数量,例如下述命令将生成20个数独终局至文件中:
sudoku.exe -c 20
- 将生成的数独终局用一个文本文件(假设名叫sudoku.txt)的形式保存起来,每次生成的txt文件需要覆盖上次的txt文件,格式如下,数与数之间由空格分开,终局与终局之间空一行,行末无空行:
2 6 8 4 7 3 9 5 1 3 4 1 9 6 5 2 7 8 7 9 5 8 1 2 3 6 4 5 7 4 6 2 1 8 3 9 1 3 9 5 4 8 6 2 7 8 2 6 3 9 7 4 1 5 9 1 7 2 8 6 5 4 3 6 8 3 1 5 4 7 9 2 4 5 2 7 3 9 1 8 6
- 程序在处理命令行参数时,不仅能处理正确的参数,还能处理各种异常的情况,如:
sudoku.exe -c abc
- 在生成数独矩阵时,左上角的第一个数为:(学号后两位相加)% 9 + 1。例如学生A学号后两位是80,则该数字为(8 + 0)% 9 + 1,那么生成的棋盘如下(x表示满足数独规则的任意数字):
9 x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x
求解数独:
- 在命令行中使用-s参数加文件名的形式求解数独,并将结果输出至文件,如:
sudoku.exe -s absolute_path_of_puzzlefile
- 格式如下,其中0代表空格,题目与题目之间空一行,行末无空格,最后一个数独题目后无空行:
9 0 8 0 6 0 1 2 4 2 3 7 4 5 1 9 6 8 1 4 6 0 2 0 3 5 7 0 1 2 0 7 0 5 9 3 0 7 3 0 1 0 4 8 2 4 8 0 0 0 5 6 0 1 7 0 4 5 9 0 8 1 6 8 9 0 7 4 6 2 0 0 3 0 5 0 8 0 7 0 9
- sudoku.txt的格式如下(与生成终局的要求相同):
9 5 8 3 6 7 1 2 4 2 3 7 4 5 1 9 6 8 1 4 6 9 2 8 3 5 7 6 1 2 8 7 4 5 9 3 5 7 3 6 1 9 4 8 2 4 8 9 2 3 5 6 7 1 7 2 4 5 9 3 8 1 6 8 9 1 7 4 6 2 3 5 3 6 5 1 8 2 7 4 9
- 数独题目个数N(1<=N<=1000000),保证数独格式正确。
三、时间估计:
PSP2.1 | Personal Software Process Stages | 预估耗时 | 实际耗时 |
Estimate | 估计这个任务需要多少时间 | 1month | 15days |
Analysis | 需求分析(包括学习新技术) | 7days | 1days |
Design Spec | 生成设计文档 | 3days | 1days |
Design Review | 设计复核(审核设计文档) | 3days | 1days |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 1days | 2days |
Design | 具体设计 | 10days | 1days |
Code Review | 具体编码 | 1days | 6days |
Test | 测试 | 3days | 1days |
Others | 其他 | 2-3days | 2days |
四、解题思路:
先审题,发现该题目包括两个部分:生成终局和求解数独,这两部分可以完全分开来做,其公用部分为输入、输出和数独规则的判断,所以可以将其分为两个不同的函数来执行。首先,实现输入的部分:本题与以往题目不同的地方就在于其输入,本题是通过在终端用命令调用exe文件,同时在此过程中传递参数。经百度,了解到main(int argc, char* argv[])中的参数argc和argv就是用来接收终端传来的参数,argc表示参数的个数,argv表示参数的数组。同时,该程序的输入和输出都是在文件中进行的,百度后了解到要用头文件fstream,用ifstream和ofstream来实现输入输出。下面具体分析生成终局和求解数独的做法:
生成终局:
如果完全按照搜索来做,复杂度会相当高,所以查阅相关资料后,结合数独的相关特点,总结了以下几点:
- 每行生成一个1-9的排列,从而保证行里的数字不会重复;
- 然后将下一行进行平移(平移距离不为0),从而保证列里的数字不会重复;
- 前三行的平移距离为0、3、6(036或063两种情况),接下来三行平移1、4、7(147、174、417、471、714、741六种情况),最后三行平移2、5、8(258、285、528、582、825、852六种情况),从而保证了每个3*3方格里的数字不会重复,且总方案数2*6*6*8!= 2903040 > 10^6,满足题目要求。
求解数独:
暂时除了深搜没有太好的方法,所以用一个结构体(或类)保存所有空格的行、列、3*3方阵,从而一个个进行试探,如果1-9均不合适,则返回上一层,若到达最后一个数字也没有冲突,则输出该结果。
五、流程图:
六、具体实现:
总体采用了面向对象的思想:
- 设计了一个名为Sudo的类,实现生成终局的函数Finality和实现解数独的函数Solution作为该类的方法,以及在实现生成终局时移位的函数Movesudo和实现解数独时生成时的搜索函数Tryspace也作为该类的方法;
- 将输入输出的文件变量fin和fout作为该类的属性,以及解数独时的空格结构体space和空格的数量space_num和存放数独的题目数组array作为该类的属性。
在主函数里面新建对象sudo,通过命令行传来的参数来判断对象具体调用的方法。
七、性能分析:
- 最初的版本使用ofstream来将生成的终局输出到文件,在dev上生成的.exe文件生成1000000个终局需要二十多秒,在vs上需要两分半;之后将输出改成fputc之后,在dev上生成.exe文件生成1000000个终局需要四秒多,在vs上需要十三秒多。可以说,代码的性能得到了很大的提升。
- 最初在找到一个解后没有返回,所以速度极慢;之后加入了判断,用变量ok来标记是否全部试探完毕,之后速度快多了。
七、主要代码:
生成移动规则并遍历:
char rule0[10][5] = {"036", "063"};
char rule1[10][5] = {"258", "285", "528", "582", "825", "852"};
char rule2[10][5] = {"147", "174", "417", "471", "714", "741"};
int count = 0; //记录终局个数
bool flag = true;
do {
a[8] = '3'; //插入学号 :(2+0)% 9 + 1 = 3
//平移的2*6*6种方式
for (int i = 0; i < 2 && flag; ++i) {
for (int j = 0; j < 6 && flag; ++j) {
for (int k = 0; k < 6 && flag; ++k) {
Movesudo(rule0[i], rule1[j], rule2[k], a, fout);
++count;
if (count >= num) {
flag = false;
break;
}
}
}
}
} while (next_permutation(a, a + 8)); //使用STL全排列函数
根据移动规则将终局输出到文件:
//开始移位并存储在文件中
for (int i = 0; i < 9; ++i) {
int step = steps[i];
fputc(a[(8+step)%9], fout);
for (int j = 1; j < 17; ++j) {
fputc(' ', fout);
fputc(a[((16-j)/2 + step) % 9], fout);
++j;
}
fputc('\n', fout);
}
fputc('\n', fout);
读取文件中的数独题目并存储:
while (!fin.eof()) {
//读取题目
int count = 1; //空格的编号
for (int i = 0; i < 9; ++i) {
for (int j = 0; j < 9; ++j) {
fin >> array[i][j];
if (array[i][j] == 0) {
space[count].row = i;
space[count].col = j;
space[count++].anum = (i / 3) * 3 + (j / 3);
}
}
}
num_space = count - 1;
ok = false;
Tryspace(1);
fout << endl;
}
深搜:
//逐个试探空格中的数字
if (ok) {
return;
}
if (now > num_space) {
for (int i = 0; i < 9; ++i) {
for (int j = 0; j < 8; ++j) {
fout << array[i][j] << " ";
}
fout << array[i][8] << endl;
}
return;
}
int row = space[now].row; //当前空格的所在行
int col = space[now].col; //当前空格的所在列
int anum = space[now].anum; //当前空格的所在方格数
for (int i = 1; i <= 9; ++i) {
bool flag = true; //检测是否符合数独规则
array[row][col] = i; //试探该空格的值
for (int j = 0; j < 9 && flag; ++j) {
//检测该行是否出现重复的数字
if (array[row][j] == i && j != col) {
flag = false;
break;
}
}
for (int j = 0; j < 9 && flag; ++j) {
//检测该列是否出现重复的数字
if (array[j][col] == i && j != row) {
flag = false;
break;
}
}
for (int j = (anum / 3) * 3; j <= (anum / 3) * 3 + 2 && flag; ++j) {
for (int k = (anum % 3) * 3; k <= (anum % 3) * 3 + 2 && flag; ++k) {
//检测该方格里是否出现重复的数字
if (array[j][k] == i && (j != row || k != col)) {
flag = false;
break;
}
}
}
if (flag) {
//向下一个空格试探
Tryspace(now + 1);
}
}
array[row][col] = 0;
总结:
通过这个数独项目的学习和制作,我学习了一些在完整软件制作过程中的一些基本技巧,学会了使用性能分析软件来指导自己对代码进行优化。而且,还借此机会学习了CSDN和GitHub的使用。帮助自己养成在开发前进行计划,安排和设计的习惯,为自己以后的软件开发打下了良好的基础。