LeetCode 37. 解数独
摘要
LeetCode 37 解数独题解,采用回溯法在决策树上深度优先遍历,通过行、列、九宫格三个哈希表剪枝冲突位置,递归填充空白格,时间复杂度 O(9^(空格数)),空间复杂度 O(1)(固定 9×9 棋盘)。
标签
#算法 #LeetCode #回溯 #DFS #矩阵 #剪枝 #题解
目录
-
题目描述
-
易错点
-
思路
- 决策树模型
- 剪枝条件
- 回溯流程
-
编写代码
1. 题目描述
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字
1-9在每一行只能出现一次。 - 数字
1-9在每一列只能出现一次。 - 数字
1-9在每一个以粗实线分隔的3x3宫内只能出现一次。
数独部分空格已填,空白格用 '.' 表示。
示例 1:
text
输入: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 == 9board[i].length == 9board[i][j]是一位数字或'.'- 题目数据保证输入数独仅有一个解
2. 易错点
- 递归返回值的处理:
backtrack函数必须返回boolean,一旦找到可行解立即逐层返回true,避免继续搜索。 - 九宫格索引计算:
box = Math.floor(row / 3) * 3 + Math.floor(col / 3),范围[0, 8]。 - 状态恢复:回溯时需将
board[row][col]恢复为'.',同时将三个标记数组置为false。 - 跳过已有数字:若当前位置已有数字,直接递归下一个位置,不进行尝试。
- 结束条件:
pos === 81表示所有格子处理完毕,返回true。 - 标记数组维度:行、列、九宫格各为
9×10(数字 1-9),索引使用数字值,0 位不使用。
3. 思路
决策树模型
数独的决策树每个节点对应一个空白格(按行优先顺序从 0 到 80)。每个节点的分支是 1~9 的候选数字,但只有通过剪枝条件(行、列、宫不冲突)的数字才能进入下一层。当所有空白格都被填满(pos === 81)时,得到一个合法解。
与普通回溯题不同,数独的答案唯一,因此一旦找到解即可立即返回,无需枚举所有解。
剪枝条件
在尝试放置数字 num 之前,检查:
rows[row][num]:第row行是否已有num。cols[col][num]:第col列是否已有num。boxes[box][num]:所在九宫格是否已有num。
任意为 true 则跳过该数字。
回溯流程
-
初始化标记数组:遍历棋盘,将已有数字记录到行、列、九宫格标记中。
-
定义递归函数
backtrack(pos):-
若
pos === 81,返回true(全部填满)。 -
计算当前格子坐标
row = Math.floor(pos / 9),col = pos % 9。 -
若
board[row][col] !== '.',直接递归backtrack(pos + 1)。 -
否则,尝试数字 1~9:
- 检查是否冲突,冲突则跳过。
- 放置数字,更新标记。
- 递归
backtrack(pos + 1),若返回true,立即返回true。 - 否则撤销放置(回溯),继续尝试下一个数字。
-
若所有数字尝试均失败,返回
false。
-
-
从
pos = 0开始递归。
该方法每个空格最多尝试 9 个数字,但剪枝会大幅减少实际搜索量。由于数独唯一解,找到即停。
复杂度
- 时间复杂度:最坏 O(9^m),m 为空格数,但实际剪枝后远小于此。
- 空间复杂度:O(1),固定 9×9 棋盘和常数大小的标记数组。
4. 编写代码
javascript 运行
javascript
var solveSudoku = function(board) {
const rows = Array.from({ length: 9 }, () => Array(10).fill(false));
const cols = Array.from({ length: 9 }, () => Array(10).fill(false));
const boxes = Array.from({ length: 9 }, () => Array(10).fill(false));
for (let i = 0; i < 9; i++) {
for (let j = 0; j < 9; j++) {
if (board[i][j] !== '.') {
const num = parseInt(board[i][j]);
rows[i][num] = true;
cols[j][num] = true;
const box = Math.floor(i / 3) * 3 + Math.floor(j / 3);
boxes[box][num] = true;
}
}
}
const backtrack = (pos) => {
if (pos === 81) return true;
const row = Math.floor(pos / 9);
const col = pos % 9;
const box = Math.floor(row / 3) * 3 + Math.floor(col / 3);
if (board[row][col] !== '.') {
return backtrack(pos + 1);
}
for (let num = 1; num <= 9; num++) {
if (rows[row][num] || cols[col][num] || boxes[box][num]) continue;
board[row][col] = String(num);
rows[row][num] = true;
cols[col][num] = true;
boxes[box][num] = true;
if (backtrack(pos + 1)) return true;
rows[row][num] = false;
cols[col][num] = false;
boxes[box][num] = false;
board[row][col] = '.';
}
return false;
};
backtrack(0);
};