Leetcode 37:解数独(超详细的解法!!!)

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

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

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。

空白格用 '.' 表示。

一个数独。

答案被标成红色。

Note:

  • 给定的数独序列只包含数字 1-9 和字符 '.'
  • 你可以假设给定的数独只有唯一解。
  • 给定数独永远是 9x9 形式的。

解题思路

这个之前的问题Leetcode 51:N皇后(最详细的解法!!!)其实是一样的,都是通过回溯法来解。我们就要判断我们每次摆放数字是不是有效,我们只要进行如下三种判断即可

  • 所有行是不是满足条件
  • 所有列是不是满足条件
  • 当前的小正方型是不是满足条件

我们定义函数isValid(row, col, c)用于判断row行上是不是有字符c,判断col列上是不是有字符crowcol所在的小正方形内是不是有c

这里唯一需要注意的就是小正方形内的坐标变换非常需要技巧,参考Leetcode 36:有效的数独(超详细的解法!!!)中的处理思路,每个小方格看成(row//3,col//3),然后小方格内的每个元素坐标就是(row//3*3+i//3,col//3*3+i%3)

def isValid(self, board, row, col, c):
    for i in range(9):
        if board[i][col] == c or board[row][i] == c or\
            board[row//3*3+i//3][col//3*3+i%3] == c:
            return False
    return True

接着我们只要从到位遍历board,如果当前的位置不是"."的话,我们就一次放入"123456789"挨个尝试。

class Solution:
    def solveSudoku(self, board):
        """
        :type board: List[List[str]]
        :rtype: void Do not return anything, modify board in-place instead.
        """
        for i in range(9):
            for j in range(9):
                if board[i][j] != '.':
                    continue
                    
                for c in "123456789":
                    if self.isValid(board, i, j, c):
                        board[i][j] = c        
                        if self.solveSudoku(board):
                            return True
                        board[i][j] = '.'
                return False
        return True
                        
    def isValid(self, board, row, col, c):
        for i in range(9):
            if board[i][col] == c or board[row][i] == c or\
                board[row//3*3+i//3][col//3*3+i%3] == c:
                return False
        return True

上面这个算法对于一般的应试来说是足够了的,但是这种思路不够快。那么对于这种dfs的问题,一个比较好的策略就是开始的搜索范围最好尽量小(对于这个问题中就是尽量优先填充有较少选择的点),实际上我们在解数独问题的时候也是使用的这种策略。

那么如何确定哪些点选择较少呢?我们可以通过row[x]记录x未出现的数,通过col[y]记录y列未出现的数,通过cell[x//3][y//3]记录(x,y)对应小正方形内未出现的数,那么当前数的可选范围就是(row[x]&col[y]&cell[x//3][y//3])

那么现在要怎么处理rowcolcell的存储呢?因为要使用交集,所以很显然使用set这个结构(当然也可以使用位运算)。

from collections import defaultdict
class Solution:
    def solveSudoku(self, b: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        U = set([str(i) for i in range(1, 10)])
        row, col, cell = defaultdict(set), defaultdict(set), defaultdict(set)
        
        def get(i, j):
            return (U-row[i]) & (U-col[j]) & (U-cell[i//3,j//3])
                    
        def dfs(cur):
            if not cur:
                return True
            
            minv, x, y = 10, 0, 0
            for i in range(9): # 每次寻找最少的搜索范围
                for j in range(9):
                    if b[i][j] == '.':
                        tc = len(get(i, j))
                        if len(get(i, j)) < minv:
                            minv, x, y = tc, i, j
                            
            for i in get(x, y):
                row[x].add(i)
                col[y].add(i)
                cell[x//3,y//3].add(i)
                b[x][y] = i
                
                if dfs(cur - 1):
                    return True
                
                row[x].remove(i)
                col[y].remove(i)
                cell[x//3,y//3].remove(i)
                b[x][y] = '.'
            return False
        
        for i in range(9):#计算每行、每列、每个单元格的数字个数
            for j in range(9):
                if b[i][j] != '.':
                    row[i].add(b[i][j])
                    col[j].add(b[i][j])
                    cell[i//3,j//3].add(b[i][j])
                    
        dfs(res)

上面的代码思路很清晰,比之前的版本快了十几倍,但是依旧不是最好的写法。

我们可以将上面代码中每次寻找最少的搜索范围进行优化,可以通过一个结构存储每个'.'的可填充数。每次递归的时候查找最少的可搜索元素minv,然后将该位置删除(因为这个位置将要填充元素)。接着遍历最少的可搜索元素minv中的每个数i,删除存储结构中横纵坐标或者小方格坐标和删除位置相同的元素(因为相同的横纵坐标和小方格内不能再使用该数)。

from collections import defaultdict
class Solution:
    def solveSudoku(self, b: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        U = set([str(i) for i in range(1, 10)])
        row, col, cell, cnt = defaultdict(set), defaultdict(set), defaultdict(set), defaultdict(set)
        
        def get(i, j):
            return (U-row[i]) & (U-col[j]) & (U-cell[i//3,j//3])
                    
        def dfs(cur):
            if not cur:
                return True
            
            x, y = min(cnt, key=lambda n: len(cnt[n]))# 计算最少的搜索范围
            minv = cnt[x,y]
            del cnt[x,y]
                            
            for i in minv:
                to_update = []
                for k, v in cnt.items(): #删除其他位置与之重叠的元素
                    if i in v and (k[0] == x or k[1] == y or \
                        (x//3, y//3) == (k[0]//3, k[1]//3)):
                        v.remove(i)
                        to_update.append(k)
                b[x][y] = i
                
                if dfs(cur - 1):
                    return True
                
                b[x][y] = '.'
                for pos in to_update:
                    cnt[pos].add(i)
                
            cnt[x,y] = minv
            return False
        
        for i in range(9):
            for j in range(9):
                if b[i][j] != '.':
                    row[i].add(b[i][j])
                    col[j].add(b[i][j])
                    cell[i//3,j//3].add(b[i][j])
        
        res = 0
        for i in range(9):
            for j in range(9):
                if b[i][j] == '.':
                    cnt[i,j] = get(i, j)
                    res += 1
                    
        dfs(res)

这个代码的效率比前一个版本又快了一倍,非常优秀!。

我将该问题的其他语言版本添加到了我的GitHub Leetcode

如有问题,希望大家指出!!!

发布了706 篇原创文章 · 获赞 443 · 访问量 81万+

猜你喜欢

转载自blog.csdn.net/qq_17550379/article/details/103814382
今日推荐