回溯算法来解决数独问题

实现了一个基于回溯法的数独求解器,并且使用了位掩码来优化数字的使用情况检查,这样的实现可以提高效率。不过,存在一些可以进一步优化或改进的地方,特别是在 backtrack 函数中如何进行回溯以及使用最小剩余值 (MRV) 策略选择空单元格时的细节。

1. 初始化状态

1 row_used = [0] * 9  # 每行已使用数字的位掩码
2 col_used = [0] * 9  # 每列已使用数字的位掩码
3 box_used = [0] * 9  # 每个 3x3 宫格已使用数字的位掩码
4 empty_cells = []
  • row_usedcol_usedbox_used 是分别表示行、列、九宫格中已使用的数字的位掩码。每个元素初始化为 0,表示数字 1 到 9 没有被使用。
  • empty_cells 用来存储棋盘上所有为空的单元格位置。

2. 预处理棋盘,更新状态

复制代码
1 for i in range(9):
2     for j in range(9):
3         if board[i][j] != '.':
4             num = int(board[i][j]) - 1  # 将 1-9 转换为 0-8
5             row_used[i] |= (1 << num)
6             col_used[j] |= (1 << num)
7             box_used[(i // 3) * 3 + j // 3] |= (1 << num)
8         else:
9             empty_cells.append((i, j))  # 记录所有空单元格的位置
复制代码
  • 遍历整个棋盘。如果一个单元格不是空的(即不为 '.'),则将该数字映射为 0 到 8(例如,'1' 映射为 0,'9' 映射为 8),然后通过位操作标记该行、列、九宫格中该数字已被使用。
  • 空单元格的位置被记录到 empty_cells 列表中。

3. 排序空单元格

1 empty_cells.sort(key=lambda pos: self.get_possible_values_count(pos, row_used, col_used, box_used))
  • 通过 get_possible_values_count 方法对空单元格进行排序,优先选择候选数字最少的单元格。这种排序是根据“最小剩余值(MRV)”启发式策略,目的是减少回溯的深度。

4. 回溯函数

复制代码
 1 def backtrack(index=0):
 2     if index == len(empty_cells):  # 如果所有空单元格都填充完毕,返回 True
 3         return True
 4 
 5     i, j = empty_cells[index]
 6     possible_values = self.get_possible_values(i, j, row_used, col_used, box_used)
 7 
 8     for num in possible_values:  # 尝试每一个可能的数字
 9         num_bit = 1 << (num - 1)  # 将数字转换为位掩码
10 
11         # 检查数字是否可以放入当前单元格
12         if not (row_used[i] & num_bit) and not (col_used[j] & num_bit) and not (box_used[(i // 3) * 3 + j // 3] & num_bit):
13             # 更新状态
14             board[i][j] = str(num)  # 将数字作为字符串放入棋盘
15             row_used[i] |= num_bit
16             col_used[j] |= num_bit
17             box_used[(i // 3) * 3 + j // 3] |= num_bit
18 
19             # 递归放置下一个数字
20             if backtrack(index + 1):
21                 return True
22 
23             # 回溯:撤销更新
24             self.restore_state(i, j, num, row_used, col_used, box_used, board)
25 
26     return False  # 如果没有数字可以填入当前单元格,返回 False
复制代码
  • backtrack 是回溯算法的核心函数。它从当前空单元格开始,递归地尝试填入一个可行的数字。如果成功填充了所有空单元格,返回 True 表示数独求解成功。
  • 每次填充一个数字后,更新行、列和九宫格的状态,然后继续递归。
  • 如果遇到无法继续填充的情况,执行回溯操作,撤销当前的数字填充,并尝试下一个可能的数字。

5. 获取某个空单元格的可选数字个数

复制代码
 1 def get_possible_values_count(self, pos, row_used, col_used, box_used):
 2     """
 3     获取某个空单元格的可选数字个数
 4     """
 5     i, j = pos
 6     possible_values = 0
 7     for num in range(1, 10):  # 检查 1 到 9 的数字
 8         num_bit = 1 << (num - 1)
 9         if not (row_used[i] & num_bit) and not (col_used[j] & num_bit) and not (box_used[(i // 3) * 3 + j // 3] & num_bit):
10             possible_values += 1
11     return possible_values
复制代码
  • get_possible_values_count 方法用于计算某个空单元格可填入的数字个数。它检查该单元格所在行、列和九宫格中哪些数字已经被占用,并返回剩下可以填入的数字数量。

6. 获取某个空单元格的所有可能数字

复制代码
 1 def get_possible_values(self, i, j, row_used, col_used, box_used):
 2     """
 3     获取某个空单元格可以填入的所有数字(1-9)
 4     """
 5     possible_values = []
 6     for num in range(1, 10):  # 检查 1 到 9 的数字
 7         num_bit = 1 << (num - 1)
 8         if not (row_used[i] & num_bit) and not (col_used[j] & num_bit) and not (box_used[(i // 3) * 3 + j // 3] & num_bit):
 9             possible_values.append(num)
10     return possible_values
复制代码
  • get_possible_values 方法返回某个空单元格所有可能的数字(1-9)。它会检查该单元格所在行、列和九宫格中已填充的数字,返回那些没有被占用的数字。

7. 恢复状态

复制代码
1 def restore_state(self, i, j, num, row_used, col_used, box_used, board):
2     """
3     恢复填入数字后的状态
4     """
5     num_bit = 1 << (num - 1)
6     row_used[i] &= ~num_bit
7     col_used[j] &= ~num_bit
8     box_used[(i // 3) * 3 + j // 3] &= ~num_bit
9     board[i][j] = '.'  # 恢复单元格为空
复制代码
  • restore_state 方法在回溯过程中用于恢复之前的状态,即撤销数字填充。它通过位运算恢复行、列和九宫格中数字的使用状态,并将棋盘上的该单元格恢复为空。

测试:

你可以用如下的数独棋盘来测试代码:

复制代码
 1 board = [
 2     ["5","3",".",".","7",".",".",".","."],
 3     ["6",".",".","1","9","5",".",".","."],
 4     [".","9","8",".",".",".",".","6","."],
 5     ["8",".",".",".","6",".",".",".","3"],
 6     ["4",".",".","8",".","3",".",".","1"],
 7     ["7",".",".",".","2",".",".",".","6"],
 8     [".","6",".",".",".",".","2","8","."],
 9     [".",".",".","4","1","9",".",".","5"],
10     [".",".",".",".","8",".",".","7","9"]
11 ]
12 
13 sol = Solution()
14 sol.solveSudoku(board)
15 print(board)
复制代码

 

※以下为完整代码

复制代码
 1 class Solution:
 2     def solveSudoku(self, board: List[List[str]]) -> None:
 3         """
 4         Do not return anything, modify board in-place instead.
 5         """
 6         # 初始化行、列、九宫格的状态,使用位掩码表示已使用的数字
 7         row_used = [0] * 9  # 每行已使用数字的位掩码
 8         col_used = [0] * 9  # 每列已使用数字的位掩码
 9         box_used = [0] * 9  # 每个 3x3 宫格已使用数字的位掩码
10         empty_cells = []
11 
12         # 预处理初始棋盘并更新 row_used、col_used、box_used
13         for i in range(9):
14             for j in range(9):
15                 if board[i][j] != '.':
16                     num = int(board[i][j]) - 1  # 将 1-9 转换为 0-8
17                     row_used[i] |= (1 << num)
18                     col_used[j] |= (1 << num)
19                     box_used[(i // 3) * 3 + j // 3] |= (1 << num)
20                 else:
21                     empty_cells.append((i, j))  # 记录所有空单元格的位置
22 
23         # 按照最小剩余值(MRV)启发式排序空单元格:选择候选数字最少的单元格优先
24         empty_cells.sort(key=lambda pos: self.get_possible_values_count(pos, row_used, col_used, box_used))
25 
26         def backtrack(index=0):
27             if index == len(empty_cells):  # 如果所有空单元格都填充完毕,返回 True
28                 return True
29 
30             i, j = empty_cells[index]
31             possible_values = self.get_possible_values(i, j, row_used, col_used, box_used)
32 
33             for num in possible_values:  # 尝试每一个可能的数字
34                 num_bit = 1 << (num - 1)  # 将数字转换为位掩码
35 
36                 # 检查数字是否可以放入当前单元格
37                 if not (row_used[i] & num_bit) and not (col_used[j] & num_bit) and not (box_used[(i // 3) * 3 + j // 3] & num_bit):
38                     # 更新状态
39                     board[i][j] = str(num)  # 将数字作为字符串放入棋盘
40                     row_used[i] |= num_bit
41                     col_used[j] |= num_bit
42                     box_used[(i // 3) * 3 + j // 3] |= num_bit
43 
44                     # 递归放置下一个数字
45                     if backtrack(index + 1):
46                         return True
47 
48                     # 回溯:撤销更新
49                     self.restore_state(i, j, num, row_used, col_used, box_used, board)
50 
51             return False  # 如果没有数字可以填入当前单元格,返回 False
52 
53         # 开始回溯过程
54         backtrack()
55 
56     def get_possible_values_count(self, pos, row_used, col_used, box_used):
57         """
58         获取某个空单元格的可选数字个数
59         """
60         i, j = pos
61         possible_values = 0
62         for num in range(1, 10):  # 检查 1 到 9 的数字
63             num_bit = 1 << (num - 1)
64             if not (row_used[i] & num_bit) and not (col_used[j] & num_bit) and not (box_used[(i // 3) * 3 + j // 3] & num_bit):
65                 possible_values += 1
66         return possible_values
67 
68     def get_possible_values(self, i, j, row_used, col_used, box_used):
69         """
70         获取某个空单元格可以填入的所有数字(1-9)
71         """
72         possible_values = []
73         for num in range(1, 10):  # 检查 1 到 9 的数字
74             num_bit = 1 << (num - 1)
75             if not (row_used[i] & num_bit) and not (col_used[j] & num_bit) and not (box_used[(i // 3) * 3 + j // 3] & num_bit):
76                 possible_values.append(num)
77         return possible_values
78 
79     def restore_state(self, i, j, num, row_used, col_used, box_used, board):
80         """
81         恢复填入数字后的状态
82         """
83         num_bit = 1 << (num - 1)
84         row_used[i] &= ~num_bit
85         col_used[j] &= ~num_bit
86         box_used[(i // 3) * 3 + j // 3] &= ~num_bit
87         board[i][j] = '.'  # 恢复单元格为空
View Code
复制代码

 

posted @   梓涵VV  阅读(55)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
历史上的今天:
2012-12-25 【五子棋AI循序渐进】发布一个完整的有一定棋力的版本(含源码)
2012-12-25 C语言获取系统时间的几种方式
2012-12-25 c++ 类的继承与派生
点击右上角即可分享
微信分享提示