相信大家都玩过数独,小学的时候玩数独,我把类似的所有类似取值带入做排除的方法都叫做“套”,意思就是一个一个试,一个一个往里“套”,这个相信大家都不陌生。数独恰恰就是这点方法,套进去,判断合理不合理,合理就选用,继续套下一个空位;不合理就换一个套,9个数字都套完了都不对,那就是上一个套进去的数字出问题了,那就再回到上一个套的数字进行更换。这可是纯天然的回溯思想啊!!
审题分析
首先,我觉得应该先从返回着和传入参数入手。
因为题目给定的输入就是一个二维的字符数组,输出也是一个二维的字符数组,那么简单来说,我们的操作就全部都在这个二维数组上,这是和之前做过的回溯法题目有点点区别的,需要注意!
其次,对比N皇后问题,N皇后的处理上是固定了每行,找到Queen正确的列数,而数独则需要确定当前空位,这个空位是行列无规律的(谁也不知道初始的数独形状),所以需要定位到具体的行和列,并进行1-9这九个自然数的分别试探,所以在循环层数上,肯定是比N皇后问题要多一层。
最后,判断合法性不言而喻,行、列、三三方框的判断方式需要我们重新确定。
代码分析
通过审题中的几个加粗部分的总结,我们已经有了大致的思路,接下来就是将思路转换成代码语言,下面先给出框架,值得注意的是,这次递归回溯函数的返回值类型为Bool,原因的话先请思考一下,其实顺着代码写下去就会发现。
class Solution {
//主方法
public void solveSudoku(char[][] board) {
}
//回溯方法
boolean backtracking(char[][] board){
}
//判断方法
boolean isValid(int row,int col,char var,char[][] board){
}
}
由于本题中所有的操作都在处理char [][] board,并且题目交代了数独的解是唯一的,那么主方法内就不用事先声明过程函数之类的操作了。我们要做的就是把判断方法和回溯过程整理好就行。
判断合法性方法
所有的东西都在当前可以处理的char [][] board中,所以只需要传入board检查。而我们操作的过程是每个空位,为了节省内存,并且顺便完成剪枝动作,顺便也就传入了当前空位的行(row)、列(col),填入元素(var)。判断行列还比较简单,得稍微注意一下怎么判断三三方框的合法性。
//判断方法
boolean isValid(int row,int col,char var,char[][] board){
//判断行
for(int i = 0;i < 9;i++){
if(board[row][i]==var){
return false;
}
}
//判断列
for(int j = 0;i < 9;j++){
if(board[j][col]==var){
return false;
}
}
//判断三三方框
//startXXX通过计算机语言的整数除法特性定位到当前空位所在方框的首元素位置
int startRow=(row/3)*3;
int startCol=(col/3)*3;
for(int i = startRow;i < startRow+3;i++){
for(int j = startCol;j < startCol+3;j++){
if(board[i][j]==var){
return false;
}
}
}
//三次判断结束
return true;
}
递归回溯
这里就该提到递归方法的返回类型为啥是Bool了。在我们的递归过程中,本质上只个地方需要跳出:
当前空位遍历1-9都不合法
又由于我们操作修改的矩阵是贯穿整个运行过程,所以我们需要自己设计出口,才不会导致程序进入无限循环(我猜我没有讲太清楚...)
//回溯方法
boolean backtracking(char[][] board){
//遍历行和列来寻找空位,只有出现'.'时才做出修改
for(int i = 0;i < 9;i++){
for(int j=0;j<9;j++){
//如果当前元素不是'.',说明这个位置已经有元素了,立即跳过
if(board[i][j]!='.'){
continue;
}
//开始进行尝试,从1-9
for(char k = '1';k <= '9';k++){
//如果当前合法,则先修改当前元素,再递归至下一层
if(isValid(i,j,k,board)){
board[i][j] = k;
//判断不断递归的结果是否能够有解
//如果有解则在递归的最后直接返回真
//如果无解,在if外回溯即可
if(backtracking(board)){
return true;
}
//将当前结果回退
board[i][j] = '.';
}
}
//如果在1-9循环都跳出,则需要返回false,以帮助上一个递归步骤进行回溯
//直接返回也避免了找不到还一直找的可能
return false;
}
}
//遍历完,没有false也应该return true,这是倒数第二个递归进行的调用
return true;
}
全部代码
class Solution {
//主方法
public void solveSudoku(char[][] board) {
backtracking(board);
}
//回溯方法
boolean backtracking(char[][] board){
//遍历行和列来寻找空位,只有出现'.'时才做出修改
for(int i = 0;i < 9;i++){
for(int j=0;j<9;j++){
//如果当前元素不是'.',说明这个位置已经有元素了,立即跳过
if(board[i][j]!='.'){
continue;
}
//开始进行尝试,从1-9
for(char k = '1';k <= '9';k++){
//如果当前合法,则先修改当前元素,再递归至下一层
if(isValid(i,j,k,board)){
board[i][j] = k;
//判断不断递归的结果是否能够有解
//如果有解则在递归的最后直接返回真
//如果无解,在if外回溯即可
if(backtracking(board)){
return true;
}
//将当前结果回退
board[i][j] = '.';
}
}
//如果在1-9循环都跳出,则需要返回false,以帮助上一个递归步骤进行回溯
//直接返回也避免了找不到还一直找的可能
return false;
}
}
//遍历完,没有false也应该return true,这是倒数第二个递归进行的调用
return true;
}
//判断方法
boolean isValid(int row,int col,char var,char[][] board){
//判断行
for(int i = 0;i < 9;i++){
if(board[row][i]==var){
return false;
}
}
//判断列
for(int j = 0;j< 9;j++){
if(board[j][col]==var){
return false;
}
}
//判断三三方框
//startXXX通过计算机语言的整数除法特性定位到当前空位所在方框的首元素位置
int startRow=(row/3)*3;
int startCol=(col/3)*3;
for(int i = startRow;i < startRow+3;i++){
for(int j = startCol;j < startCol+3;j++){
if(board[i][j]==var){
return false;
}
}
}
//三次判断结束
return true;
}
}
总结:
通过之前和今天的三道题目,应该足够把回溯法的精髓总结一遍了。
犯个懒,记在心里~~