编写程序,来求解一个数独问题。
一个数独的答案必须满足以下规则:
- 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))