我项目的GitHub地址:https://github.com/gmj0301/gmj
前面的博客有修改,请查看!
测试
该测试主要分为单元测试和正确性测试。
单元测试
程序分为 main.cpp,create.cpp,solve.cpp,create_sudo.h,solve_sudo.h 五个文件,分别实现对外接口,生成数独终局和解数独。具体思路已在前面的博客中完成。
函数关系如下:
因为outpu(),write(),main(),create(),solve() 可以直接通过文件,命令提示符查看便没有设计。对此,我根据剩余的 9 个函数设计了单元测试。
覆盖率如下所示:
正确性测试
生成数独终局
输入命令,依次生成1,10,100,1000个数独终局。时间都没有超过 60s ,结果如下:
在网上我找到了一个检查数独终局是否有重复的可执行文件,使用后发现,没有重复。
当输入指令不对时,显示如下:
解数独
输入命令,依次解1,10,100,1000个数独题目。
时间如下,没有超过60s:
解会输出到同目录下的 solve_sudo.txt 文件中,结果也正确,显示如下:
性能分析
生成数独终局
输入-c 100000 时,结果如下图所示:
通过上面的图可以看出,输出函数调用最多,占用时间最长,该函数代码如下:
void output(int sudo[9][9], FILE* fp) //输出函数
{
jiaohan_1(snum1, c1);
jiaohan_2(snum2, c2); //判断当前两个参数表示哪两种顺序
//输出数独
for (int i = 0; i < 9; i++)
{
int k = i;
if (k >= 3 && k <= 5) k = c1[k - 3]; //如果当前是中间三行,按照顺序输出
else if (k > 5) k = c2[k - 6]; //如果当前是中间三行,按照顺序输出
for (int j = 0; j <= 8; j++) {
char x = sudo[k][j] + '0'; //转换成字符型
fputc(x, fp); //写入文件
if (j != 8) fprintf(fp, " ");
}
fprintf(fp, "\n");
}
}
经过测试,依次生成 1,10,100,1000,10000,100000,1000000 个数独终局,所需时间如下:
修改
经过思考,我把输出从一个个输出改成一行行输出。代码如下:
void output(int sudo[9][9], FILE* fp) //输出函数
{
jiaohan_1(snum1, c1);
jiaohan_2(snum2, c2); //判断当前两个参数表示哪两种顺序
char s[18];
//输出数独
for (int i = 0; i < 9; i++)
{
int k = i;
if (k >= 3 && k <= 5) k = c1[k - 3]; //如果当前是中间三行,按照顺序输出
else if (k > 5) k = c2[k - 6]; //如果当前是中间三行,按照顺序输出
for (int j = 0; j <= 8; j++) {
int kt = j * 2;
s[kt] = sudo[k][j] + '0';
s[kt + 1] = ' ';
}
s[17] = '\0';
fputs(s, fp); //写入文件,整行写入
fprintf(fp, "\n");
}
}
当生成 1000,10000,100000,1000000 个数独时,所用时间如下:
当生成 1e6 个数独时,从 42s 到只需要不到 8s 的时间,大大缩短了时间。
解数独
输入-s wert.txt 时,wert.txt 文本中有 10000 个数独题目,结果如下图所示:
通过上面的图可以看出,judge 函数和 solve_sudo 函数调用最多,占用时间最长,代码如下:
bool judge(int h, int l) {
for (int i = 0; i < 9; i++)
if (sudo[i][l] == sudo[h][l] && i != h) //如果同一列存在与当前位置相同的数字,则出错
return false;
for (int i = 0; i < 9; i++)
if (sudo[h][l] == sudo[h][i] && i != l) //如果同一行存在与当前位置相同的数字,则出错
return false;
int x = (h / 3) * 3;
int y = (l / 3) * 3;
for (int i = x; i < x + 3; i++)
for (int j = y; j < y + 3; j++)
if (sudo[h][l] == sudo[i][j] && i != h && j != l)
//如果同一九宫格存在与当前位置相同的数字,则出错
return false;
return true; //没有出现上述错误,则返回true
}
void solve_sudo(int i, int j) {
if (judge_all == 1) return;
int k, h, l, temp;
for (k = sudo[i][j] + 1; k <= 9; k++) {
sudo[i][j] = k;
if (judge(i, j)) { //当前位置可以填入数字i
h = i; //记录下当前位置旁边的下标[h,l]
l = j + 1;
temp = 0;
for (; h < 9; h++) {
for (; l < 9; l++)
if (sudo[h][l] == 0) { //如果[h,l]处数字为0
temp = 1; //temp=1标记
break;
}
if (temp == 1) break;
else l = 0;
}
if (temp == 1) solve_sudo(h, l); //[h,l]处数字为0,调用solve_sudo函数
else {
judge_all = 1; //所有位置都不为0,说明数独题目解完了
return; //返回
}
if (judge_all == 1) return; //返回
}
}
//1-9这9个数字都填完了,没有符合的数字
//说明上一个位置出的数字填错了,当前位置置0,回到上一个位置
sudo[i][j] = 0;
return;
}
经过测试,依次解 1,10,100,1000,10000,100000,1000000 个数独题目,所需时间如下:
修改
经过思考,把剪枝进行优化。代码如下:
bool judge(int h, int l) {
for (int i = 0; i < 9; i++)
if ((sudo[i][l] == sudo[h][l] && i != h) ||( sudo[h][l] == sudo[h][i] && i != l))
//如果同一行、同一列存在与当前位置相同的数字,则出错
{
return false;
}
int x = (h / 3) * 3;
int y = (l / 3) * 3;
for (int i = x; i < x + 3; i++)
for (int j = y; j < y + 3; j++)
if (sudo[h][l] == sudo[i][j] && i != h && j != l)
//如果同一九宫格存在与当前位置相同的数字,则出错
return false;
return true; //没有出现上述错误,则返回true
}
深搜时,从列的角度思考,哪列的挖空数目最少,从哪列开始搜索,这样可以减少调用 judge 函数的次数,也可以减少调用 solve_sudo 的次数,从而到达减少时间。代码如下:
int serch() //找哪列空白个数最少
{
int min = 0, first = 0;
for (int i = 0; i < 9; i++)
{
if (numl[i] != 0 && first == 0) //0个空白就说明这列不用填,略过
{
min = i;
first = 1;
}
if (numl[i] < numl[min] && numl[i] != 0 && first == 1)
min = i;
}
if (first == 0) return -1; //都是0,返回-1
else return min;
}
int mmin(int j) //求这一列第一个是0的下标
{
int res = 0;
for (int i = 0; i < 9; i++)
if (res == 0 && sudo[i][j] == 0)
return i;
return -1;
}
void solve_sudo(int i, int j) {
if (judge_all == 1) return;
int k, h;
numl[j]--; //当前列的空白个数减一
for (k = sudo[i][j] + 1; k <= 9; k++) {
sudo[i][j] = k;
if (judge(i, j)) { //当前位置可以填入数字i
if (numl[j] > 0) //当前列还有空白
solve_sudo(mmin(j), j);
else
{
h = serch();
if (h == -1)
{
judge_all = 1; //所有列的空白个数均为0,说明数独题目解完了
return; //返回
}
else solve_sudo(mmin(h), h);
}
if (judge_all == 1) return; //返回
}
}
//1-9这9个数字都填完了,没有符合的数字
//说明上一个位置出的数字填错了,当前位置置0,回到上一个位置
sudo[i][j] = 0;
numl[j]++;
return;
}
除此之外,输出时也从一个个输出变成了一行行输出。
当解 1,10,100,1000,10000,100000,1000000 个数独时,所用时间如下:
可以看到,解 1000 个数独题目没有太大变化,当解 1e4 ~ 1e6 个数独时,时间明显缩短了。优化后,解 1e4 个数独只需要 11s 左右,解 1e5 个数独只需要 2min 左右,解 1e6 个数独需要 20min 左右,是之前的一半,优化效果很明显。
修改后,单元测试部分也进行了修改,也通过了:
当然,这不是最好的优化结果,还有待提高。可以使用其他更好的剪枝方法和解方法,减少递归使用;输入输出上,也可以缩短读取和写入文本的时间。