【算法】【回溯篇】第2节:解数独问题

本期任务:介绍算法中关于回溯思想的几个经典问题

【算法】【回溯篇】第1节:八皇后问题

【算法】【回溯篇】第2节:解数独问题

【算法】【回溯篇】第3节:正则表达式问题

【算法】【回溯篇】第4节:全排列问题

【算法】【回溯篇】第5节:组合问题

【算法】【回溯篇】第6节:子集问题

【算法】【回溯篇】第7节:0-1背包问题


一、问题描述

问题来源:LeetCode 37. 解数独

编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 ‘.’ 表示。
在这里插入图片描述
一个数独。
在这里插入图片描述
答案被标成红色。

Note:

扫描二维码关注公众号,回复: 11158803 查看本文章
  • 给定的数独序列只包含数字 1-9 和字符 ‘.’ 。
  • 你可以假设给定的数独只有唯一解。
  • 给定数独永远是 9x9 形式的。

二、算法思路

1. 策略选择

  • 解数独问题是典型的“多阶段决策最优解”问题:每个空白位置都需要决策一次;最优解是满足同一行、同一列或同一九宫格内不冲突。
  • 不满足无后效性:一个空白位置填写之后,可能导致不存在可行解,即后面的状态可能影响前面状态,故不能使用动态规划。
  • 本问题也不满足贪心选择性,即无法通过局部最优的选择,能产生全局的最优选择,故不能使用贪心策略。
  • 本题使用回溯算法暴力穷举所有可能的填写方式,并通过剪枝策略进行优化。

2. 回溯算法思路

  • 暴力穷举,每一个位置都可能有1-9共9种可能,所有可能的摆放方式共有 9 n 9^n ,其中n为空白的位置数,穷举过程遵循深度优先搜索规则。
  • 维护三个9*9的二维数组srows, cols, boxes,用于保存每行、每列、每个九宫格中各个数字出现的情况。
  • 剪枝策略:利用三个二维数组,若当前位置的数字在srows, cols或boxes中出现过则跳过。
  • 结算情形:所有位置都已填写完成

注意事项:

  1. 如何计算坐标(x,y)在第几个九宫格,
box_num = int(x / 3) * 3 + int(y / 3)  
  1. 由于回溯过程对三个二维数组进行了修改,故回溯完成需要对称复原。(注意区别前一节八皇后问题中的处理。)
  2. 由于题目明确表示本题有且仅有一个解,故一旦找到符合要求的解,就停止递归,打印解即可。(注意区别前一节八皇后问题中关于return部分的处理。)

三、Python代码实现

class Solution:
    def solveSudoku(self, board):
        """
        Do not return anything, modify board in-place instead.
        """
        self.board = board
        self.size = len(self.board)

        self.rows = [[0] * self.size for _ in range(self.size)]  # 存储行约束
        self.cols = [[0] * self.size for _ in range(self.size)]  # 存储列约束
        self.boxes = [[0] * self.size for _ in range(self.size)]  # 存储九宫格约束

        self.fix_existed()  # 根据已确定的数字,更新三个约束矩阵

        self.helper(0, 0)

    def fix_existed(self):
        for x in range(self.size):
            for y in range(self.size):
                if self.board[x][y] != '.':
                    box_num = int(x / 3) * 3 + int(y / 3)  # 当前位置位于第几个九宫格
                    v = int(self.board[x][y]) - 1
                    self.rows[x][v] = 1
                    self.cols[y][v] = 1
                    self.boxes[box_num][v] = 1

    def helper(self, x=0, y=0):
        """
        使用回溯法进行数独填写,核心就是维护三个约束表
        易错点:因为本题只有一个解,所以需要提前返回,切记return部分的区别
        """

        if x == self.size:  # 已全部填写完毕, 此时进行结算
            return True

        # 下一个位置
        ny = (y + 1) % self.size
        nx = x if ny else x + 1

        if self.board[x][y] != '.':  # 当前位置数字已给定,更新三张约束表,进入下一个位置
            return self.helper(nx, ny)

        box_num = int(x / 3) * 3 + int(y / 3)  # 当前位置位于第几个九宫格
        for i in range(self.size):  # 每个位置都可以为1-9
            if self.rows[x][i] == 0 and self.cols[y][i] == 0 and self.boxes[box_num][i] == 0:
                self.rows[x][i] = 1
                self.cols[y][i] = 1
                self.boxes[box_num][i] = 1
                self.board[x][y] = str(i + 1)

                if self.helper(nx, ny):  # 题目只要求返回一个结果
                    return True

                # 由于回溯过程对三个二维数组进行了修改,故回溯完成需要对称复原
                self.board[x][y] = '.'
                self.rows[x][i] = 0
                self.cols[y][i] = 0
                self.boxes[box_num][i] = 0

        return False  # 不能省略!!!(因为可能因为某个位置填写不当导致无可行解)

    def printSudoku(self, board):
        for _ in board:
            print(_)
        print()


def main():
    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"]]

    client = Solution()
    client.solveSudoku(board)
    client.printSudoku(client.board)  # 打印当前矩阵


if __name__ == '__main__':
    main()

运行结果

['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']
原创文章 36 获赞 32 访问量 2745

猜你喜欢

转载自blog.csdn.net/weixin_43868754/article/details/105679105