力扣日记:【回溯算法篇】37. 解数独
日期:2023.3.8
参考:代码随想录、力扣
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] 是一位数字或者 ‘.’
- 题目数据 保证 输入数独仅有一个解
题解
cpp ver
class Solution {
private:
bool isValid(vector<vector<char>>& board, int row, int col, char c) {
// 同一行
for (int i = 0; i < 9; i++) {
if (board[row][i] == c) return false;
}
// 同一列
for (int i = 0; i < 9; i++) {
if (board[i][col] == c) return false;
}
// 确定九宫格的位置
int x = col / 3; // 是商而不是余数!!!
int y = row / 3;
for (int j = y * 3; j < y * 3 + 3; j++) {
for (int i = x * 3; i < x * 3 + 3; i++) {
if (board[j][i] == c) return false;
}
}
return true;
}
public:
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
// 返回值:只需要找到一个解即可,所以用bool
// 参数:board
bool backtracking(vector<vector<char>>& board) {
// 在当前层递归,通过两层for循环找到当前需要填数字的位置(空格)
for (int row = 0; row < 9; row++) {
for (int col = 0; col < 9; col++) {
// 找到空位置
if (board[row][col] == '.') {
// 如果为空,开始填数字,此时才是所谓的for循环横向遍历
for (char c = '1'; c <= '9'; c++) { // 啊啊啊!这里不小心写成 < '9' 直接导致不能找到答案!!!导致最后棋盘是空的
// 如果有效,则处理节点并递归
if (isValid(board, row, col, c)) {
board[row][col] = c;
// 递归(进入下一个棋盘,从头开始找空格)
// 这里用if接受返回结果,如果下一层递归遍历完整个棋盘都没有空格(则返回true),这里也直接返回
if (backtracking(board)) return true;
// 否则进行回溯
board[row][col] = '.';
}
}
// 如果9个数都不行,说明之前层的递归中放的数字有问题,则返回false
return false;
}
}
}
// 如果在当前层递归遍历完一整个棋盘都没有空格,说明填满了,返回true
return true;
}
#endif
};
复杂度
时间复杂度:
空间复杂度:
思路总结
- 本题与之前做的Q皇后,甚至组合、子集等问题,在思路上完全不一致。
- 前面的题目,在递归时是有指明递归方向的(纵向遍历),如Q皇后中,递归参数
row + 1
指明递归是往下一行遍历,组合、子集问题中递归参数startindex = i + 1
也指明递归是往下一个值遍历。 - 而本题中,进入下一层递归时,并没有指明方向,而是直接给一整个棋盘,至于如何知道当前层递归需要处理的是哪个位置,则通过最开始的两层for循环来找位置。
- (硬要说的话,可能排列问题中,在每次递归时也是直接给一整个集合,然后在下一层递归从头开始遍历(再通过
used
去重)。这种思路可能有点类似。但是本题除了通过for循环找到当前位置外,还要在当前位置再去进行一层横向遍历(遍历数字1-9),所以相比前者,多了一个维度。)
- 前面的题目,在递归时是有指明递归方向的(纵向遍历),如Q皇后中,递归参数
- 因此,每层递归的这两个for循环,是为了找到当前这层递归,是轮到哪一个位置放数字了。一个位置一个位置看是否已经有值了,如果是空的,才进行所谓的“横向遍历”(看1-9哪一个数字有效)。再如果找到了有效的数字,就进行处理节点和递归(又从头开始找哪一个位置放数字)。
- 有效数字要满足三个条件:
- 同一行不能重复
- 同一列不能重复
- 同一个九宫格不能重复(要先确定在哪个九宫格)
- 有效数字要满足三个条件:
- 至于什么时候终止,则是等最后一层递归时,发现自己遍历完棋盘上所有位置都没有空格了,就返回true表示都填满了。然后退出当前层递归,会由上一层递归捕获到其返回的true,再继续退出。因此,不需要专门的终止条件。
- 至于回溯,则是当前层递归如果试了9个数字都不行,则说明前面层的递归所放的数字是存在问题的,则返回false退出当前层递归,再由上一层递归捕获到false后进行回溯。
- 由于需要这个true和false来判断是否终止,因此返回值则需要一个bool值(而不是之前的void)。
- 用bool还是void与之前树的题目类似,也是看是需要一个结果(一条边)即可,还是要多个结果(所有边)。