数独
题目描述
数独是根据 9 × 9 9 \times 9 9×9 盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫内的数字均含 1 − 9 1 - 9 1−9 ,不重复。每一道合格的数独谜题都有且仅有唯一答案,推理方法也以此为基础,任何无解或多解的题目都是不合格的。
芬兰一位数学家号称设计出全球最难的“数独游戏”,并刊登在报纸上,让大家去挑战。
这位数学家说,他相信只有“智慧最顶尖”的人才有可能破解这个“数独之谜”。
据介绍,目前数独游戏的难度的等级有一到五级,一是入门等级,五则比较难。不过这位数学家说,他所设计的数独游戏难度等级是十一,可以说是所以数独游戏中,难度最高的等级。他还表示,他目前还没遇到解不出来的数独游戏,因此他认为“最具挑战性”的数独游戏并没有出现。
输入格式
一个未填的数独。
输出格式
填好的数独。
样例 #1
样例输入 #1
8 0 0 0 0 0 0 0 0
0 0 3 6 0 0 0 0 0
0 7 0 0 9 0 2 0 0
0 5 0 0 0 7 0 0 0
0 0 0 0 4 5 7 0 0
0 0 0 1 0 0 0 3 0
0 0 1 0 0 0 0 6 8
0 0 8 5 0 0 0 1 0
0 9 0 0 0 0 4 0 0
样例输出 #1
8 1 2 7 5 3 6 4 9
9 4 3 6 8 2 1 7 5
6 7 5 4 9 1 2 8 3
1 5 4 2 3 7 8 9 6
3 6 9 8 4 5 7 2 1
2 8 7 1 6 9 5 3 4
5 2 1 9 7 4 3 6 8
4 3 8 5 2 6 9 1 7
7 9 6 3 1 8 4 5 2
提示
2022-04-17 @farteryhr 贡献了三组 hack 数据。加入了其中两组。第三组过强(来源:https://www.dcc.fc.up.pt/~acm/sudoku.pdf),放在下边供自测。
9 0 0 8 0 0 0 0 0
0 0 0 0 0 0 5 0 0
0 0 0 0 0 0 0 0 0
0 2 0 0 1 0 0 0 3
0 1 0 0 0 0 0 6 0
0 0 0 4 0 0 0 7 0
7 0 8 6 0 0 0 0 0
0 0 0 0 3 0 1 0 0
4 0 0 0 0 0 2 0 0
输出
9 7 2 8 5 3 6 1 4
1 4 6 2 7 9 5 3 8
5 8 3 1 4 6 7 2 9
6 2 4 7 1 8 9 5 3
8 1 7 3 9 5 4 6 2
3 5 9 4 6 2 8 7 1
7 9 8 6 2 1 3 4 5
2 6 5 9 3 4 1 8 7
4 3 1 5 8 7 2 9 6
不再赘述数独游戏的规则
思路
见到这种填数题,大概率需要用到深度优先搜索,其大致思路就是暴力枚举,如果当前情况下这种填法无法满足题意,便会回溯寻找下一种情况,持续下去,一定会搜索到满足题意的情况
假设我们先填了1,然后下一个点尝试1,不成功便尝试2,一直到9,如果都不行,说明先填的1存在问题,将先填入的1改成2,这样循环下去。
由于这道题建立在数独游戏的背景之上,所以我们需要关注游戏规则。数独要求每一行,每一列,每一宫不能有同样的数字出现,可以填入的数字为1到9,那么我们可以设置四个数组
int hang[9][10]={0},
lie[9][10]={0},
pand[9][10]={0},//即数字盘,先初始化为0
gong[3][3][10] = {0};
其中,hang[x][i] = 0
代表i这个数字在第x行没有被填过,lie[y][i] = 0
代表i这个数字没有在第y列出现过
需要注意的是对于宫数的判断,我可以举一个例子,我们数盘第一行第一列的数字坐标为(0,0)
那么对于第一行第三列的数字来说,它的坐标为(0,2)
。为什么纵坐标为2而不是3呢?因为我们的起始坐标为0,对于这个点来说它属于第一宫,对应gong[0][0]
,他紧接着右边的点属于第二宫,也就是说(0,3)对应第二宫,也就是gong[0][1]
所以我们可以得到宫数组的定义和表达式
gong[x/3][y/3][i] = 0
代表着i这个数字没有在对应宫出现过。这里的x和y不会等于9,因为x是从零开始,最多等于8
既然要用到深度优先搜索,我们需要先考虑回溯的条件,我们可以确定横坐标然后搜索纵坐标。如果正好完成了整个数独,刚好填入最后一个数字的时候,也就是临界情况为x == 8 && y == 8
,我们已经确定了搜索思路,那么完成后,紧接着x和y对应的值就是x == 8 && y == 9
,这种情况下就应该直接输出整个数盘
if(x == 8 && y == 9 ){
for(int i = 0; i < 9; i++){
for(int j = 0; j < 9; j ++){
cout<<pand[i][j]<<" ";
}
cout<<endl;
}
exit(0);
}
如果我们把exit(0)换成return,那么会有一个测试点超时!
我们可以思考这个过程,在完成全部填入之前,x相对于y变化很是缓慢,那么我们就先考虑y到临界的情况,在y为8时,填入数字后,根据顺序,y的值便成了9,那么这个时候就应该在下一列的第一行填入数字,代码如下
if(y == 9){
dfs(x+1,0);
}
对于正常情况,因为在填入过程中会有回溯过程,所以我们应该判断这个点是否存在数字,如果存在,那么跳过这个点就行,如果没有,我们需要先判断填入的数字是否在行,列,宫出现过,如果出现,就跳过,如果没有,就进行正常填入,然后进行回溯。
因为这是一个暴力搜索的过程,所以我们可以从1开始填入
else{
if(pand[x][y] != 0){
dfs(x,y+1);
}
if(pand[x][y] == 0){
for(int n = 1; n <= 9; n++){
if(!hang[x][n] && !lie[y][n] && !gong[x/3][y/3][n]){
pand[x][y] = n;
hang[x][n] = 1;
lie[y][n] = 1;
gong[x/3][y/3][n] = 1;
dfs(x,y+1);
hang[x][n] = 0;
lie[y][n] = 0;
gong[x/3][y/3][n] = 0;
pand[x][y] = 0;
}
}
}
}
!hang[x][n] && !lie[y][n] && !gong[x/3][y/3][n]
这段代表n这个没有在行,列,宫出现过。
为什么要把填入数字盘的数字也抹掉呢?对应pand[x][y] = 0;
就是因为最开始说的,如果这个数字不合适,就会回溯然后寻找新的可能。换句话说,先前填入的数字不一定是正确的,如果发现填不下去,就会返回。
因为题目中给了部分数字,所以我们在main函数里面需要对这些数字进行操作
int main()
{
for(int i = 0; i < 9; i++){
for(int j = 0;j < 9;j++){
cin>>pand[i][j];
if(pand[i][j] > 0){
hang[i][pand[i][j]]=1; //pand[i][j]代表的是一个数
lie[j][pand[i][j]] = 1;
gong[i/3][j/3][pand[i][j]] = 1;
}
}
}
dfs(0,0);
return 0;
}
对题目中已经给的数字,相当于已经出现过了,让对应行列宫的值等于一即可。
完整代码:
#include<iostream>
#include<algorithm>
using namespace std;
int hang[9][10]={0},lie[9][10]={0},pand[9][10]={0},gong[3][3][10] = {0};
void dfs(int x,int y){
if(x == 8 && y == 9 ){
for(int i = 0; i < 9; i++){
for(int j = 0; j < 9; j ++){
cout<<pand[i][j]<<" ";
}
cout<<endl;
}
exit(0);
}
if(y == 9){
dfs(x+1,0);
}
else{
if(pand[x][y] != 0){
dfs(x,y+1);
}
if(pand[x][y] == 0){
for(int n = 1; n <= 9; n++){
if(!hang[x][n] && !lie[y][n] && !gong[x/3][y/3][n]){
pand[x][y] = n;
hang[x][n] = 1;
lie[y][n] = 1;
gong[x/3][y/3][n] = 1;
dfs(x,y+1);
hang[x][n] = 0;
lie[y][n] = 0;
gong[x/3][y/3][n] = 0;
pand[x][y] = 0;
}
}
}
}
}
int main()
{
for(int i = 0; i < 9; i++){
for(int j = 0;j < 9;j++){
cin>>pand[i][j];
if(pand[i][j] > 0){
hang[i][pand[i][j]]=1;
lie[j][pand[i][j]] = 1;
gong[i/3][j/3][pand[i][j]] = 1;
}
}
}
dfs(0,0);
return 0;
}
有不清楚或者错误的地方请指出