Leetcode演算法——37、求解數獨
編寫程式,來求解一個數獨問題。
一個數獨的答案必須滿足以下規則:
- 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))