项目地址
GitHub项目地址:https://github.com/lostcake/ZJQ
(这个项目的名字当时创建后改不过来了TAT……)
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
·Estimate | ·估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | ||
·Analysis | ·需求分析(包括学习新技术) | 50 | 40 |
·Design Spec | ·生成设计文档 | 90 | 60 |
·Design Review | ·设计复审(和同学审核设计文档) | 10 | 10 |
·Coding Standard | ·代码规范(为目前的开发指定合适的规范) | 30 | 20 |
·Design | ·具体设计 | 120 | 150 |
·Coding | ·具体编码 | 1500 | 1200 |
·Code Review | ·代码复审 | 120 | 120 |
·Test | ·测试(自我测试,修改代码,提交修改) | 150 | 120 |
Reporting | 报告 | ||
·Test Report | ·测试报告 | 150 | 120 |
·Size Measurement | ·计算工作量 | 30 | 20 |
·Postmortem & Process Improvement Plan | ·事后总结,并提出过程改进计划 | 60 | 90 |
合计 | 2340 | 1980 |
解题思路
最初步的想法自然是采用深度优先遍历的方法,但是因为至多需要生成100万个数独,单纯采用深度优先遍历恐怕会严重超时无法完成。但在采用优化方法前,暂且采用这个简单方法简化思路是可行的。
因此我们可以将这个项目分为三部分:输入与输入检查部分、创建数独部分和求解数独部分。
首先从输入入手。由于我的代码基础比较薄弱,在这里还多花费了一点时间。通过搜索知道了命令行程序的正确运行方法,需要使用main函数的参数argc和argv。了解了这一点就可以继续进行编写。我们需要输入的有三个参数,第一个参数是程序的地址,在程序中不必考虑;第二个参数是-c或者-s,-c表示创建数独,-s表示解数独;第三个参数在创建数独时表示需要创建的数量,在解数独时表示待解决文件的地址。
接下来是最为复杂的创建数独部分。由于这一部分有性能要求,且数据量很大,因此需要慎重处理。我们采用一种比较简单的数独生成方式:
①首先创建一个1-9的数列,例如:
(1,2,3,4,5,6,7,8,9)
在第一位固定的情况下,这种方法能够生成8!=40320个不同的数列。
②将此数列三个三个为一组,即A.(1,2,3)、B.(4,5,6)、C.(7,8,9)三部分。可以按照ABC、BCA、CAB的方式构造三排。即:
(1,2,3,4,5,6,7,8,9)
(4,5,6,7,8,9,1,2,3)
(7,8,9,1,2,3,4,5,6)
③取上述三排的每一列,三个三个为一组,例如A.(1,4,7)、B.(2,5,8)、C.(3,6,9)。接下来按照ABC、BCA、CAB的方式构造每一列。构造后的结果为:
(1,2,3,4,5,6,7,8,9)
(4,5,6,7,8,9,1,2,3)
(7,8,9,1,2,3,4,5,6)
(2,3,1,5,6,4,8,9,7)
(5,6,4,8,9,7,2,3,1)
(8,9,7,2,3,1,5,6,4)
(3,1,2,6,4,5,9,7,8)
(6,4,5,9,7,8,3,1,2)
(9,7,8,3,1,2,6,4,5)
采用这种方式只需要构造一个数列就可以构造出一个数独。用这种方式可以生成40320个不同的数独,还达不到要求。因此需要继续进行扩增。
④采用行交换的方式。1-3、4-6、7-9的行任意交换即可构造出一个不同的数独。由于首位数字固定,因此我们取2、3行交换。令九行为ABCDEFGHI,即可以有:
A(BC)(DEF)(GHI)
括号内的组相互交换,因此不同的变换方法有:
C
2
2
×
C
3
3
×
C
3
3
=
72
\mathrm{C}_2^2×\mathrm{C}_3^3×\mathrm{C}_3^3=72
C22×C33×C33=72因此通过变换我们总共可以构造出72×40320=2903040,达到了要求。因为最大数目为一百万,因此(BC)可以不必变换就可以达到数目(36×40320=1451520)。
综上,通过①到③步构建的数独再通过随机变换456行和789行就可以得到足够数目的数独。
最后是求解数独部分。首先从文档中读取数据,九行九行地取入数组。接下来开始依次寻找数组中的0,寻找能填入的数字。递归地进行这一过程,就可以找到解。最后将解输出即可。
设计实现过程
按照思路,设计实现也大致分为三部分。
1.输入与输入检查部分
这一部分可以直接写在主函数里:
①首先检查输入的参数数目,如果不是3则输出“参数数量错误”的提示;
②接下来检查第二个参数,如果不是“-c”或“-s”则输出“请使用正确的指令(‘-c’或‘-s’)”;
③如果第二个参数是“-s”,则检查第三个参数,取出第三个参数作为文件名。接下来尝试打开该文件,如果打开失败则输出“找不到该文件”的提示。否则就开始求解,转向第三部分;
④如果第二个参数是“-c”,则开始对第三个参数进行检查。一边检查一边将字符型的输入转化为整数型,如果出现非数字字符则输出“请输入数字”的提示;如果结果超出范围则输出“请输入数字N(1<=N<=1000000)”;如果没有错误则开始创建数组,转向第二部分。
2.创建数独部分
按照上面的思路编写代码。我的学号后两位是“93”,因此按照题目要求,数独第一位数字是“4”。我的想法是,首先将首位确定为4,接下来第二位随机生成一个不是4的数字,剩余七位通过循环进行深度优先遍历,以这种方式生成第一个数列。例如,生成的随机数为7时,第一个生成的数列为:
- 4,7,1,2,3,5,6,8,9
接下来就按照上面思路讲述的方法构造数独。这里我采用比较笨的方法重复写(懒得想更简便的方法了XD):
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
sdk[3][3 * i + j] = sdk[0][3 * i + (j + 1) % 3];
sdk[6][3 * i + j] = sdk[0][3 * i + (j + 2) % 3];
}
} //用来构造第一、第四和第七行
for (i = 0; i < 9; i++)
{
sdk[1][i] = sdk[0][(i + 3) % 9];
sdk[2][i] = sdk[0][(i + 6) % 9];
sdk[4][i] = sdk[3][(i + 3) % 9];
sdk[5][i] = sdk[3][(i + 6) % 9];
sdk[7][i] = sdk[6][(i + 3) % 9];
sdk[8][i] = sdk[6][(i + 6) % 9];
} //用构造好的三行构造其他部分
通过这一过程,我们就根据一个初始序列构造出了一个完整数独。接下来是变换的过程。由前所述,对每个数独都有36种不同的变换。此段代码如下:
for (i = 0; i < 6; i++)
{
for (j = 0; j < 6; j++)
{
if ((i == 1) || (i == 3) || (i == 5)) change(4, 5);
if ((i == 2) || (i == 4)) change(3, 4);
if ((j == 1) || (j == 3) || (j == 5)) change(7, 8);
if ((j == 2) || (j == 4)) change(6, 7);
//下面用于输出生成的数独并进行计数
n--;
for (s = 0; s < 9; s++)
{
for (t = 0; t < 8; t++)
{
cout << sdk[s][t] << " ";
}
cout << sdk[s][8] << endl;
}
if (n > 0) cout << endl;
else return;
}
}
change函数表示交换参数所示的两行内容。此段用i表示456行的交换过程,用j表示789行的交换过程。这里的原理为:
用A、B、C表示第4、5、6行,i表示第几种变换(j同理)。
i=0时,顺序为ABC;i=1时,顺序为ACB;i=2时,顺序为CAB;
i=3时,顺序为CBA;i=4时,顺序为BCA;i=5时,顺序为BAC;
此为全部六种情况,可见i为1、3、5时,交换二三位的内容;i为2、4时,交换一二位的内容。
通过这种交换,我们得到了全部36种变换。变换结束后再次进入深度优先遍历部分,直到达到数量要求。
3.求解数独部分
由主函数调用solve函数,在solve函数中,九行九行地将数字存入数组,存入数组后开始求解。数独需要保证每行、每列、每个九宫格内都没有重复的数字,因此我写了三个简单的函数judge_row、judge_column和judge_box来进行判断,用judge函数把三个函数结合到一起,三个函数全为真时则返回真,否则返回假。
新建一个布尔型函数fill,用来填0,fill有一个参数m。m表示这是数独中的第m位(1≤m≤81),从0开始到达81后则表示填入成功,返回true。此段代码如下:
bool fill(int m)
{
int i, x, y; //x为第m个元素的行数,y为列数
if (m >= 81) return true;
x = m / 9;
y = m % 9;
if (sdk[x][y] == 0)
{
for (i = 1; i <= 9; i++)
{
if (judge(x, y, i))
{
sdk[x][y] = i;
return fill(m + 1);
}
}
}
else return fill(m + 1);
return false;
}
sdk数组中存储的即是数独数组,x和y分别是第m个元素所在的行数和列数,接下来察看该元素是否为0,如果不是0则递归地查找下一个元素;如果是0则循环地查找里面所能填写的数字,填写好后再递归地查找下一个元素。这是一个简单的DFS过程。
填写成功后回到solve函数,将数组中的元素输出到文件内。全部求解结束之后,在命令行上输出“求解完毕”的字样。
性能优化
因为本人比较懒,所有的函数都写在了一个文件里。再加上我能力有限,只能写出这种程度的代码;时间也有限,无法完成更细致的优化。另外我不太会使用性能测试工具,只能直观地以代码运行时间为判断依据。
通过上面的代码写出的程序运行时速度比较慢,生成一百万个数独需要大概一分半钟左右的时间。通过同学之间的讨论发现极有可能是输出的问题,原始代码是使用C++的cout进行输出的,每产生一个数独就进行一次输出。这样大大减缓了程序的运行速度。
针对此进行改进,创建一个非常巨大的buffer字符型数组,然后将每次产生的数独都写入这个数组,同时数组中也写出空格和换行。所有数独生成结束之后,使用fputs函数进行输出。
令N=10000进行测试,使用clock函数,最后输出运行的时间。优化前需要3359毫秒才能运行完毕,而优化后只需要14毫秒,效果显著。
实验总结
虽然整个程序并不长,实验也相对简单,难度并不大。但是毕竟是自己独立完成,中途还是遇到了很多困难,也积累了很多经验。
没有使用面向对象的方式进行编程,主要还是因为自己对面向对象的内容实在不熟悉,只是知道很多原理性的内容,没有真正实践过。再加上时间有限,只能采用我最熟悉的方式进行编程。所有的函数也写在了同一个文件里,所幸代码不长,还比较清晰。
实验过程中发现了自己的很多不足,甚至对C++还有很多不熟悉的地方,尤其是文件操作和控制台的部分,查阅了很多资料,还好最后能够完成,得到了一个算是自己能够满意的结果吧。