一、概述
输入一个9*9的数独,输出该数独的解。注意每一个数独只有唯一解。
第一眼看到这题的时候心里有点懵,因为自己做数独都是先靠眼睛看出哪些地方的可选择数字较少,然后再一个一个试,简而言之就是智能剪枝的DFS算法。但是这个智能剪枝怎么实现呢?怎么才能发现哪里可选择的数字较少?
没法实现,经过我和室友的讨论,分析得知这个数独没法特别好的剪枝。因此只能硬着头皮用DFS了。效果其实不错。
二、分析
由于我的代码还看得过去,因此只分析我自己的代码。
考虑一下纯DFS,每个节点有9个子树,深度为81,那么要是遍历所有节点,则共有9^81条路径。这个数量有点太大了,所以必定要剪枝。剪枝以什么为标准呢?就要看行、列、3*3的小矩阵中的元素是否矛盾,矛盾的话就返回上一层。
这就是中心思想了。听上去很简单,但是要形成这样一个“分析问题→选择算法→DFS→剪枝→哈希表→优化”的思路,还是有点难的。
代码如下:
class Solution {
int subNum[9][9]=
{
{1,1,1,2,2,2,3,3,3},
{1,1,1,2,2,2,3,3,3},
{1,1,1,2,2,2,3,3,3},
{4,4,4,5,5,5,6,6,6},
{4,4,4,5,5,5,6,6,6},
{4,4,4,5,5,5,6,6,6},
{7,7,7,8,8,8,9,9,9},
{7,7,7,8,8,8,9,9,9},
{7,7,7,8,8,8,9,9,9}
};
int flag=0;
public:
void solveSudoku(vector<vector<char>>& board) {
int row[9][10]={0};
int col[9][10]={0};
int matrix[9][10]={0};
for(int i=0;i<9;++i)
for(int j=0;j<9;++j)
{
if(board[i][j]!='.')
{
row[i][board[i][j]-'0']=1;
col[j][board[i][j]-'0']=1;
matrix[subNum[i][j]-1][board[i][j]-'0']=1;
}
}
DFS(0,0,board,row,col,matrix);
}
void DFS(int i,int j,vector<vector<char>>& board,int row[9][10],int col[9][10],int matrix[9][10])
{
if(i>8)
{
flag=1;
return;
}
else if(board[i][j]!='.')
{
if(j<8)
DFS(i,j+1,board,row,col,matrix);
else if(j==8)
DFS(i+1,0,board,row,col,matrix);
}
else
{
for(int k=1;k<10&&flag==0;k++)
{
if(row[i][k]==0&&col[j][k]==0&&matrix[subNum[i][j]-1][k]==0)
{
board[i][j]=k+'0';
row[i][k]=1;
col[j][k]=1;
matrix[subNum[i][j]-1][k]=1;
if(j<8)
DFS(i,j+1,board,row,col,matrix);
else if(j==8)
DFS(i+1,0,board,row,col,matrix);
if(flag==0)
{
board[i][j] = '.';
row[i][k] = 0;
col[j][k] = 0;
matrix[subNum[i][j] - 1][k] = 0;
}
}
}
return;
}
}
};
剪枝我们用的是元素是否重复,因此我们使用三个9*10的哈希表,第一个用来标记每行有没有重复的元素,第二个用来标记每列有没有重复的元素,第三个用来标记每个小矩阵有没有重复的元素。哈希表的行就对应着一行/列/小矩阵。
另外,判断下标来判断当前元素处于第几个小矩阵太麻烦了,因此新建一个9*9的矩阵,标记出各元素位于哪个小矩阵。
接下来,既然哈希表初始化好了,我们选择遍历一次输入数独来为哈希表设置初值。
然后开始递归。
一定要注意以下事实:
对于一个递归函数,它可能用到三种变量:全局变量、参数变量以及函数内变量。
对于全局变量,递归函数对其进行了改动,则无论递归函数return不return,这个改动一直存在。即在下一层中,函数改动了全局变量的值,回到上一层,变量的值不会变成原来的;
对于参数变量,递归函数在下一层对其进行了改动,上一层的变量值不变;
对于函数内变量,也不变。
关键就在于参数变量和全局变量,若参数变量为数组,则其特性和全局变量一样。原因在于,递归的时候,每调用一次函数,就会将其参数全复制一遍,如果是正常的变量,则随便改,不会影响到上一层;如果是数组,则由于传入的是数组头的地址,对数组元素进行修改相当于传参修改,上一层变量的值也会变化。
就如同本题中的哈希表,在第一层中,哈希表的值发生了变化,进入第二层哈希表又发生了变化,如果不作处理,返回上一层的话,哈希表是不会变成原样的,体现出来的bug就是:明明本行没有数字2,数字2对应的哈希表的值为1。
因此总结起来,对于全局变量和参数变量:
如果我们需要每层递归的变量都是独立的,那么最好将其写为参数变量的形式,类似DFS中的i和j,但是如果对变量的修改没法在参数中实现,例如不是+1,而是给数组中的某几个元素赋值,那么就要用全局变量。这时全局变量在返回上层时候要对其修改使其恢复原样。
另外,由于DFS最终会找到一条或多条正确路线,找到后会return。因此,在函数中调用DFS之后的代码,在一层一层返回的时候都会调用。例如上述代码中的
board[i][j] = '.';
row[i][k] = 0;
col[j][k] = 0;
matrix[subNum[i][j] - 1][k] = 0;
这是用于将全局变量变回原值的,但是只有在剪枝的时候才用得到,如果找到正确的路线,它就不要了。因此需要另外设置一个flag,用来判断是否找到正确路线。
三、总结
一道很好的训练递归的题目。