1. 程式人生 > >Leetcode演算法——37、求解數獨

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))