一个数独终局的生成和求解的控制台程序
Github项目地址
https://github.com/lx59ling/shudu
任务
实现一个能够生成数独终局并能求解数独问题的控制台程序,该命令行程序能实现如下功能:
1、生成不重复的数独终局至文件
2、读取文件内的数独问题,求解并将结果输出到文件
时间预估
PSP2.1 | Personal Software Process Stages | 预估耗时 (分钟) | 实际耗时 (分钟) |
Planning | 计划 | 60 | 60 |
Estimate | 估计这个任务需要多少时间 | 3200 | |
Development | 开发 | ||
Analysis | 需求分析(包括学习新技术) | 30 | 100 |
Design Spec | 生成设计文档 | 60 | 60 |
Design Review | 设计复审 | 60 | 60 |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 120 | 60 |
Design | 具体设计 | 120 | 120 |
Coding | 具体编码 | 240 | 600 |
Code Review | 代码复审 | 120 | 200 |
Test | 测试(自我测试,修改代码,提交修改) | 180 | 360 |
Reporting | 报告 | 120 | 120 |
Test Report | 测试报告 | 120 | 120 |
Size Measurement | 计算工作量 | ||
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 100 | 120 |
合计 | 1820 |
需求分析
任务中已经提到,该程序需要实现的功能为:
1、对输入的命令进行判断,实现不同的功能,对非法输入进行报错;
2、生成不重复的数独终局至文件;
3、读取文件内的数独问题,求解并将结果输出到文件。
模块设计
命令判断模块:
根据题目要求:-c + 数字N,输出N个数独终局至文件sudoku.txt中;
-s + 文件名,求解对应文件中的数独问题,输出结果至文件sudoku.txt中。
要求两种不同的功能,于是首先需要一个命令判断(选择控制)模块,不同的命令进入不同的功能模块中,同时该模块还需判断输入的命令是否合法,不合法的输入需要报错。
生成数独模块:
(经上次的初次汇报过后,由老师指出生成数独模块设计的算法思路有问题,于是在我通过网上查找,找到了经过单元测试和性能测试后明显更高效率的算法,经分析后重新选择了该新的算法:)
根据题目要求:"在生成数独矩阵时,左上角的第一个数为:(学号后两位相加)% 9 + 1",对于我自己的数独生成,左上角第一个元素应为 (6+8)%9 + 1 = 6。该数据固定后,则第一行的其他数据有 8!= 40320 种排列,即第一行有 40320 种排列,在第一行数据确定后,可以以第一行为模板进行变化得到新得到 40320 个数独终局(暂未满足题目的 1,000,000 个数独终局上限的要求),建立在该 40320 个模板的基础上,可以通过交换某一个数独终局中的两个数,或在不打破数独规则的基础上交换行列,便可以得到新的数独终局。
考虑行变换:由于第一行固定,则生成的数独终局剩下8行在以第一行为模板的基础上进行行交换生成,生成得到一个数独终局的模板,然后再根据行变化交换后8行得到新的数独终局。因为交换行不会打破列的合法行,但可能会打破3×3矩阵的合法性,所以我们只在3×3矩阵内部进行交换,即第2行和第3行交换(共2种),第4行和第5、6行交换(3!= 6 种),第7行和第8、9行交换(3!= 6 种)。在原来 40320 个终局模板的基础上添加行变换我们可以得到总共 40320*2*6*6 = 2,903,040 种不重复的数独终局,已经满足了题目 生成的数独终局数N的范围为 [0,1,000,000] 的要求,故本程序只考虑行变换,不再考虑数字交换和列变换了。
求解数独模块:
按照之前的思路,采用深度优先搜索的方法对数独问题进行求解。
类图:
private:
int sudo_map[9][9]——存储9*9数独图的二维数组
int count——记录当前生成数独终局的个数
int num——记录需要得到的数独终局个数
public:
sudo(int n)——构造函数,参数n为需要生成数独终局的个数
void creat_End()——生成数独终局的函数,对应数独终局生成模块
void write_to_file()——输出数独终局至文件
void solve_Que(int count)——解决数独问题并生成结果的函数,对应求解数独问题模块
bool is_right(int count)——判断某一个数是否符合数独的要求
性能分析
测试数据为生成100000个数独终局,总用时为36.813s
经VS自带的性能分析工具发现时间大多花费在write_to_file函数即写入数据至文件的部分上。
于是开始对这部分函数进行修改。经过上网查询fstream的原理并询问同学,发现多次使用<<写入文件会耗费大量的时间,于是改进方法为先将所有的数独终局存入一个一维int型数组中,再将这个一维int型数组的内容按输出格式(数字之间空格,每9个数字换行,每个终局间换行)的要求存入一个一维的char型数组中,最后直接将这个char型数组的内容写入到文件中便可大大提高写入速率。
改进后生成100000个数独终局的总用时为7.495s,比之前快了很多!
核心代码
全排列:
#ifndef _PERM_H
#define _PERM_H
#include<cstdio>
#include<cstdlib>
void swap(int *a, int *b)//交换
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void Reversal(int *a, int *b)//反转区间
{
while (a < b)
{
swap(a++, b--);
}
}
int Perm(int *s_start, int *s_end) //对从s_start到s_end进行全排列
{
int *p, *q, *s_find;
p = s_end;
while (p != s_start) //排列未结束
{
q = p;
p--;
if (*p < *q) //找到相邻的,左邻小于右邻的地址
{
s_find = s_end;
while (*s_find <= *p) //从最右端开始找大于左邻小于右邻的值 的地址
{
--s_find;
}
swap(p, s_find); //替换
Reversal(q, s_end); //反转
return 1;
}
}
Reversal(p, s_end); //如果没有下一个排列,全部反转后返回0
return 0;
}
#endif
生成数独终局:
int first_line[9] = { 5,1,2,3,4,6,7,8,9 }; //数独模型第一行的某一种情况,第一个数:(6+8)%9+1=5
int row[8] = { 1,2,3,4,5,6,7,8 }; //进行行变化对应行的下标
int arr_change[8] = { 3,6,1,4,7,2,5,8 }; //由第一行的数分别向左移动 3、6、1、4、7、2、5、8 可以得到一个完整的数独终局
do
{
for (int k = 0; k < 9; k++) //为第一行赋值
this->sudo_map[0][k] = first_line[k];
//根据map的关系和第一行的某个排列生成其他行,得到一个数独终局的模板
for (int i = 1; i < 9; i++)
{
for (int j = 0; j < 9; j++)
this->sudo_map[i][j] = first_line[(arr_change[i - 1] + j) % 9]; //分别将first_line进行不同程度移位后的行赋给后续的行
}
do
{
do
{
do
{
for (int j = 0; j < 9; j++)
{
}
//将产生的矩阵存入result数组中
for (int k = 0; k < 9; k++)
result[k + 81 * this->count] = first_line[k]; //存第一行数据
for (int i = 1; i < 9; i++)
for (int j = 0; j < 9; j++)
result[i * 9 + j + 81*this->count] = this->sudo_map[row[i-1]][j]; //将数独终局存入一个一维数组中,便于输出
this->count++;
if (this->count == this->num) //终局数达到要求了
return;
}while(Perm(row+5,row+7)); //对第7、8、9行进行排列
} while (Perm(row+2, row+4)); //对第4、5、6行进行排列
} while (Perm(row, row+1)); //对第2、3行进行排列
} while (Perm(first_line + 1, first_line + 8)); //每对第一行的后8位进行排列后便据此生成一个新的数独终局模板
return ;
求解数独终局:
void sudo::solve_Que(int count,int que_num)
{
//如果81个数字均有合法的填入,说明数独问题解决完毕,打印结果至文件
if (count == 81 && op ==1)
{
for (int i = 0; i < 81; i++)
result[i + 81* que_num] = solve_map[que_num][i]; //将解得的数独终局存入一个一维数组中
que_num++;
this->num++;
return;
}
int row = count / 9; //当前空所在行
int col = count % 9; //当前空所在列
//如果该位置为0,即需要进行求解
if (solve_map[que_num][count] == '0')
{
op = 0;
for (int i = 1; i <= 9; i++)
{
solve_map[que_num][count] = i; //将1-9填入该空位
if (is_right(count,que_num)) //判断该数是否合法
{
op = 1;
solve_Que(count + 1,que_num); //如果合法,则对下一个0位置进行操作
}
}
//如果该位置始终没找到合适的数字,或者dfs到某一层没有找到一个合适的数字,则重新置0等待回溯
solve_map[que_num][count] = 0;
op = 0;
}
//如果该位置不为0,则直接对下个位置进行操作
else
{
solve_Que(count + 1,que_num);
}
}
将数据写入文件:
void sudo::write_to_file()
{
int t = 0;
for (int k = 0; k < this->num; k++)
{
for (int j = 0; j < 81; j++)
{
write_type[t] = result[81 * k + j] + '0';
t++;
if (j % 9 == 8)
{
write_type[t] = '\n';
t++;
continue;
}
write_type[t] = ' ';
t++;
}
if (k < this->num - 1)
{
write_type[t] = '\n';
t++;
}
}
ofstream fp;
fp.open("sudoku.txt", ios::app);
if (!fp)
{
cout << "打开文件失败" << endl;
return ;
}
int i = 0;
while (write_type[i]!='\0')
{
fp << write_type[i];
i++;
}
cout << "work" << endl;
}
实验总结
通过本次实验最大的收获就是完整地体验了一次软件代码开发的绝大部分过程,而非往常写代码就是上网搜思路或者函数,然后自己一个劲地在编译器上一股脑往下写,没有构思整体和模块,想到啥编写啥。反倒是这次实验因为要求而做到了很多具体且对实验编码很有帮助的工作:如时间计划安排,概要设计、模块设计,包括后期最重要的单元测试和性能测试等,都让我从中学到了很多编写代码应有的良好习惯和过程。此外第一次一个人完成整个实验,虽然从网上学习和借鉴了很多方法和思路,代码也并没有理想的那样能完美地实现题目要求的功能,性能上也还有很大的提升空间,但是这样的实践经历确实是令我收获颇丰,为我日后的代码编写奠定了一个较好的基础,但时间安排上自己也有挺大的失误,许多前期准备工作没有做到很好(如按老师的要求每周更新进度),导致最后的性能测试上有许多欠缺,这也是我日后需要改进的地方。相信以后的实验我一定能完成的更加完善。