[回溯+堆优化]37. 解数独
题目要求:编写一个程序,通过已填充的空格来解决数独问题。数独的解法需遵循以下规则:
- 数字 1-9 在每一行只能出现一次。
- 数字 1-9 在每一列只能出现一次。
- 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
给定的数独是一个9x9的二维数组,其中空格用 '.' 表示,并且题目保证只有唯一解。
注意:题目要求直接修改输入的二维数组(board),不需要返回任何值。
我们使用回溯法(DFS)来解决,但为了优化,我们采用最小候选数策略(MRV),即每次选择当前可填数字最少的空格进行填充,以降低回溯的复杂度。
具体步骤:
- 1.初始化:创建三个辅助数据结构,分别记录每行、每列、每个3x3宫已经出现的数字。
- row_set: 长度为9的列表,每个元素是一个集合,记录第i行已出现的数字。
- col_set: 长度为9的列表,每个元素是一个集合,记录第j列已出现的数字。
- sub_box_set: 3x3的二维列表,每个元素是一个集合,记录第(i//3, j//3)宫已出现的数字。
- 遍历整个数独板,记录所有空格的位置,并初始化辅助集合(将已填入的数字加入对应的集合)。
- 我们使用一个最小堆(优先队列)来存储空格的位置,堆中的元素是(候选数个数,行索引,列索引)。这样每次我们都可以选择候选数最少的空格进行填充,以尽量减少分支。
定义递归函数dfs:
如果堆为空,说明所有空格已填完,返回True(成功)。
从堆中弹出候选数最少的空格(i, j)。
对于该空格,尝试填入1-9中不在当前行、列、宫的数字(即候选数字)。
填入一个候选数字x(更新board和三个集合)。
递归调用dfs,如果返回True,则直接返回True(成功)。
否则,撤销填入(从集合中移除x,并且注意board中这个位置要恢复为空,但因为我们后面会重新覆盖,所以可以不用立即恢复,但为了清晰,我们在回溯时也可以恢复,但这里我们采用覆盖的方式,所以不恢复board,因为下一次填入会覆盖)。
如果所有候选数字都不行,则将该空格重新加入堆中(因为候选数可能变化,我们重新计算候选数个数并入堆),然后返回False(失败)。
class Solution:def solveSudoku(self, board: List[List[str]]) -> None:"""Do not return anything, modify board in-place instead."""# 我们初始化三个集合(row_set、col_set、sub_box_set)来跟踪每行、每列和每个3x3宫格中已填入的数字。row_set = [set() for _ in range(9)]col_set = [set() for _ in range(9)]# 整个9x9的数独网格被划分为9个3x3的小宫格sub_box_set = [[set() for _ in range(3)] for _ in range(3)]empty_pos = []for i,row in enumerate(board):for j,b in enumerate(row):if b == '.':# 记录所有空格的位置empty_pos.append((i,j))else:# 如果格子不为空,则横向、纵向都要记录该格子的值x = int(b)row_set[i].add(x)col_set[j].add(x)# i // 3:计算当前单元格所在的行宫格索引(0, 1, 2)# j // 3:计算当前单元格所在的列宫格索引(0, 1, 2)# sub_box_set是一个3x3的二维数组(列表的列表)sub_box_set[i//3][j//3].add(x)# lambda函数get_candidates用于计算在数独板位置(i, j)上可以填入的候选数字的个数# row_set[i]:第i行已经包含的数字集合。# col_set[j]:第j列已经包含的数字集合。# sub_box_set[i//3][j//3]:位置(i, j)所在的3x3宫格已经包含的数字集合# row_set[i] | col_set[j] | sub_box_set[i//3][j//3]:这三个集合的并集,即位置(i, j)所在的行、列、宫格中已经出现的所有数字。# len(...):计算这个并集的元素个数,也就是在行、列、宫格中已经出现的不同数字的个数。# 9 - len(...):因为数独中每个位置可以填的数字是1到9,所以用9减去已经出现的数字个数,得到的就是当前空格(i, j)还可以填入的候选数字的个数。# 这个函数返回的是候选数字的个数,而不是具体的候选数字。在最小候选数策略(MRV)中,我们优先选择候选数字个数最少的空格进行填充,这样可以减少回溯的次数,提高效率。# 在代码中,这个函数被用来初始化一个最小堆(优先队列),堆中的每个元素是(候选数字个数, i, j),这样每次从堆中弹出的就是候选数字个数最少的空格。get_candidates = lambda i,j: 9-len(row_set[i] | col_set[j] | sub_box_set[i//3][j//3])empty_heap = [(get_candidates(i,j),i,j) for i,j in empty_pos]heapify(empty_heap)def dfs() -> bool:if not empty_heap:return True_,i,j = heappop(empty_heap)candidates = 0for x in range(1,10):if x in row_set[i] or x in col_set[j] or x in sub_box_set[i//3][j//3]:continueboard[i][j] = digits[x]row_set[i].add(x)col_set[j].add(x)sub_box_set[i//3][j//3].add(x)if dfs():return Truerow_set[i].remove(x)col_set[j].remove(x)sub_box_set[i//3][j//3].remove(x)candidates += 1heappush(empty_heap, (candidates,i,j))return Falsedfs()