今日主要总结一下,37. 解数独
题目描述:
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
示例 1:
输入:board = [[“5”,“3”,“.”,“.”,“7”,“.”,“.”,“.”,“.”],[“6”,“.”,“.”,“1”,“9”,“5”,“.”,“.”,“.”],[“.”,“9”,“8”,“.”,“.”,“.”,“.”,“6”,“.”],[“8”,“.”,“.”,“.”,“6”,“.”,“.”,“.”,“3”],[“4”,“.”,“.”,“8”,“.”,“3”,“.”,“.”,“1”],[“7”,“.”,“.”,“.”,“2”,“.”,“.”,“.”,“6”],[“.”,“6”,“.”,“.”,“.”,“.”,“2”,“8”,“.”],[“.”,“.”,“.”,“4”,“1”,“9”,“.”,“.”,“5”],[“.”,“.”,“.”,“.”,“8”,“.”,“.”,“7”,“9”]]
输出:[[“5”,“3”,“4”,“6”,“7”,“8”,“9”,“1”,“2”],[“6”,“7”,“2”,“1”,“9”,“5”,“3”,“4”,“8”],[“1”,“9”,“8”,“3”,“4”,“2”,“5”,“6”,“7”],[“8”,“5”,“9”,“7”,“6”,“1”,“4”,“2”,“3”],[“4”,“2”,“6”,“8”,“5”,“3”,“7”,“9”,“1”],[“7”,“1”,“3”,“9”,“2”,“4”,“8”,“5”,“6”],[“9”,“6”,“1”,“5”,“3”,“7”,“2”,“8”,“4”],[“2”,“8”,“7”,“4”,“1”,“9”,“6”,“3”,“5”],[“3”,“4”,“5”,“2”,“8”,“6”,“1”,“7”,“9”]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
提示:
board.length == 9
board[i].length == 9
board[i][j] 是一位数字或者 ‘.’
题目数据 保证 输入数独仅有一个解
大家在做这道题之前最好做一下51. N 皇后
这道题,解数独可以认为是N皇后的进阶版
或者看一下一文搞懂回溯解决经典的N皇后问题上篇我对N皇后这道题有详细的讲解
这两道题本质上都是棋盘问题,解数独和N皇后的区别也是难点,主要体现在,N皇后问题是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来来遍历列,然后一行一列确定皇后的唯一位置。
本题就不一样了,本题中棋盘的每一个位置都要放一个数字(而N皇后是一行只放一个皇后),并检查数字是否合法,解数独的树形结构要比N皇后更宽更深。
这道题完全照搬N皇后的思路是没法AC掉本题的!
"而解决问题最有效的办法是上升到一个更高的维度!!!"
实际上本题我们要升高一个维度,要做的是二维递归。
之前我们讲过的回溯算法题目,例如:77.组合(组合问题),131.分割回文串(分割问题),78.子集(子集问题),46.全排列(排列问题),以及51.N皇后(N皇后问题)其实这些题目都是一维递归。
那什么是二维递归呢,本质上就是在两层嵌套for循环下面来进行递归回溯调用
首先要注意这道题的递归函数的返回值需要是bool类型,为什么呢?
因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。而需要遍历所有情况(遍历树形结构所有分支)时需要使用void返回值类型!!!
因为这个树形结构太大了,我抽取一部分,如图所示:
从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。
那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。
正确解法
class Solution {
public:
bool isvalid(vector<vector<char>>& board, int row, int col, int val){
for(int i = 0; i < 9; i++){//判断行该数是否被使用
if(board[row][i] == val) return false;
}
for(int i = 0; i < 9; i++){//判断列该数是否被使用
if(board[i][col] == val) return false;
}
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] == val) return false;
}
}
return true;
}
bool backtracing(vector<vector<char>>& board){
for(int i = 0; i < board.size(); i++){
for(int j = 0; j < board[0].size(); j++){
if(board[i][j] == '.'){
for(char k = '1'; k <= '9'; k++){
if(isvalid(board, i, j, k)){
board[i][j] = k;
if(backtracing(board)) return true;
board[i][j] = '.';
}
}
return false;
}
}
}
return true;
}
void solveSudoku(vector<vector<char>>& board) {
backtracing(board);
}
};
细心的同学可以发现这一题为社么没有终止条件?
其实本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
不用终止条件会不会死循环?
递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了,也就是找到一个满足条件的叶子节点时,通过backtracing函数最后一个return true来终止递归)所以不需要终止条件!
注意这里backtracing函数中return false的地方,这里放return false 是有讲究的。
因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
那么会直接返回, 这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!
如果还一直停留在单层递归的逻辑中,这道题目可以让大家瞬间崩溃。
实际上本题我们要升高一个维度,要做的是二维递归。
本质上就是在两层嵌套for循环下面来进行递归回溯调用
首先要注意这道题的递归函数的返回值需要是bool类型,为什么呢?
因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到 叶子节点一条唯一路径,所以需要使用bool返回值。而需要遍历所有情况(遍历树形结构所有分支)时需要使用void返回值类型!!!
3. 其实本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
不用终止条件会不会死循环?
递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了,也就是找到一个满足条件的叶子节点时,通过backtracing函数最后一个return true来终止递归)所以不需要终止条件!
注意这里backtracing函数中return false的地方,这里放return false 是有讲究的。
因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
那么会直接返回, 这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!
欢迎大家关注本人公众号:编程复盘与思考随笔
(关注后可以免费获得本人在csdn发布的资源源码)
公众号主要记录编程和刷题时的总结复盘笔记和心得!并且分享读书、工作、生活中的一些思考感悟!