数独 ( 三 ) ——解数独
我的地址:https://github.com/gmj0301/gmj
请翻看前面的博客,有细节上的修改。
解题思路
使用深度优先搜索方法,该过程简单。但是当求解题目数量达到很大时,所用时间很长。剪枝是很重要的一个环节。
剪枝过程
对于数独终局的每一个位置,都代表递归过程中的一层,每次在当前位置填入一个数字后,通过遍历当前行、当前列、以及当前 3 × 3 网格中的数字,就可以排除不符合条件的数字,从而达到剪枝的目的。
剪枝的过程其实已经完成了对当前数独的合法性的检查。
函数设计
整体设计实现过程如下:
函数设计如下:
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <string.h>
int sudo[9][9];
int judge_all = 0, num_1 = 0;
void write(); //把解写入数独答案文件
bool judge(int h, int l); //剪枝函数,判断当前位置是否能放该数字
void solve_sudo(int i, int j); //深度优先搜素
void solve(const char* txt); //读数独题目文件,解数独,写入
主要的函数是 judge 和 solve_sudo 这两个函数,其设计和代码如下:
judge函数
当把数字填入蓝色的空时,填入的数字,要考虑是否与黄色部分重复,重复不能填入。
机器不能像人一样,可以在填入之前就可直接判断出是否重复,需要选定一个数字填入后再判断。
判断过程有三步:
- 该数字所处行是否有重复,如果有,返回 false;
- 该数字所处列是否有重复,如果有,返回 false;
- 该数字所处九宫格是否有重复,如果有,返回 false;
在判断第 3 步时,需找到九宫格的数组下标范围。
在设计时,我的存储数独的数组下标本身比他的个数少 1,需先除以 3 确定是第几个行或列,再乘以 3 确定该九宫格的第一行,加 2 确定最后行。
如,检查下标为 [ 0, 2 ] 位置的数字,0 ÷ 3 = 0,0 × 3 = 0,0 + 2 = 2,第一个九宫格的行数由 0 到 2。
2 ÷ 3 = 0,0 × 3 = 0,0 + 2 = 2,第一个九宫格的列数由 0 到 2。
代码如下:
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][i] == sudo[h][l] && 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
}
深度优先搜索函数 solve_sudo
在当前位置分别填入数字 1 - 9 ,填入数字后,使用 judge 函数判断,如果返回值为 true,搜索找到下个没填数字的位置,调用 solve_sudo 函数。如果数字 1 - 9 依次填过后都返回false,说明此位置没有符合的数字,是上一个填入的数字出错导致的,当前位置置 0(防止被认为当前位置不需要填入数字,影响后续)后返回上一个位置。
solve_sudo 函数内部的实现过程如下:
代码如下:
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;
}
写入文件时是一个字符一个字符写入的。
代码如下:
void write()
{
FILE* fp;
if (num_1 == 0) fp = fopen("solve_sudo.txt", "w"); //当前是第一题的解,需要清空文件里面的所有内容
else fp = fopen("solve_sudo.txt", "a+"); //当前不是第一题的解,需要保存文件里面的所有内容,在其基础上再写入新的解
if (num_1 != 0) fprintf(fp, "\n"); //除了第一题,每一题都要先输出一个空格符,再输出其解,与前面的答案分开
char s[18];
int k;
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
k = j * 2;
s[k] = sudo[i][j] + '0';
s[k + 1] = ' ';
}
s[17] = '\0';
fputs(s, fp);
fprintf(fp, "\n");
}
fclose(fp);
if (num_1 == 0) num_1++; //表明这不是第一题了
}
相关测试放到下一篇博客里,请查看下一篇博客。
因为最开始更博客时,有些函数不完善,所以博客有更改。