解数独

标签: 数组 哈希表 回溯 矩阵

难度: Hard

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

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

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

Submission

运行时间: 528 ms

内存: 15.1 MB

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        def valid(i, j, val):
            for n in range(9):
                if board[n][j] == val:
                    return False
                if board[i][n] == val:
                    return False
                if board[i//3*3 + n//3][j//3*3+n%3] == val:
                    return False
            return True


        def backtrack(i, j):
            if j == 9:
                return backtrack(i+1, 0)
            if i == 9:
                return True
            if board[i][j] != '.':
                return backtrack(i, j+1)

            for val in range(1, 10):
                val = str(val)
                if not valid(i, j, val):
                    continue
                board[i][j] = val
                if backtrack(i, j+1):
                    return True
                board[i][j] = '.'
        
        backtrack(0, 0)

Explain

这个题解使用回溯法来解决数独问题。回溯法是一种通用的算法思路,适合由多个步骤组成的问题,并且每个步骤都有多个选项。通过尝试每一个选项,可以一步步构建问题的解,当某个步骤无法满足要求时,就回溯到上一个步骤,尝试其他选项。对于数独问题,从第一行第一列开始,尝试填入数字1-9。对于每个空格,先判断当前数字是否合法,如果合法就填入,然后递归地去填下一个空格,直到填完整个数独。如果中途发现无解,就回溯到上一个空格,尝试其他数字。

时间复杂度: O(9^(n^2))

空间复杂度: O(n^2)

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        def valid(i, j, val):
            # 检查行
            for n in range(9):
                if board[n][j] == val:
                    return False
            # 检查列  
            for n in range(9):
                if board[i][n] == val:
                    return False
            # 检查3*3宫
            for n in range(9):
                if board[i//3*3 + n//3][j//3*3+n%3] == val:
                    return False
            return True

        def backtrack(i, j):
            # 如果列已经填完,就换到下一行
            if j == 9:
                return backtrack(i+1, 0)
            # 如果所有的行和列都填完,说明找到了一个解    
            if i == 9:
                return True
            # 如果当前位置已有数字,就跳过 
            if board[i][j] != '.':
                return backtrack(i, j+1)

            # 尝试填入数字1-9
            for val in range(1, 10):
                val = str(val)
                # 如果不合法,就跳过
                if not valid(i, j, val):
                    continue
                # 填入数字  
                board[i][j] = val
                # 递归地填写下一个位置
                if backtrack(i, j+1):
                    return True
                # 无解的话,恢复当前位置为空格
                board[i][j] = '.'
        
        backtrack(0, 0)

Explore

数独问题的本质是一个约束满足问题,其中每个数字的填充必须满足行、列以及3x3宫格的约束。回溯法适用于这种类型的问题,因为它可以通过试探和错误来找到问题的解。相比之下,动态规划适用于有最优子结构和重叠子问题的情况,而数独的每一步选择都依赖于整个盘面的状态,没有明显的最优子结构;贪心算法则是每一步都做出当下最优的选择,但对于数独来说,前面的选择直接影响后面的选择,一旦选择错误,整个数独就无法完成,因此贪心算法不适用。回溯法能够在每次选择失败时回退到上一步,是解决数独这种类型问题的理想选择。

可以通过维护额外的数据结构来优化性能,例如使用三个数组分别记录每行、每列以及每个3x3宫格中哪些数字已经被使用。这样,在验证数字是否可以放在某个位置时,只需查看相关数组的状态,而不必每次都遍历对应的行、列或宫格。这种方法可以显著减少验证过程中的重复操作,提高算法的效率。

在`backtrack`函数中,当递归填充到最后一个空格之后,下一个递归调用的`i`参数将会是9(因为数独的索引是从0到8),这时表示所有的行已经被成功填充。若`backtrack`能够到达这一步,即i等于9时,就意味着找到了一个有效解,这时函数返回True。这个返回值向上递归传递,直到整个数独被成功解决。因此,特定的标志就是递归函数能够顺利到达i等于9的情况。

根据数独的规则,一个有效的数字填充必须满足同一行、同一列以及所在的3x3宫内没有重复数字。这三种检查针对的是不同的约束条件,不存在重叠的可能性。行检查确保没有行内重复,列检查确保没有列内重复,而3x3宫检查则确保在每个宫内没有重复。这三个条件是并列的,缺一不可,因此需要分别进行检查以确保数字的合法性。