python实现可视化数独求解器(附代码链接及点点讲解)
寫在前面:【學校課程要求】
設計一個數獨游戲,能自動生成初盤,也能人工設置初盤,能檢測人工設置初盤的合法性;
并編寫一個求解數獨終盤的算法。
1. 準備工作
找了不少資料,這個可視化感覺挺好看的,但是我寫完啦,就沒仔細看了(這是講解?的鏈接,里面有給 github 的地址):https://blog.csdn.net/u010751000/article/details/109610683
學習數獨的算法思想,可以參考 知乎季以安 的分享(用到了唯一侯選數法和關鍵數刪減法,感覺這兩種算法就可以解決有唯一解的數獨題目了):https://zhuanlan.zhihu.com/p/75974196
本文實現的代碼,是基于某項目改編的,遍歷的方法參考了另一個寫的挺簡練的項目(但是我找半天找不到地址了,,這兩個代碼還在,需要的戳我呀)
使用配置: windows 10,python 3.7,pycharm 2018.2,anaconda 2020.11
2. 基本知識
數獨盤面是個 9*9 的棋盤,要求利用給出的部分已知數字,基于邏輯和推理,在空白方格上填入數字 1-9,使其在每一行、每一列和每一九宮格中都出現且只出現一次。
對數獨游戲難度的設置包括兩種:根據初盤中空白方格的多少認定難度,空白方格越多,難度越大;根據求解數獨使用的方法來認定難度,求解數獨終盤使用的方法越多,難度越大。
數獨的解法有很多,其中回溯遞歸的方法簡單易懂,也較容易實現,能夠解決全部有解的數獨問題。但玩家在實際求解中很少使用遍歷的方法,一般采用“直觀法”和“候選數法”這兩大類求解思維,它們又各自包括多種不同方法:直觀法包括唯一解法、基礎摒除法、區塊摒除法、唯余解法、矩形摒除法、單元摒除法、余數測試法等;候選數法包括唯一候選數法、隱性唯一候選數法、三鏈數刪減法、隱性三鏈數刪減法、矩形頂點刪減法、三鏈列刪減法、關鍵數刪減法等。
3. 實現的功能說明
能夠根據難度自動生成初盤,也能夠人工設置初盤(在交互界面輸入;用 txt 文件導入)。
實現的數獨求解器不但可以得到求解結果(左側),還可以得到具體的求解步驟,也能實現“上一步”與“下一步”的分步查看。
如上圖所示,菜單欄分為三部分,點開后如圖。點擊“生成初盤“,可以進一步選擇生成數獨題目的難度,包括簡單、中級、困難、困難+和專家這五個等級;點擊”文件”,可以選擇保存或載入,兩種方式對應的文件都為.txt文件(包括9行,每行為該行方格對應的字符串);點擊“關于”,會彈出中間所示的信息。
主要選項卡包括左側的數獨展示界面(清空按鈕可恢復原始狀態)、右上的描述信息以及右下的具體步驟展示。
主要功能展示:可根據難度自動生成初盤,也可人工設置初盤(在交互界面輸入;用txt文件導入),選擇點擊右側的“一鍵解題”或“上一步”“下一步”來得到最終結果或分步查看。左側顯示求解結果(黑色加粗為初盤數字,橙色為求解得到的數字),右側顯示具體的求解步驟。
根據需要設置了各類提示信息:在載入文件出錯時會報錯;在當前數據為空時,不能保存為文件或求解數獨,否則會出現報錯;若人工設置的初盤存在問題,在點擊解題后報錯。當數獨題目可能不止有一個解時(之前使用的方法無法解決),會出現提示,點擊“OK”后,會在當前的解題基礎上調用回溯遍歷法,得到一個可能的解。解題成功時,也會彈出提示信息。
4. 代碼實現
本來是作為一個sudoku_solver.py文件的,但是有強迫癥,強行理解并拆成了 sudoku_solver.py、show_GUI.py、show_funcion.py,又增加了用于自動生成初盤的sudoku_creator.py(原來是直接讀取設置好的數獨棋盤)。除此之外,還有兩個.txt文件,作為信息顯示的讀入。下面是講解以及對應的代碼(項目中加了很多注釋,以下只是思路的講解和一小部分代碼),全部項目見 gitee:https://gitee.com/mxx11/sudoku
4.1 自動生成數獨初盤
這里根據空白方格的數量來劃分設置初盤的難度。由于數獨的求解過程中涉及到界面展示,生成初盤后無法調用求解過程求解。所以只能保證生成數獨初盤一定有解,但不能保證是唯一解(生成后,不再求解驗證)。整體可分為以下三步(頭兩步其實不是很理解,或許生成的基本盤數量比較少,所以需要交換?而且是怎么保證基本盤一定能生成出來的?):
生成基本盤:先生成9*9的棋盤,再從1-9中隨機選取第一個方格的數字,然后從左到右,從上到下,遍歷生成基本盤,保證1-9在每行、每列、每個九宮格中都出現且只出現一次。
# 生成基本盤 def create_base_sudo(self):# 9*9的二維矩陣,每個方格默認值為0sudo = np.zeros((9, 9), dtype=int)# 隨機生成第一個方格的數字num = random.randrange(9) + 1# 遍歷從左到右,從上到下逐個遍歷for row_index in range(9):for col_index in range(9):# 獲取該方格對應的行、列、九宮格sudo_row = sudo[row_index, :] # 獲取方格所在的行的全部方格sudo_col = sudo[:, col_index] # 獲取方格所在的列的全部方格row_start = row_index // 3 * 3 # 獲取方格所在的九宮格的全部方格col_start = col_index // 3 * 3sudo_block = sudo[row_start: row_start + 3, col_start: col_start + 3]# 如果該數字已經存在于對應的行/列/九宮格,則繼續判斷下一個候選數字,直到沒有重復while (num in sudo_row) or (num in sudo_col) or (num in sudo_block):num = num % 9 + 1sudo[row_index, col_index] = num # 賦值num = num % 9 + 1return sudo通過隨機交換得到終盤:根據觀察可以發現,在已有的數獨結果上,調換同一個九宮格內任意兩個方格所在的行/列后的結果,還是一個有效的數獨。據此,多次隨機交換行和列,可以得到一個與基本盤相差較大的終盤。
# 隨即交換生成終盤 def random_sudo(self):sudo = self.create_base_sudo()times = 50 # 交換次數for _ in range(times):# 隨機交換兩行rand_row_base = random.randrange(3) * 3 # 從0,3,6隨機取一個rand_rows = random.sample(range(3), 2) # 從0,1,2中隨機取兩個數row_1 = rand_row_base + rand_rows[0]row_2 = rand_row_base + rand_rows[1]sudo[[row_1, row_2], :] = sudo[[row_2, row_1], :]# 隨機交換兩列rand_col_base = random.randrange(3) * 3rand_cols = random.sample(range(3), 2)col_1 = rand_col_base + rand_cols[0]col_2 = rand_col_base + rand_cols[1]sudo[:, [col_1, col_2]] = sudo[:, [col_2, col_1]]return(sudo)根據難度挖去不同數量的方格:實際測試表明,空白方格的數量控制在17-67比較恰當,即最多清除64個數字,最少清除14個數字。據此將難度分為5個等級,每個等級挖去數字的數量區間不同。在挖去數字時,用0-80代指81個方格。隨機生成0-80間指定數量的數字,再計算每個隨機生成的數字指代方格的所在行和所在列,將其挖去。
# 根據難度等級擦除方格 def get_sudo_subject(self, level):sudo = self.random_sudo()subject = sudo.copy()max_clear_count = 64 # 最多清除個數min_clear_count = 14 # 最少清除個數each_level_count = (max_clear_count - min_clear_count) / 5 # 每個等級清除的個數level_start = min_clear_count + (level - 1) * each_level_count # 該等級最小數del_nums = random.randrange(level_start, level_start + each_level_count) # 該等級范圍內的隨機數# 隨機擦除(從0到80,隨機取要刪除的個數)clears = random.sample(range(81), del_nums)for clear_index in clears:# 把0到80的坐標轉化成行和列索引,避免重復刪除同一個格子的數字row_index = clear_index // 9col_index = clear_index % 9subject[row_index, col_index] = 0subject = self.change_format(subject)return subject4.2 求解數獨終盤
設置求解數獨終盤的整體過程:
如下圖所示,先根據規則要求,得到每個方格的可能取值,再循環使用唯一候選數法和區塊摒除法。若仍未解決,則進一步使用關鍵數刪減法,若嘗試10個有多個可能值的方格后,仍未得到最終解,判定方法失敗,使用回溯遍歷法得到一個可能解。圖中畫框的方法用到了全局更新,會在后面詳細說明。
設置了幾個用到的基礎函數:
用于檢查原始數據是否有效、是否得到最終解;
用于填寫空白方格的可能取值、獲取新的數獨題目。這里填寫空白方格的可能取值挺有意思,不是找行/列/九宮格值域的交集,而是遍歷1-9,看是否在行/列/九宮格中出現,這與它設置的信息交互方式有關。
循環使用唯一候選數法和區塊摒除法:
先調用唯一候選法,若未發生改變則返回False;若發生改變且為最終結果,則返回True;若發生改變且不是最終結果,則調用區塊摒棄法。
若未發生改變則返回False;若發生改變且為最終結果,則返回True;若發生改變且不是最終結果,則循環,再次調用唯一候選法。
唯一候選數法:
逐漸排除不合適的候選數,當某個方格的候選數排除至只有一個數字時,這個數字為該方格的唯一候選數,即最終解。排除方法:若在某行/列/九宮格中,只有某個方格的可能值含有某數字,那么該方格的值可以唯一確定為該數字。實現時,對每行/列/九宮格中各方格的值域分別進行不去重合并,若原方格的某個可能取值在合并得到的新列表中唯一,則將這個唯一值賦值給該值所在方格。
這個方法會進行多輪,涉及多次全局更新:分別尋找每行、每列、每個九宮格的中僅出現一次的數字,若有這樣的數字,則將其所在方格的值替換為該數字,然后由當前有確定值的方格得到新的數獨題目,再在空白方格中填入可能值,并再次尋找每行、每列、每個九宮格的中僅出現一次的數字。循環以上步驟直至不再發生改變。
區塊摒除法:
在九宮格中,如果某一數字僅出現在某行或某列中,那么這一行或者這一列中,其它九宮格的可能取值都可以排除掉這個數字。可以通過構建詞典來實現。詞典格式:{1: {‘row’: [2, 4], ‘column’: [3]}}
實際上也可以多輪,但感覺性價比不高(實際上這個方法一般情況下也沒多大用?);也不涉及全局更新,若有改變,會直接再調用唯一候選數法,在唯一候選數法中更新即可。
關鍵數刪減法:
對某個有多個可能取值的方格,依次假定每個可能取值為該方格的最終結果,繼續求解。如果發生錯誤,則嘗試其他可能取值。
具體實現:依次嘗試未確定方格的所有可能值,并將其填入方格,然后據此得到新的數獨題目,再在空白方格中填入可能值。若出現錯誤,則嘗試數字不合理;若未出現錯誤,則調用唯一候選數法和區塊摒棄法,檢驗新得到的數獨是否有解(有解則返回True,無解則嘗試下一個可能值)。若當前未確定方格的所有可能值都沒有解,則恢復嘗試前數據,開始嘗試下一個未確定方格的所有可能值。
根據觀察,在嘗試10個未確定方格后仍沒有解時,應終止嘗試,節省時間。
本來的想法是不斷使用遞歸:依次嘗試未確定方格的所有可能值,遞歸檢測所選值是否正確。在假設某方格的值后,檢查得到的值域列表是否合法。若值域列表合法,進一步檢驗是否已得到最終答案,若仍不是最終答案,則調用遞歸,查看下一個可能數字不唯一的方格,直至調用過程中返回最終解或最終發現無唯一解;若值域列表不合法,則更換當前嘗試方格的選值,若所有選值得到的值域列表均不合法,則數獨題目無唯一解。
但是在實際編寫時才發現,由于最初的設定是每步都可以顯示,所以遞歸難以實現,最終思路為:依次嘗試未確定方格的所有可能值,檢驗新得到數獨是否有解;若都沒有解,則嘗試下一個未確定方格的所有可能值。
回溯遍歷法:
考慮到自動設置的初盤可能不止有唯一解,而之前的方法只能求解有唯一解的數獨題目。所以在使用以上方法嘗試失敗后,又設置了回溯遍歷法,能夠得到數獨題目的一個可能解。實現時,依次嘗試每個未確定方格的值,可行則繼續嘗試下一個方格,有誤則不斷回溯,再次嘗試。由于涉及遞歸,此方法只展示右側的具體步驟以及最終結果,不支持“上一步”和“下一步”操作。
具體實現:在調用關鍵數刪減法前,保存當時的數獨題目,若關鍵數刪減法嘗試失敗,則將嘗試前的數獨題目傳入回溯遍歷法。在回溯遍歷函數中設置列表spaces存儲不確定方格的位置;設置新的行、列、九宮格列表,用于存儲1-9中每個數字是否在該行、列、九宮格中出現。最初將有確定值方格對應的位置設為True,其余設為False。然后開始遞歸,將當前確定方格對應的位置設為True,失敗則回溯并將嘗試失敗的對應位置改回False。通過設置True和False值來完成回溯遍歷。
4.3 求解與可視化間的數據信息連接
表格中的數據存儲在sudoku_data_dic中,便于直接獲取行、列、九宮格形式的列表。相當于列、九宮格形式的數據與行形式數據的轉換,感覺很巧妙。
def get_sudoku_table_data(self):self.sudoku_data_dic = {'row': [['' for i in range(9)] for j in range(9)],'column': [['' for i in range(9)] for j in range(9)],'block': [[] for j in range(9)]}for row in range(9):for column in range(9):cell_value = self.sudoku_table.item(row, column).text().strip()self.sudoku_data_dic['row'][row][column] = cell_valueself.sudoku_data_dic['column'][column][row] = cell_valueblock_num = (row // 3) * 3 + column // 3 # 所在九宮格self.sudoku_data_dic['block'][block_num].append(cell_value)文本展示等用到的信息,存儲在step_dic中,并隨著解題過程不斷添加到step_list中。后續會利用step_list中存儲的信息,進行GUI中數獨展示界面、步驟展示文本、提示信息的更新與展示。
def update_step_list(self, step_text_list=[], message_text_list=[]):self.get_sudoku_table_data()step_dic = {'row_list': copy.deepcopy(self.sudoku_data_dic['row']),'step_text': copy.deepcopy(step_text_list),'message_text': copy.deepcopy(message_text_list)}self.step_list.append(step_dic)4.4 可視化界面及輔助功能
使用了pyqt5,用于繪制交互界面,主要分為菜單欄和主要選項卡。
def init_ui(self):self.gen_menu_bar() # 生成菜單欄self.gen_main_tab() # 生成主要選項卡# 繪制GUI窗口self.setWindowTitle('Sudoku Solver') # 設置窗口標題self.resize(1030, 650) # 設置屏幕大小qr = self.frameGeometry() # 設置在屏幕中間顯示cp = QDesktopWidget().availableGeometry().center()qr.moveCenter(cp)self.move(qr.topLeft())菜單欄 包括3部分:自動生成初盤、文件載入導出、關于。
主要選項卡 包括3部分:數獨界面(標題、清空按鈕、9*9表格)、描述信息、步驟展示(一鍵解題、上一步、下一步、文本展示)。
對應的功能也在這里實現,比如文件的載入、保存,清空數獨界面;還引用了前面的.py文件,如根據難度自動生成初盤,“上一步” “下一步”。
總結
以上是生活随笔為你收集整理的python实现可视化数独求解器(附代码链接及点点讲解)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 访问oracle数据库语句,Oracle
- 下一篇: python爬取豆丁网文章_百度文库爬取