文章摘要:
- 这篇文章详细讲解了如何解决数独问题。主要内容包括: 数独规则分析:每行、每列和每个3x3宫格内数字1-9不能重复 使用三个布尔数组(行、列、宫格)来记录数字出现情况 基于回溯算法的解决思路: 通过DFS遍历每个空格 尝试填入1-9的数字,检查合法性 合法则递归继续填下一个空格 不合法则回溯并尝试其他数字 提供了完整的Java代码实现,包括初始化状态记录和DFS解法 文章强调先完成"有效的数独"作为前置题目,并给出了详细的算法解析和代码注释,帮助理解回溯法在解决数独问题中的应用。
文章目录
一、题目解析
我们需要解决题目给出的数独问题,在解决数独的同时需要注意数独的合法条件:
- 每一行只能出现一次数字
1~9 - 每一列只能出现一次数字
1~9 - 每一个
3 x 3九宫格内只能出现一次数字1~9
如图:

二、算法原理 + 代码实现
前置题目
做本题之前,可以先去做一下本题的前置题目:36. 有效的数独,这道题目中只需要考虑数独的合法条件如何编写,而这个条件正是本题剪枝所需要的。
在前置题目中,我们只需要判断题目给出的数独是否是有效的数独即可,那么就抓住三个:行、列以及九宫格。
如何判断数独中的某一个位置所在的行、列以及九宫格中是否存在相同的数字呢?
我们可以使用哈希的策略:定义三个布尔类型的变量(数组),分别记录行、列以及九宫格中是否存在某个数字。
使用二维数组 checkRow[9][10] 和 checkCol[9][10] 分别记录当前位置所在的行和列中是否存在某个数字(第一个括号用来表示行数/列数,第二个括号用来表示数字 1~9)。
我们可以将整个数独以九宫格为单位 来划分:

这样就可以使用一个二维数组来表示每一个九宫格了,于是使用一个三维数组表示九宫格: grid[3][3][10] 记录某一个九宫格中是否存在某个数字(第一第二个括号用来表示哪一个九宫格,第三个括号表示数字 1~9)。
通过这三个布尔数组我们就可以快速地对数独进行判断了,大概思路:
- 对数独进行遍历,拿到数独中的数字
- 对数字所处的位置的行、列及九宫格进行合法性判断(利用三个数组):若不合法就返回false;若合法就将更改数组的映射状态为true,表示当前位置已经存在数字
代码实现如下:
Java
class Solution {
public boolean isValidSudoku(char[][] board) {
boolean[][] row = new boolean[9][10];
boolean[][] col = new boolean[9][10];
boolean[][][] grid = new boolean[3][3][10];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
char ch = board[i][j];
if (ch >= '1' && ch <= '9') {
// 判断是否有效
int num = ch - '0'; // 数字字符可以直接减'0'
if (row[i][num] || col[j][num] || grid[i/3][j/3][num]) {
// 都不满足,返回false
return false;
}
row[i][num] = col[j][num] = grid[i/3][j/3][num] = true;
}
}
}
return true;
}
}
决策树
做过前置题目后,我们做这道题目就方便很多了。
我们从上往下依次遍历每一行,然后在一行中依次遍历每一列,看看能将 1~9 中的哪个数填进去(依据有效的数独那题的核心思路来判断),如果可以填入,就递归看看基于这个选择的后续情况。
根据这个思路,画出的部分决策树如下:

从决策树中可以看到,我们的选择在后面可能有不合法的情况,这时候要及时告知上一层并更改选择(dfs 函数来处理)。
全局变量
本题的全局变量主要是前置题目中的那些全局变量(布尔数组),由于我们直接在数组中进行修改,因此不需要返回任何值。
为了递归时更方便,我们将题目给出的数独(board)改成全局变量。
Java
boolean[][] checkRow, checkCol; // 用于记录行和列的情况
boolean[][][] grid; // 用于记录九宫格的情况
char[][] board;
dfs 函数
函数头
本题只涉及到对数组的修改,而且我们已经提前将题目所给的数组改成全局变量,不需要将其作为参数了。
这里比较重要的是函数的返回值,这里我们需要知道填入一个数字后,基于该选择的情况是否能够解决数独,如果无法解决数独就及时更改数字(剪枝)以减少不必要的递归操作,因此将函数的返回值设置成布尔类型。
Java
boolean dfs();
函数体
我们按照先遍历行再遍历列的顺序嵌套两层循环,然后拿出二维数组(数独)中的一个字符,判断它是否是数字,如果不是数字,就看看可以填入 1~9 中的哪个数字,填入数字之后就看看基于这个数字的情况是否是合法的,如果是合法的,就返回 true;否则就返回现场。如果数字全部无法填入,就返回 false,告诉上层这个选择不可以解决;如果遍历行和遍历列的循环都已经走完了但是还未返回,说明已经没有空位置了,此时也返回 true。
Java
for (int row = 0; row < 9; row++) {
for (int col = 0; col < 9; col++) {
// 拿到二维数组某个位置的一个字符
// 判断该字符是否是数字,若不是数字,就遍历数字 1~9 并判断三个布尔数组条件是否满足
// 若都满足,就填入该数字
// 然后递归
}
}
return true;
细节问题
回溯
这里的回溯是将对应 row 和 col 位置的字符改回 .,注意也要将三个记录数独情况的布尔数组更新。
剪枝
本题的剪枝操作通过"有效的数独"这一题的核心代码实现,就是通过判断数独中某一位置的同一行、列、九宫格中的数字情况来决定是否填入数字。
递归出口
递归出口就通过 dfs 函数的返回来实现,当填入的数字已满,就已经结束递归了。
代码实现
要注意,我们解数独之前要将三个记录数独状态的布尔数组先更新一下,让它们记录下题目所给的数独的初始状态。
Java
class Solution {
boolean[][] checkRow, checkCol;
boolean[][][] grid;
char[][] board;
public void solveSudoku(char[][] givenBoard) {
board = givenBoard;
checkRow = new boolean[9][10];
checkCol = new boolean[9][10];
grid = new boolean[3][3][10];
// 记录数独初始状态
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
char ch = board[i][j];
if (ch >= '1' && ch <= '9') {
int num = ch - '0';
checkRow[i][num] = checkCol[j][num] = grid[i/3][j/3][num] = true;
}
}
}
dfs();
}
private boolean dfs() {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
char ch = board[i][j];
if (ch == '.') {
// 将数字放到该位置上
for (int num = 1; num <= 9; num++) {
if (!checkRow[i][num] && !checkCol[j][num] && !grid[i/3][j/3][num]) {
board[i][j] = (char)(num + '0');
checkRow[i][num] = checkCol[j][num] = grid[i/3][j/3][num] = true;
if (dfs()) return true; // 告诉上层该位置放该数字有效
// 若放法无效,就恢复现场
board[i][j] = '.';
checkRow[i][num] = checkCol[j][num] = grid[i/3][j/3][num] = false;
}
}
return false; // 若走到此处仍未返回,说明该位置已经没有数字能放了
}
}
}
return true; // 若走到此处仍未返回,说明数独中已经没有空位置
}
}
文章到这里就结束啦,若有错误请尽管指出~
完