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