LeetCode 37:解数独
问题定义与核心挑战
数独规则:
- 数字
1-9
在每一行、每一列、每个3×3
宫格内仅出现一次。 - 输入棋盘含空单元格(用
.
表示),需填充为合法数独。
核心挑战:
- 解空间大:直接暴力枚举所有可能会超时,需通过回溯 + 剪枝高效搜索。
- 约束检查:如何快速判断当前数字是否违反行、列、宫格的唯一性约束。
核心思路:回溯法 + 标记数组
- 标记数组:维护三个二维数组(
row
、col
、box
),分别记录行、列、宫格中数字的出现情况,快速判断约束。 - 回溯填充:遍历每个空单元格,尝试填入
1-9
,若合法则递归处理下一个单元格;若递归失败则回溯(恢复状态,尝试其他数字)。
算法步骤详解
步骤 1:初始化标记数组
row[i][num]
:第i
行是否已存在数字num
(num ∈ [1,9]
)。col[j][num]
:第j
列是否已存在数字num
。box[boxIndex][num]
:第boxIndex
个宫格是否已存在数字num
(宫格索引boxIndex = (i/3)*3 + j/3
)。
遍历初始棋盘,标记已有数字:
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') {
int num = board[i][j] - '0';
int boxIndex = (i / 3) * 3 + (j / 3); // 计算宫格索引
row[i][num] = true;
col[j][num] = true;
box[boxIndex][num] = true;
}
}
}
步骤 2:回溯函数设计
递归填充空单元格,核心逻辑:
- 寻找空单元格:遍历棋盘,找到第一个值为
.
的位置(i,j)
。 - 尝试数字
1-9
:对每个数字num
,检查row
、col
、box
是否允许填入(即对应位置为false
)。 - 递归与回溯:
- 若允许,填入
num
并更新标记数组,递归处理下一个空单元格。 - 若递归返回
true
,说明找到解,直接返回true
。 - 若递归返回
false
,回溯(恢复棋盘和标记数组),尝试下一个数字。
- 若允许,填入
- 终止条件:遍历完所有单元格(无空单元格),返回
true
(找到解)。
回溯函数代码
private boolean backtrack(char[][] board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] == '.') { // 找到空单元格
for (int num = 1; num <= 9; num++) {
int boxIndex = (i / 3) * 3 + (j / 3);
// 检查行、列、宫格是否允许填入num
if (!row[i][num] && !col[j][num] && !box[boxIndex][num]) {
// 填入数字,更新标记
board[i][j] = (char)(num + '0');
row[i][num] = true;
col[j][num] = true;
box[boxIndex][num] = true;
// 递归处理下一个单元格
if (backtrack(board)) {
return true; // 找到解,直接返回
}
// 回溯:恢复状态
board[i][j] = '.';
row[i][num] = false;
col[j][num] = false;
box[boxIndex][num] = false;
}
}
return false; // 所有数字都尝试过,无解(回溯)
}
}
}
return true; // 所有单元格填满,找到解
}
步骤 3:调用回溯函数
在 solveSudoku
方法中初始化标记数组,然后启动回溯:
public void solveSudoku(char[][] board) {
// 初始化标记数组(类成员变量,避免参数传递)
row = new boolean[9][10];
col = new boolean[9][10];
box = new boolean[9][10];
// 标记已有数字
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') {
int num = board[i][j] - '0';
int boxIndex = (i / 3) * 3 + (j / 3);
row[i][num] = true;
col[j][num] = true;
box[boxIndex][num] = true;
}
}
}
backtrack(board); // 启动回溯
}
关键逻辑解析
1. 宫格索引计算
boxIndex = (i / 3) * 3 + (j / 3)
:
i/3
和j/3
分别将行、列划分为0-2
的区间(对应宫格的行、列块)。- 例如,
i=4, j=5
→i/3=1, j/3=1
→boxIndex=1*3+1=4
(第 5 个宫格,索引从 0 开始)。
2. 标记数组的作用
通过 O(1)
时间判断数字是否冲突,避免每次检查行、列、宫格时的 O(9)
遍历,大幅提升效率。
3. 回溯的核心
- 递归深入:填入合法数字后,递归处理下一个空单元格,探索解空间。
- 回溯恢复:若递归失败,恢复棋盘和标记数组,尝试其他数字,确保不影响上层递归的状态。
完整代码(Java)
class Solution {
// 标记数组:row[i][num]、col[j][num]、box[boxIndex][num]
private boolean[][] row = new boolean[9][10];
private boolean[][] col = new boolean[9][10];
private boolean[][] box = new boolean[9][10];
public void solveSudoku(char[][] board) {
// 初始化标记数组
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') {
int num = board[i][j] - '0';
int boxIndex = (i / 3) * 3 + (j / 3);
row[i][num] = true;
col[j][num] = true;
box[boxIndex][num] = true;
}
}
}
// 回溯填充
backtrack(board);
}
private boolean backtrack(char[][] board) {
// 遍历所有单元格,寻找空位置
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] == '.') {
// 尝试填入1-9
for (int num = 1; num <= 9; num++) {
int boxIndex = (i / 3) * 3 + (j / 3);
// 检查行、列、宫格是否允许填入num
if (!row[i][num] && !col[j][num] && !box[boxIndex][num]) {
// 填入数字,更新标记
board[i][j] = (char)(num + '0');
row[i][num] = true;
col[j][num] = true;
box[boxIndex][num] = true;
// 递归处理下一个位置,若找到解则返回true
if (backtrack(board)) {
return true;
}
// 回溯:恢复状态
board[i][j] = '.';
row[i][num] = false;
col[j][num] = false;
box[boxIndex][num] = false;
}
}
// 所有数字都尝试过,无法填充,返回false(回溯)
return false;
}
}
}
// 所有单元格填满,返回true(找到解)
return true;
}
}
示例验证(以示例1为例)
输入棋盘的空单元格逐步被填充:
- 找到第一个空单元格,尝试合法数字(如
(0,2)
位置填入4
,需检查行、列、宫格无冲突)。 - 递归处理下一个空单元格,持续填充直到所有单元格填满。
- 由于题目保证唯一解,回溯过程最终会找到合法填充方式。
该方法通过 回溯 + 标记数组,高效约束解空间,确保在合理时间内找到数独的唯一解,是处理此类约束满足问题的经典方案。