任务
实现一个能生成数独终局并且求解数独问题的控制台程序
GitHub项目:https://github.com/tooS1mple/sudoku
个人项目开发PSP表格
PSP2.1 | Person Software Progress Stages | 预估耗时(min) | 实际耗时(min) |
---|---|---|---|
Planning | 计划 | 40 | 45 |
Estimate | 估算这个项目的耗时 | 1500 | 1650 |
Development | 开发 | 150 | 200 |
Analysis | 需求分析 | 20 | 20 |
Design Spec | 生成设计文档 | 30 | 30 |
Design Review | 设计复审 | 15 | 15 |
Coding Standard | 代码规范 | 15 | 15 |
Design | 具体设计 | 60 | 75 |
Coding | 具体编码 | 900 | 800 |
Code Review | 代码复审 | 30 | 30 |
Test | 测试 | 100 | 100 |
Reporting | 报告 | 200 | 200 |
Test Report | 测试报告 | 40 | 45 |
Size Measurement | 计算工作量 | 50 | 30 |
Postmortem & Process Improvement Plan | 事后总结并改进 | 60 | 60 |
total | 合计 | 1500 | 1650 |
生成终局
实现一个命令行程序,可以生成指定数量的不重复的数独终局至文件
1.在命令行中使用-c参数加数字N(1 <= N <= 1e6)控制生成数独终局的数量,例如
sudoku.exe -c 20
来生成20个终局
2.将生成的数独终局用一个文本文件的形式保存起来(假设名字叫做finality.txt),每次生成的文件要覆盖上一次生成的文件,文件格式如下,数和数之间由空格分开,终局之间空一行,行末无空格:
6 9 8 7 5 4 3 2 1
3 2 1 6 9 8 7 5 4
7 5 4 3 2 1 6 9 8
2 1 6 9 8 7 5 4 3
5 4 3 2 1 6 9 8 7
9 8 7 5 4 3 2 1 6
1 6 9 8 7 5 4 3 2
4 3 2 1 6 9 8 7 5
8 7 5 4 3 2 1 6 9
6 9 8 7 5 4 3 2 1
3 2 1 6 9 8 7 5 4
7 5 4 3 2 1 6 9 8
2 1 6 9 8 7 5 4 3
5 4 3 2 1 6 9 8 7
9 8 7 5 4 3 2 1 6
1 6 9 8 7 5 4 3 2
4 3 2 1 6 9 8 7 5
8 7 5 4 3 2 1 6 9
- 程序在处理命令行参数时,不仅能够处理正确的参数,还能够处理各种异常的情况,如
sudoku.exe -c abc
- 在生成数独矩阵时,左上角第一个数位(1 + 9) % 9 + 1 //学号为1120161819
生成数独解题思路
拿到题目我的第一个想法就是通过解数独函数来一个一个地回溯生成数独终局,在经过片刻的思考后,这种方法太过于暴力,而且有可能生成重复的终局,否定了这种办法。我从数独的性质入手:“数独的每一行,每一列不允许出现重复的数字,每一宫也不允许出现重复的数字”。这时若有一个基础数独终局,通过每一个宫内行的互相交换和列的互相交换就能生成大量的不重复的数独终局,这种想法也得到了正确的验证,以下是我的具体实现思路。
1.先写出数独的第一行,第一个数字是(1+9)% 9 + 1 = 2;
2.以第一行为基准,将第一行向右移动{3,6,1,4,7,2,5,8}行,依次形成余下的第二行至第八行,如此生成了一个合乎要求的矩阵
3.以这个矩阵为基础矩阵,将第4行到第6行进行交换,第7行到第9行进行交换,这样就能从一个基础矩阵衍生出 3! * 3! = 36个不同的矩阵
4.因为数独的第一行中只有第一个数字不变,所以第一行有8! = 40320种不同的情况,每一种情况可以衍生出一个基础数独矩阵,每一个基础数独矩阵可以衍生出36种不同的数独矩阵,所以可以生成8! * 36 = 1451520种不同的数独终局,满足题目中1e6的数目要求
代码实现过程
思路中提到了要对第一行除了第一个数字以外的其他数字进行全排列,自己写的话代码暴力繁琐,我在网上查阅资料,查到了next_permutation()函数,在了解它的用法后,决定采用next_permutation()全排列函数
1.通过下面的代码生产8!个基础终局,其中temp是数独的第一行,通过将数独的第一行向右移动{3,6,1,4,7,2,5,8}构造出一个基础数独终局,为了方便移动,move数组中存储的是移动的距离{0,3,6,1,4,7,2,5,8}
do { //对第一行进行全排列 可生成8!个基础终局
for (int i = 0; i < 9; i++) { //向右移动
for (int j = 0; j < 9; j++)
puzzle[i][j] = temp[(j - move[i] + 9) % 9];
}
/*通过交换生产不同终局的代码*/
}while (next_permutation(temp + 1, temp + 9));
/*****成功生成了一个基础终局*****/
2.每生成一个终局就打印一次,finality中存储的是所有的终局,最开始时是生成一个输出一个,后来经过性能分析后发现这样做有大量的时间耗费在打开文件,写入文件上,后来经过改进后,花在输出文件的时间上得到了大大缩减
这种方法生成的数独都是不同的,不存在相同数独的情况
do {
do {
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 9; j++) {
finality[i + 9 * sudo_num][j] = puzzle[swap[i]][j];
}
}
for (int i = 6; i < 9; i++) {
for (int j = 0; j < 9; j++) {
finality[i + 9 * sudo_num][j] = puzzle[swap[i]][j];
}
}
sudo_num++;
if (sudo_num == sudo_amount) {
return;
}
} while (next_permutation(swap + 6, swap + 9));
} while (next_permutation(swap + 3, swap + 6));
/*****对行进行交换*****/
3.在输出文件时,最开始采用的方法是ofstream函数,它的性能并不好,经过性能分析后有超过了95%的时间用在了输出上,生成只要几秒的时间,算上输出时间要总共花掉将近五分钟。
后来上网查阅资料后,发现用fputc函数更为高效,采用了这个方法之后性能得到了极大的提升
以下为经过改进后输出终局的代码
void sudo::print_generate_sudo(int n) {
FILE* fp1;
//output.open("finality.txt");
fp1 = fopen("sudoku.txt", "w");
for (int i = 0; i < n; i++) {
for (int j = 9 * i; j < 9 * i + 9; j++) {
for (int k = 0; k < 9; k++) {
fputc(finality[j][k] + '0', fp1);
//output << finality[j][k];
if (k != 8) fputc(32, fp1); //output << " ";
//else fputc(10, fp1);//output << endl;
}
if(j != 9 * i + 8)fputc(10, fp1);
}
if (i != n - 1) {
fputc(10, fp1); //终局之间换行
fputc(10, fp1);
}
}
fclose(fp1);
}
解数独解题思路
拿到题目后,决定采用回溯法解决这个问题,经过分析后得知,一个一个地对每一个空格内的数据进行试探,若符合要求,则继续递归解下一个空格,直至解决万最后一行的最后一个空格;若不符合要求,则return false,返回到上一层递归。
解数独函数返回值是布尔型的,若该函数的返回值为false,则该数独无解;若返回值为true,则该数独可解,并且puzzle数组中存的就是该数独题目的终局数独。
实现代码
通过调用solve(0,0),即可解数独
若row = 8 且 colum = 9,此时已经解完了该数独,返回true
若column = 9,此时走到了行的末尾,需要进行换行,行数+1,列数置零
若该空格处存储的不是0,即不是空格,这时直接走到下一个位置
然后从1~9逐个试探,通过judge函数判断是否可以在这里放置该数,若可以放置,则在放置该数的情况下向后递归解题,若不可以则将这个位置置零且返回false,返回到上一层继续进行其他的试探求解
/*************** 解数独函数 **************/
bool sudo::solve(int row, int column) { //解一个数独
if (row == 8 && column == 9)
return true; //结束条件
if (column == 9) {
row++;
column = 0; //行走到最后一个 走到下一列的第一个
}
if (puzzle[row][column] != 0) {
return solve(row, column + 1); //若不是空 则往右走
}
for (int i = 1; i < 10; i++) {
if (judge(row, column, i) == true) {
puzzle[row][column] = i; //若符合要求 则填入
if (solve(row, column + 1))
return true; //递归往后求解
}
}
puzzle[row][column] = 0;
return false; //无解 回溯
}
判断函数judge() :通过判断行、列、宫是否有重复数字,判断这个数字能否放置在该位置
bool sudo::judge(int x, int y, int num) {
for (int i = 0; i < 9; i++) //检验行是否有重复的
if (puzzle[x][i] == num)
return false;
for (int j = 0; j < 9; j++) //检验列是否有重复的
if (puzzle[j][y] == num)
return false;
for (int i = x / 3 * 3; i < x / 3 * 3 + 3; i++) { //检验宫是否有重复的
for (int j = y / 3 * 3; j < y / 3 * 3; j++)
if (puzzle[i][j] == num)
return false;
}
return true;
}
测试用例
生成数独
能做到对非法输入的判断和输出,生成数独的效率较高
生成1e6个不同的数独时间在优化之后有了显著生成,要10s左右
解数独
通过相对路径来读入txt文件中的数据,并且将得到的结果输出到同目录的sudoku.txt文件中,结果正确
收获与反思
通过这次个人项目,我对软件的开发工作有了更为全面的认识,开发软件不仅仅是写代码,编写代码只是在开发中的一部分,项目的开发时间分配和开发计划在开发的全程中起着非常重要的作用,代码测试同样也很重要,合适的样例能帮助对程序进行优化。同时在进行项目的开发时,也深刻认识到了自己的种种不足,还需要努力磨炼技能,能力通过这次项目得到提升。