回溯算法来解决数独问题
实现了一个基于回溯法的数独求解器,并且使用了位掩码来优化数字的使用情况检查,这样的实现可以提高效率。不过,存在一些可以进一步优化或改进的地方,特别是在 backtrack
函数中如何进行回溯以及使用最小剩余值 (MRV) 策略选择空单元格时的细节。
1. 初始化状态
1 row_used = [0] * 9 # 每行已使用数字的位掩码 2 col_used = [0] * 9 # 每列已使用数字的位掩码 3 box_used = [0] * 9 # 每个 3x3 宫格已使用数字的位掩码 4 empty_cells = []
row_used
,col_used
,box_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] = '.' # 恢复单元格为空
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 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++ 类的继承与派生