Leetcode算法——37、求解数独

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/HappyRocking/article/details/83999153

编写程序,来求解一个数独问题。

一个数独的答案必须满足以下规则:

  • 1-9的每个数字都必须在每一行中都只出现一次
  • 1-9的每个数字都必须在每一列中都只出现一次
  • 1-9的每个数字都必须在每一个3*3的小方块中都只出现一次

空格子用.表示。

思路

求解数独问题,用人脑来解决,一般思路是:
1、观察全局,根据3个数独规则,查看哪些空白只有一种可能性,直接填补上这个数。
2、然后这个填补的数,又会影响到这个数所在行、所在列和所在小方块的所有空白位置的可能性。
3、重新观察全局,继续填补只有一种可能性的空白。
4、如果每个空白都至少有两种可能性,那么只有先随机挑选一个数填补上去,看看最后是否有矛盾的地方,如果有,则说明这个数是错误的,再继续填补另外剩下的数中的一个随机数,如果能让其他所有空白都填补上,说明这个数是正确的。
5、重复1~4步,直至所有空白填补完毕。

让计算机使用程序来解决数独问题,也是可以参照这个思路。
1、计算每个空白的可能性个数,并按照从小到大的顺序排列。
2、从可能性最小的空白开始进行试探:
(1)如果可能性个数只有1,则直接填补。
(2)如果可能性个数>1,则随机选择一个数进行填补。
注意,这个空白填补之后,要更新同行同列同小方块的所有空白的可能性(可能的个数-1)
3、重复第2步,继续填补可能性最小的空白,按照同样的方法进行试探。
4、在试探的过程中,如果有一个空白,无论填补哪个数都违反唯一性条件,说明在之前某一次试探中填补了错误的数字。这时使用回溯的方法,修改上一次的填补数字,继续试探,如果上一次试探的所有数字都会令下一次试探产生矛盾,则需要修改上上次的填补数字。重复这个步骤,直至找到了真正错误的那一次试探,修改填补数字,让剩下的所有试探都可以成功。
5、重复3~4步,直至所有空白都填补完成。

这个思路,用到了回溯的思想。回溯是递归的一种,在递归的基础上,增加了递归失败之后对上一次操作的反悔操作,这样便可以对所有可能的情况进行遍历,因此肯定会遍历到正确的解法。

另外,第一步其实不按照从小到大排列,也是可以遍历到正确解法的。但是先对可能性少的进行填补,可以减少许多不必要的错误试探,提高遍历效率。

python实现

import copy

class Solution:
    
    def __init__(self):
        self.board = None
        self.possible_board = None # 可能性矩阵,存放每个格子可能的值
        self.empty_list = [] # [(i,j)],存放空缺的位置
    
    def solveSudoku(self, board):
        """
        :type board: List[List[str]]
        :rtype: void Do not return anything, modify board in-place instead.
        回溯法。
        """
        
        # 初始化
        self.board = [['.' for x in range(9)] for y in range(9)]
        self.possible_board = [[set([str(y) for y in range(1,10)]) for x in range(9)] for z in range(9)]
        self.empty_list = []
        for i in range(9):
            for j in range(9):
                if board[i][j] == '.':
                    self.empty_list.append((i, j))
                elif not self.set_value(i, j, board[i][j]):
                    return
        
        # 空缺位置排序,从可能性最少的位置开始
        self.empty_list = sorted(self.empty_list, key = lambda x : len(self.possible_board[x[0]][x[1]]))
        
        # 开始回溯
        self.backtrack(0)
        
        # 复制给board
        for i in range(9):
            for j in range(9):
                board[i][j] = self.board[i][j]

    def update_possible(self, i, j, ex_value):
        '''
        更新(i,j)位置的可能性,去除ex_value这个可能值
        '''
        # 已经是这个值了
        if self.board[i][j] == ex_value:
            return False
        
        # 本来就不可能是ex_value
        if ex_value not in self.possible_board[i][j]:
            return True
        
        # 去除可能值
        self.possible_board[i][j].remove(ex_value)
        
        # 可能性为空
        if not self.possible_board[i][j]:
            return False
        
        # 可能性为多个
        if len(self.possible_board[i][j]) > 1:
            return True
        
        # 只有一种可能性,直接赋值
        return self.set_value(i, j, list(self.possible_board[i][j])[0])
        
    def set_value(self, i, j, v):
        '''
        在(i,j)的位置上放入v
        '''
        
        # 本来就是v
        if self.board[i][j] == v:
            return True
        
        # 不可能是v
        if v not in self.possible_board[i][j]:
            return False
        
        # 赋值
        self.board[i][j] = v
        self.possible_board[i][j] = {v}
        
        # 修改同行、同列、同子块的其他位置的可能性
        for k in range(9):
            if k != i and not self.update_possible(k, j, v):
                return False
            if k != j and not self.update_possible(i, k, v):
                return False    
            sub_i = i // 3 * 3 + k // 3
            sub_j = j // 3 * 3 + k % 3
            if sub_i != i and sub_j != j and not self.update_possible(sub_i, sub_j, v):
                return False
        return True
    
    def backtrack(self, k):
        '''
        为第k个之后的所有空缺位置填补数字
        '''
        if k >= len(self.empty_list):
            return True
        i = self.empty_list[k][0]
        j = self.empty_list[k][1]
        
        # 已经有数字,则跳过
        if self.board[i][j] != '.':
            return self.backtrack(k+1)
        
        # 备份,便于回溯
        board_bak = copy.deepcopy(self.board)
        possible_board_bak = copy.deepcopy(self.possible_board)
        
        # 遍历所有可能的数字
        possible_list = list(self.possible_board[i][j])
        for v in possible_list:
            if self.set_value(i,j,v) and self.backtrack(k+1):
                # 可以设置当前值,且之后所有空缺位置也可以填充值
                return True
            # 设置失败,回溯
            self.board = board_bak
            self.possible_board = possible_board_bak
        return False
        
def output(board):
    for i in range(9):
        print(' '.join(board[i]))

if '__main__' ==  __name__:
    board = [[".",".","9","7","4","8",".",".","."],["7",".",".",".",".",".",".",".","."],[".","2",".","1",".","9",".",".","."],[".",".","7",".",".",".","2","4","."],[".","6","4",".","1",".","5","9","."],[".","9","8",".",".",".","3",".","."],[".",".",".","8",".","3",".","2","."],[".",".",".",".",".",".",".",".","6"],[".",".",".","2","7","5","9",".","."]]
    print('原始:')
    output(board)
    solution = Solution()
    solution.solveSudoku(board)
    print('答案:')
    print(output(board))
                        

猜你喜欢

转载自blog.csdn.net/HappyRocking/article/details/83999153
今日推荐