37. 解数独

49 阅读5分钟

【题目】

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

  示例 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] 是一位数字或者 '.'
  • 题目数据 保证 输入数独仅有一个解

【题目解析】

思路

解决数独问题的常用方法是使用深度优先搜索(DFS)结合回溯算法。算法的基本思路是:

  1. 预处理:首先遍历整个数独板,记录下所有空白格的位置,以及每行、每列和每个宫格中已经出现的数字。
  2. DFS与回溯:从第一个空白格开始,尝试填充1到9中尚未在当前行、列和宫格出现的数字,然后基于当前的填充继续对下一个空白格进行DFS。如果发现当前的填充无法最终解决数独,就撤销上一步的填充(即回溯),尝试另一个数字。
  3. 有效性检查:在每一步填充时,检查当前数字是否满足数独的条件,即当前数字是否在同行、同列或同宫格中已存在。
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(3)] for _ in range(3)]
        blanks = []  # 存储空白格位置的列表

        # 初始化数据结构,并记录空白格位置
        for r in range(9):
            for c in range(9):
                if board[r][c] == ".":
                    blanks.append((r, c))
                else:
                    val = board[r][c]
                    rows[r].add(val)
                    cols[c].add(val)
                    boxes[r // 3][c // 3].add(val)

        def backtrack(index: int) -> bool:
            # 如果所有空白格都填满了,返回True
            if index == len(blanks):
                return True
            
            r, c = blanks[index]
            # 一个小技巧:使用字符串数字集合与行、列、宫格集合的差集,来快速得到可以尝试的数字集合
            candidates = {"1", "2", "3", "4", "5", "6", "7", "8", "9"} - rows[r] - cols[c] - boxes[r // 3][c // 3]
            
            for val in candidates:
                # 做选择
                board[r][c] = val
                rows[r].add(val)
                cols[c].add(val)
                boxes[r // 3][c // 3].add(val)
                
                # 进入下一个空白格
                if backtrack(index + 1):
                    return True
                
                # 撤销选择
                board[r][c] = "."
                rows[r].remove(val)
                cols[c].remove(val)
                boxes[r // 3][c // 3].remove(val)
                
            return False

        backtrack(0)

执行

image.png

【总结】

数独问题的解法体现了深度优先搜索(DFS)和回溯算法在解决约束满足问题(CSP)中的应用。在编程和算法设计中,这种组合方法适用于一系列问题,尤其是那些需要遍历大量潜在选项并在特定约束下找到解的问题。这些问题包括但不限于:

  • 组合问题:如N皇后问题,需要在棋盘上放置多个棋子,使它们互不攻击。
  • 排列问题:需要找到一个序列的所有可能排列,如全排列问题。
  • 图遍历问题:在图中找到从起点到终点的路径,同时满足特定条件。
  • 分割问题:将集合或数字分割成满足特定条件的多个子集。

解决数独问题所用的算法是回溯算法,这是一种递归算法,它的执行过程是一个不断在选项树中进行深入和回溯的过程。在数独问题中,这个算法的关键点包括:

  1. 选择与撤销:通过在空白格中尝试不同的数字(选择),如果尝试失败,则撤销这一选择并尝试其他选项。

  2. 有效性检查:每次填入数字后,都要通过检查当前的行、列和宫来确保填入的数字满足数独的规则。

  3. 优化策略

    • 预处理:在递归之前,先收集信息,这有助于在后续步骤中减少重复的工作。
    • 剪枝:在递归过程中,一旦确定某一路径不可能导致解决方案,就立即停止继续递归该路径。
    • 空白格优先级:优先填充候选项最少的空白格,这有助于快速缩小搜索范围。

在编程实践中,理解和掌握DFS和回溯算法的使用对于高效解决问题至关重要。它们不仅可以帮助我们解决具体的问题,还可以提高我们的逻辑思维和问题分析能力。通过这些算法,我们能够构建一个系统的解决方案框架,逐步探索所有可能的解决路径,并找到满足所有约束的有效解。此外,优化算法的能力是提高算法性能和应对更复杂问题的关键,这需要我们在实践中不断尝试和学习。

题目链接

解数独