个人项目
1.GitHub项目地址:https://github.com/YYFCY/sudoku
2.项目PSP表格:
PS2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 60 |
.Estimate | .估计这个任务需要多少时间 | 300 | 360 |
Development | 开发 | ||
.Analysis | .需求分析(包括学习新技术) | 120 | 120 |
.Design Spec | .生成设计文档 | 30 | 30 |
.Design Review | .设计复审(和同事设计审核设计文档) | ||
.Code Standard | .代码规范(为目前的开发指定合适的规范) | 60 | 90 |
.Design | .具体设计 | 30 | 30 |
.Coding | .具体编码 | 900 | 600 |
.Code Review | .代码复审 | 30 | 30 |
.Test | .测试(自我测试,修改代码,提交修改) | 60 | 120 |
Reporting | 报告 | ||
.Test Report | .测试报告 | 60 | 60 |
.Size Measurement | .计算工作量 | 30 | 30 |
.Postmortem & Process Improvement Plan | .事后总结,并提出修改计划 | 30 | 30 |
合计 | 1680 | 1320 |
3.解题思路
3.1 生成数独终局
关于生成数独终局最初的想法就是用回溯法搜索所有的可行解,但是显然这个方法过于暴力,速度会很慢。
后来在网上看了很多博客,最后看到了一种很巧妙的快速生成一个数独的方法:通过确定第一行的数字,然后第二行到第九行可以由第一行分别右移3、6、1、4、7、2、5、8列来得到,这样生成的数独是合法的。由于数独左上角的数字固定,所以第一行就有8!=40320种排列,这样可以先得到40320种数独终局,然后对于任何数独的1到3行、4到6行、7到9行,任意交换这三行的数据,得到的依然是一个合法的数独终局,由于左上角的数字不能改变,因此只需交换4到6行中的任意两行或者7到9行中的任意两行,这样即可在之前的基础上得到3!×3!×8!=1451520种数独终局,已经达到题目所要求的1000000种。
3.2 解数独
关于解数独这部分思路就很直接了,用递归回溯法填数。首先给出检测数独是否合法的条件:要求每行每列以及每个小九宫格都要有1~9这些数字并且不重复。然后把以上的条件写成一个函数check来判断所填数字是否合法。
4.设计实现过程
整个项目主要分为两个模块:生成数独终局和解数独。
生成数独终局部分,我设计了两个函数,permutation和exchange。全排列函数permutation通过将第一行全排列以及移位得到剩下的二到九行来创建好数独终局一代模板,然后exchange函数通过交换数独的4到6或者7到9行中的任意两行得到数独终局。
解数独部分,同样设计了两个函数,一个是判断填入的数字是否合法的检测函数check,另一个是回溯求解函数,实现数独的全覆盖。
我还设计了一个ArgCheck函数用于处理命令行参数的合法问题。
最后单元测试部分我用小用例20组来检测生成的数独终局的合法性,用最大用例1000000组检测程序的性能。
5.性能分析
生成1000000个数独终局经过不断优化之后总共花了23秒左右,还算可以。
执行单个工作最多的函数模块就是permutation,其他部分消耗不大。
6.关键代码展示
全排列函数permutation通过将第一行全排列以及移位得到剩下的二到九行来创建好数独终局模板
//对数独第一行进行全排列,共有8!种,生成数独模板1
void permutation(int *a, int k, int m, int *Count, int N)
{
char sudo[10][10];
memset(sudo, 0, sizeof(sudo));
int b[8], i, j;
if (k == m)
{
if (*Count < N)
{
for (i = 0; i <= m; i++)
{
b[i] = a[i];
}
*Count += 1;
int k = 0, l = 0;
sudo[0][0] = '5';//学号1120161822,(2+2)% 9 + 1 = 5
for (i = 0; i < 8; i++)
{
sudo[0][i + 1] = b[i] + '0';
}
//分别将数独第一行右移3、6、1、4、7、2、5、8列得到第二到九行
for (i = 0; i < 9; i++)
{
if (k)
{
for (l = 0; l < 9; l++)
{
sudo[k][(l + Displacement[i - 1]) % 9] = sudo[0][l];
}
}
for (j = 0; j < 9; j++)
{
if (j) fputc(' ', fp1);
fputc(sudo[k][j], fp1);
}
fputc('\n', fp1);
k++;
}
fputc('\n', fp1);
char sudo_1[10][10];
strncpy_s(sudo_1[0], sizeof(sudo_1), sudo[0], sizeof(sudo));
*Count = exchange(sudo_1, *Count, N);
}
}
else
{
for (j = k; j <= m; j++)
{
swap(a[j], a[k]);
permutation(a, k + 1, m, Count, N);
swap(a[j], a[k]);
}
}
}
解数独部分的填入数字是否合法的检测函数check,分为行、列、九宫格三部分。
//解决数独模块里的行列以及九宫格检测,判断是否合法
bool check(int Count)
{
int x = Count / 9;//当前行位置
int y = Count % 9;//当前列位置
int i;
//每行都要有1~9且每一个数字只能存在一个
for (i = 0; i < 9; i++)
{
if (sudo_solve[x][y] == sudo_solve[x][i] && i != y) return false;
}
//每列都要有1~9且每一个数字只能存在一个
for (i = 0; i < 9; i++)
{
if (sudo_solve[x][y] == sudo_solve[i][y] && i != x) return false;
}
//每个九宫格都要有1~9且每一个数字只能存在一个
int xx = x / 3 * 3, yy = y / 3 * 3;
for (i = xx; i < xx + 3; ++i)
{
for (int j = yy; j < yy + 3; ++j)
{
if (sudo_solve[i][j] == sudo_solve[x][y] && i != x&&j != y) return false;
}
}
return true;
}
解数独模块的回溯法填数函数backtrace
//通过回溯法求解数独
void backtrace(int Count)
{
if (Count == 81)
{
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
fputc((char)(sudo_solve[i][j] + '0'), fp2);
fputc(' ', fp2);
}
fputc('\n', fp2);
}
fputc('\n', fp2);
return;
}
int x = Count / 9, y = Count % 9;
if (sudo_solve[x][y] == 0)
{
for (int i = 1; i <= 9; i++)
{
sudo_solve[x][y] = i;
if (check(Count))
backtrace(Count + 1);
}
sudo_solve[x][y] = 0;
}
else
backtrace(Count + 1);
}
7.个人总结
通过这个数独项目的学习和制作,我学习了很多一个完整软件制作过程中的一些基本技巧,学会了使用性能分析软件。
其实我个人觉得这个项目最难的部分并不是编写代码,而是后面的调试、测试还有性能分析,很多东西也是借鉴网上前辈的经验,另一方面由于时间原因算法的优化方面感觉还可以做的更好,像求解数独部分我在网上看到有人用Dancing Links算法,将数独转化成精确覆盖问题求解,效率显著提高,还有些人的博客里提到了关于输入输出的问题还可以用多种方法优化,这些地方之后还需要花时间好好思考一下。
总之,这个项目真正让我明白了项目不仅仅是编码,软件工程方面未知的东西还有很多,路漫漫其修远兮,吾将上下而求索。