LeetCode 37. 解数独
字数 2332 2025-10-26 10:28:42
LeetCode 37. 解数独
题目描述
请你编写一个程序,通过填充空格来解决数独问题。数独的解法需要遵循以下规则:
- 数字
1-9在每一行只能出现一次。 - 数字
1-9在每一列只能出现一次。 - 数字
1-9在每一个以粗实线分隔的3x3宫(也称为“块”或“盒子”)内只能出现一次。
题目会提供一个部分填充的9x9数独板,用二维字符数组board表示。其中,字符'.'表示空格(需要填充),字符'1'到'9'表示已有数字。
题目数据保证输入数独仅有一个解。你需要修改原二维数组board来得到这个解,不需要返回任何值。
解题过程
解数独是一个经典的、需要回溯算法(Backtracking)来解决的问题。回溯算法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是解(或者至少不是最后一个解),回溯算法会丢弃该解,并在上一步进行一些修改后再次尝试。
我们可以将解数独的过程理解为,在一个 9x9 的棋盘上,依次为每个空格尝试填入一个有效的数字,并递归地去填下一个空格。如果某个空格填任何数字都会导致冲突,就说明之前的某个选择是错误的,我们需要“回溯”到上一个空格,尝试另一个数字。
步骤一:理解问题与约束
首先,我们需要一个方法来快速判断在某个位置 (row, col) 填入某个数字 num 是否有效。有效性检查需要满足三个条件:
- 行检查:第
row行不能已经有num。 - 列检查:第
col列不能已经有num。 - 3x3 宫检查:
(row, col)所在的3x3宫内不能已经有num。
步骤二:设计回溯函数
我们将设计一个递归函数,通常命名为 backtrack 或 solve。这个函数的职责是尝试解决从当前位置开始的数独。
- 寻找下一个空格:函数首先需要找到数独板上的下一个空格(
'.')。我们可以按行优先的顺序(从左到右,从上到下)来遍历。 - 基准情况:如果整个棋盘上都没有空格了,这意味着我们已经成功填满了所有空格,找到了一个解。此时递归可以结束。
- 尝试填入数字:如果找到了一个空格
(i, j),我们就尝试将数字'1'到'9'填入这个位置。 - 有效性检查:在填入一个数字
c之前,必须用步骤一的方法检查在(i, j)填入c是否有效。 - 递归探索:如果有效,我们就将
c填入board[i][j]。然后,递归地调用backtrack函数,去尝试解决下一个空格。这个递归调用是解决问题的关键,它基于当前的选择去探索后续的可能性。 - 回溯:如果递归调用返回了,这有两种可能:
- 成功:递归调用一直进行下去,最终填满了所有空格,解决了问题。这时我们可以直接返回成功,不再进行其他尝试。
- 失败:递归调用进行到某一步时,发现基于当前在
(i, j)填入c的选择,会导致后面的某个空格无法填入任何有效数字。这时,递归调用会返回到当前层。我们必须撤销当前的选择,也就是将board[i][j]恢复为'.',然后尝试下一个可能的数字c+1。
步骤三:实现细节
- 有效性检查的优化:我们可以使用三个二维数组(或类似结构)来记录数字的出现情况,避免每次检查都遍历整行、整列和整个宫格,但这道题规模较小(9x9),直接遍历检查也是可行的。
- 确定3x3宫的索引:对于一个位置
(row, col),它属于哪个3x3宫?我们可以用公式box_index = (row // 3) * 3 + (col // 3)来计算。//是整数除法。这样,9个宫的索引就是从0到8。
步骤四:完整思路与示例
让我们用一个极简的示例来走一遍流程。假设有一个 2x2 的数独(规则类似,但宫格是 1x1),初始板子如下:
[['1', '.'],
['.', '.']]
- 我们从
(0,0)开始,它有数字1,跳过。 - 找到第一个空格
(0,1)。我们尝试填入1,但检查发现与第一行的(0,0)冲突。尝试2,有效。填入2,板子变为[['1','2'], ['.','.']]。 - 递归进入下一个空格
(1,0)。尝试1,检查:与第一列的(0,0)冲突。尝试2,检查:与第二行的(0,1)冲突?不,(0,1)是2,但不在同一行/列/宫?对于2x2,宫是1x1,所以(1,0)的宫里只有它自己。尝试3?规则是数字1-2,所以只能试1和2。都冲突了!说明在(1,0)无解。 - 递归调用失败,回溯到上一步
(0,1)。我们将(0,1)恢复为'.'。板子变回[['1','.'], ['.','.']]。 - 我们在
(0,1)尝试下一个数字。但1和2都试过了,没有其他数字了。说明基于(0,0)=1这个初始状态,在(0,1)无法找到解?这不可能,因为题目保证有解。这说明我们的初始板子可能不对,或者2x2数独本身无解。这个例子只是用来说明“回溯”的动作。
总结
解数独的回溯算法核心是:做出选择 -> 递归探索 -> 如果失败则撤销选择(回溯)。这是一个非常典型的“深度优先搜索”(DFS)过程,只不过在每一步,我们只尝试那些当前有效的选择,从而进行“剪枝”,提高效率。由于题目保证有唯一解,所以一旦找到一个解,整个递归就可以立即终止。