在问 · 算法题解

37. 解数独

困难回溯回溯 + 集合判重力扣原题 ↗

题目描述

输入一个 9 x 9 的数独盘面 board,部分格子已填了 '1' 到 '9',空格用 '.' 表示。

把所有空格填上数字,使每一行、每一列、以及 9 个不重叠的 3 x 3 小宫格里,1 到 9 都恰好各出现一次。

直接修改 board,函数不返回值,判题检查填完后的盘面。

示例 1
输入: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 == 9
  • board[i].length == 9
  • board[i][j] 是一位数字或 '.'。
  • 输入的数独一定有解,且解唯一。

解题思路:回溯 + 集合判重

先扫描一遍棋盘,用三组集合记录每行、每列、每宫已经用过的数字,并把空格按顺序收集起来。然后对第 k 个空格依次尝试 1–9:只要数字不在所在行、列、宫的集合里,就填进去并递归处理第 k+1 个空格。后面走不通时撤销这次填写,换下一个数字。所有空格都填好就说明找到了解,一路返回 True 停止搜索。

为什么正确

进入 dfs(k) 时,前 k 个空格都已填好且互不冲突;rows、cols、boxes 与棋盘上的数字始终一致。

复杂度分析

最坏时间 O(9^m),m 是空格数;实际因剪枝很快。额外空间 O(m) 递归栈加 O(81) 集合。

易错点

递归成功后要立刻返回 True,不能继续循环;否则后面的撤销会把已经填好的答案擦掉。另外,失败回退时三个集合都要删掉这个数字。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        rows = [set() for _ in range(9)]
        cols = [set() for _ in range(9)]
        boxes = [set() for _ in range(9)]
        empty = []
        for i in range(9):
            for j in range(9):
                d = board[i][j]
                if d == '.':
                    empty.append((i, j))
                else:
                    rows[i].add(d)
                    cols[j].add(d)
                    boxes[i // 3 * 3 + j // 3].add(d)

        def dfs(k):
            if k == len(empty):
                return True
            i, j = empty[k]
            b = i // 3 * 3 + j // 3
            for d in '123456789':
                if d in rows[i] or d in cols[j] or d in boxes[b]:
                    continue
                board[i][j] = d
                rows[i].add(d)
                cols[j].add(d)
                boxes[b].add(d)
                if dfs(k + 1):
                    return True
                rows[i].remove(d)
                cols[j].remove(d)
                boxes[b].remove(d)
            board[i][j] = '.'
            return False

        dfs(0)

载入编辑器,自己跑一遍 →

其他解法

主解法按固定顺序填空格,思路最直接;位运算 + 最少候选优先在每步选择最受约束的格子,剪枝更强,是解数独的常用加速手段。

解法 2:位运算 + 最少候选优先 时间 O(9^m),空间 O(m)

用 9 位整数 row[i]、col[j]、box[b] 记录各行、列、宫已用数字,某格可填数字的掩码为 ~(row|col|box) & 0x1FF。每层递归先扫描所有空格,挑出候选数最少的格子再枚举,只有一个候选的格子会被优先确定。枚举时用 mask & -mask 取最低位,回溯时用异或撤销。

最坏复杂度相同(m 为空格数),但最少候选优先能极大减少分支,难题上通常快几个数量级。位运算判重比集合查询更快,代价是代码更难读。

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        row = [0] * 9
        col = [0] * 9
        box = [0] * 9
        empty = []
        for i in range(9):
            for j in range(9):
                c = board[i][j]
                if c == '.':
                    empty.append((i, j))
                else:
                    bit = 1 << (int(c) - 1)
                    row[i] |= bit
                    col[j] |= bit
                    box[i // 3 * 3 + j // 3] |= bit

        def dfs(left):
            if left == 0:
                return True
            best, best_mask, best_cnt = -1, 0, 10
            for k, (i, j) in enumerate(empty):
                if board[i][j] != '.':
                    continue
                mask = ~(row[i] | col[j] | box[i // 3 * 3 + j // 3]) & 0x1FF
                cnt = bin(mask).count('1')
                if cnt < best_cnt:
                    best, best_mask, best_cnt = k, mask, cnt
                    if cnt <= 1:
                        break
            if best_cnt == 0:
                return False
            i, j = empty[best]
            b = i // 3 * 3 + j // 3
            mask = best_mask
            while mask:
                bit = mask & -mask
                mask ^= bit
                board[i][j] = str(bit.bit_length())
                row[i] |= bit
                col[j] |= bit
                box[b] |= bit
                if dfs(left - 1):
                    return True
                row[i] ^= bit
                col[j] ^= bit
                box[b] ^= bit
            board[i][j] = '.'
            return False

        dfs(len(empty))

回溯 专题的其他题目

查看 回溯 全部题目 →