项目介绍
GitHub地址:https://github.com/hobecken/Sudoku,
该项目目的是实现一个能够生成数独终局并且能求解数独问题的控制台程序。经过初步需求分析,在该项目中,需解决通过命令行控制输入,利用性能分析工具等对程序进行优化 ,将代码进展签入GitHub,使代码变得更高效而稳定。
PSP表格
PSP2.1 | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
Planning | 120 | 120 |
Estimate | 120 | 120 |
Development | 120 | 100 |
Analysis | 30 | 50 |
Design Spec | 60 | 45 |
Design Review | 60 | 60 |
Coding Standard | 60 | 30 |
Design | 60 | 90 |
Coding | 180 | 200 |
Code Review | 60 | 80 |
Test | 120 | 150 |
Reporting | 60 | 90 |
Test Report | 60 | 60 |
Size Measurement | 30 | 30 |
Postmortem & Process | 60 | 60 |
合计 | 1500 |
解题思路描述
本项目分为生成终局和解数独两部分。
生成终局
看到这个题目,我首先想到,这类问题最典型的解法是搜索。可以用二维数组表示9*9数独棋盘,通过深度优先搜索遍历,求出满足规则的终局。但是,利用搜索效率比较低,远不足以达到性能要求,之后我又进行了一定回溯剪枝的尝试,效果有限。
接着,我发现数独终局只要满足每行,每列,每宫都由1~9全排列构成,然后,我又上网查找资料,发现了一种生成数独终局的方法:全排列法。
这种方法的核心思想,在第一行第一个数被限制(必须为3)的情况下,全排列剩下的8个数,作为第一行,然后通过平移这一行,构造整个终局。然而,这样只能实现8!个终局,即40320个,还达不到题目要求生成1000000个数独终局的要求。然后,我发现,在已经生成的终局中,通过对4到6、7到9行进行全排列,可得到不重复的终局。于是,生成1e6个终局的要求得到满足。
解数独
由于解数独没有明确性能要求,因此我采用了回溯法求解,遍历整个数独,解出终局。
设计实现过程
性能分析
在首个版本中,由于该版本是测试版本,程序中保留这大量的cout,用于在控制台输出数独终局。删去这些cout,性能大幅提高。
在首次性能优化后,时间开销最大的函数是文件流输入输出符号 ‘<<’ ‘>>’ 的重载,在这个版本中,每当获得数独终局的一个元素,调用一次写入文件操作,产生大量开销。因此,我希望把写入文件操作放在生成一个终局之后,每获得其中一个元素,将它添加到字符串中。而后我考虑到如何实现这一功能。首先我考虑使用strcpy函数,但频繁调用这个函数也会带来一定时间开销,因此我使用了一个全局字符指针存储这个串,每当获得一个新的元素,就把它一次放在指针下一位,当获得整个终局后,调用文件流输入函数,将其写道文件。通过这个方法,生成1e6个终局的时间开销从40s降低到4s~5s。
质量分析
在完成代码的性能分析后,我对代码进行了质量分析。共有两个warnings。一个是可能存在未处理的空指针,一个是可能存在数组溢出。
char *s = (char *)malloc(200);
if (s == NULL)
return 0;//分配空间不成功
通过加入对指针是否为空的判断条件,我消除了第一个warning。
if (Grid_ques[i1%9][j1%9] == k)
{
f2 = -1;
break;
}
第二个warning的产生是由于i1、j1两个变量没有限制条件,如果i1、j1大于等于9,出现溢出。因此,通过考虑实际情况,我在其中加入模运算,消除了warning。
单元测试
在单元测试中,测试了Exchange(int s[], int i, int j)、ExchangeRol(int i, int j)、Cre_End()、Full_arrange(int n, int s[])、Get_ques()、Solve(int i, int j)几个函数。
代码说明
为了实现整个功能,代码分为两部分。
首先是主函数:
int main(int argc, char* argv[])
{
t1 = clock();
Rol[0] = f_num;
int c[] = {1,2,4,5,6,7,8,9};
int N = 8;
// int r, r1;
int l0;
int suanzi = 1;
// argc = 3;
// char a[] = "-c", b[] = "1000000";
// argv[1] = a;
// argv[2] = b;
cout << argc << endl;
if (argc != 3)
{
cout << "输入格式错误" << endl;
return 0;
}
l0 = (int)strlen(argv[2]);
int l1 = l0;
if (strcmp(argv[1],"-c") == 0)
{
while (l1--)
{
if (l1 != l0 - 1)
suanzi *= 10;
limit += suanzi * (argv[2][l1] - '0');
//test module
cout << limit << endl;
if (argv[2][l1] < '0' || argv[2][l1] > '9')
{
cout << "输入格式错误" << endl;
return 0;
}
}
cout << "进入终局生成模块" << endl;
//生成终局,将结果记录到sudoku.txt中
ofile.open("sudoku.txt");
Full_arrange(N, c);
ofile.close();
}
else {
if (strcmp(argv[1], "-s") == 0)
{
cout << "进入解数独模块" << endl;
//解数独,思路,从sudoku.txt中随机读取终局,
//线索Hint,提示数Clue,提示数在23~30之间
//利用终局产生题目,利用分支限界求解
char address[100] = { 0 };
int x = 0, y = 0;
while (argv[2][y] != '\0')
{
if (argv[2][y] == '\\')
{
address[x++] = '\\';
}
address[x++] = argv[2][y++];
}
printf("%s\n", address);
ifile.open(argv[2]);
Get_ques();
Solve(0, 0);
ifile.close();
}
else cout << "输入格式错误" << endl;
}
t2 = clock();
cout << "程序运行结束, 运行时间为 " << 0.1*(t2 - t1) / 100.0 << 's' << endl;
return 1;
}
生成数独终局
首先调用Full_arrange(int n, int s[])函数,对第一行进行全排列:
int Full_arrange(int n, int s[])
{
if (jud == -1) return 0;
if (n == 1)
{
Ful_que[top++] = s[0];
for (int cas = 1; cas < 9; cas++)
{
Rol[cas] = Ful_que[cas - 1];
//printf("%d", Rol[cas]);
//生成数独终局
}
jud = Cre_End();
if (jud == -1) return 0;
Ful_que[--top] = 0;
}
else
{
for (int i = 0; i < n; i++)
{
Exchange(s, 0, i);
Ful_que[top++] = *s;
Full_arrange(n - 1, s + 1);
Ful_que[top--] = 0;
Exchange(s, 0, i);
}
}
return 0;
}
每生成一个全排列,需要进入数度终局生成模块:
int Cre_End()
{
int i, j, k;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
for (k = 0; k < 9; k++)
{
Grid[j + 3 * i][k] = Rol[(i + k + j * 3) % 9];
}
}
}
//获得一种终局,打印
char *s = (char *)malloc(200);
if (s == NULL)
return 0;//分配空间不成功
int sp = 0;
for (int fy = 0; fy < 9; fy++)
{
for (int fx = 0; fx < 8; fx++)
{
*(s + sp) = Grid[fy][fx] + '0';
sp++;
*(s + sp) = ' ';
sp++;
/// ofile << Grid[fy][fx] << " ";
// cout << Grid[fy][fx] << ' ';
}
*(s + sp) = Grid[fy][8] + '0';
sp++;
*(s + sp) = '\n';
sp++;
/// ofile << Grid[fy][8] << endl;
// cout << Grid[fy][8] << endl;
}
*(s + sp) = '\0';
ofile << s << endl;
countn += 1;
if (countn == limit) return -1;
//之后交换4-6,7-9两行
//34 35 45 67 68 78
for (int cas0 = 0; cas0 < 2; cas0++)
{
for (int cas1 = 3; cas1 < 6; cas1++)
{
for (int cas2 = 4; cas2 < 6; cas2++)
{
ExchangeRol(cas1 + 3 * cas0, cas2 + 3 * cas0);
//打印到文件
memset(s, sp, sizeof(*s));
sp = 0;
for (int fy = 0; fy < 9; fy++)
{
for (int fx = 0; fx < 8; fx++)
{
*(s + sp) = Grid[fy][fx] + '0';
sp++;
*(s + sp) = ' ';
sp++;
/// ofile << Grid[fy][fx] << " ";
// cout << Grid[fy][fx] << " ";
}
*(s + sp) = Grid[fy][8] + '0';
sp++;
*(s + sp) = '\n';
sp++;
/// ofile << Grid[fy][8] << endl;
// cout << Grid[fy][8] << endl;
}
*(s + sp) = '\0';
ofile << s << endl;
// cout << endl;
countn++;
if (countn == limit) return -1;
ExchangeRol(cas2 + 3 * cas0, cas1 + 3 * cas0);
}
}
}
//for (int i = 0; i < 9; i++) Grid[0][i] = Rol[i];
//for (i = 3; i < 12; i++) Grid[][i - 3] = Rol[i % 9];//*****
}
解数独
首先再文件中获取数独开局:
int Get_ques()
{
//运行到该行
int line = 0;
int i = 0, j = 0;
int flag = 0;
// char* a = (char *)malloc(20);
// while (!ifile.eof())
// {
// if (line == r * 10) break;
// ifile.getline(a, 1e9,'\n');
// line++;
// }
while (i != 9)
{
ifile >> flag;
if (flag >= 0 && flag <= 9)
{
Grid_ques[i][j] = flag;
j++;
}
if (j == 9)
{
i++;
j = 0;
}
}
return 0;
}
进行解数独:
void Solve(int i, int j)
{
int k = 0;
for (; i < 9; i++)
{
for (; j < 9; j++)
{
if (Grid_ques[i][j] == 0)
{
int i1 = i / 3, j1 = j / 3;
i1 = i1 * 3;
j1 = j1 * 3;
int f2 = 0;
for (k = 1; k <= 9; k++)
{
f2 = 0;
//判断该数符合条件,f2==-1不符合,继续下一轮搜索
for (int cas = 0; cas < 9; cas++)
{
if (Grid_ques[cas][j] == k || Grid_ques[i][cas] == k)
{
f2 = -1;
break;
}
}
while(1)
{
while(1)
{
if (Grid_ques[i1%9][j1%9] == k)
{
f2 = -1;
break;
}
j1++;
if (j1 % 3 == 0)
{
j1 -= 3;
break;
}
}
if (f2 == -1) break;
j1++;
if (i1 % 3 == 0) break;
}
if (f2 == 0)
{
Grid_ques[i][j] = k;
Solve(i + (j + 1) / 9, (j + 1) % 9);
}
}
return;
}
}
j = 0;
}
for (int cas3 = 0; cas3 < 9; cas3++)
{
for (int cas4 = 0; cas4 < 8; cas4++)
{
cout << Grid_ques[cas3][cas4] << ' ';
}
cout << Grid_ques[cas3][8] << endl;
}
return;
}