本期任务:介绍算法中关于回溯思想的几个经典问题
一、问题描述
问题来源:LeetCode 37. 解数独
编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 ‘.’ 表示。
一个数独。
答案被标成红色。
Note:
- 给定的数独序列只包含数字 1-9 和字符 ‘.’ 。
- 你可以假设给定的数独只有唯一解。
- 给定数独永远是 9x9 形式的。
二、算法思路
1. 策略选择
- 解数独问题是典型的“多阶段决策最优解”问题:每个空白位置都需要决策一次;最优解是满足同一行、同一列或同一九宫格内不冲突。
- 不满足无后效性:一个空白位置填写之后,可能导致不存在可行解,即后面的状态可能影响前面状态,故不能使用动态规划。
- 本问题也不满足贪心选择性,即无法通过局部最优的选择,能产生全局的最优选择,故不能使用贪心策略。
- 本题使用回溯算法暴力穷举所有可能的填写方式,并通过剪枝策略进行优化。
2. 回溯算法思路
- 暴力穷举,每一个位置都可能有1-9共9种可能,所有可能的摆放方式共有 ,其中n为空白的位置数,穷举过程遵循深度优先搜索规则。
- 维护三个9*9的二维数组srows, cols, boxes,用于保存每行、每列、每个九宫格中各个数字出现的情况。
- 剪枝策略:利用三个二维数组,若当前位置的数字在srows, cols或boxes中出现过则跳过。
- 结算情形:所有位置都已填写完成
注意事项:
- 如何计算坐标(x,y)在第几个九宫格,
box_num = int(x / 3) * 3 + int(y / 3)
- 由于回溯过程对三个二维数组进行了修改,故回溯完成需要对称复原。(注意区别前一节八皇后问题中的处理。)
- 由于题目明确表示本题有且仅有一个解,故一旦找到符合要求的解,就停止递归,打印解即可。(注意区别前一节八皇后问题中关于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']