【Python】基於候選數的解數獨算法 + 使用wxPython編寫程序界面

0、本文提供了一種基於候選數的解數獨算法,並使用wxPython編寫了簡單的程序界面

wxPython的安裝和安裝後的路徑配置問題可參見:

【Eclipse+PyDev+wxPython】wxPython的安裝注意事項&&安裝後Eclipse中仍然報錯問題的解決


1、基於候選數的解數獨算法具體流程如下:

該算法類似於人工求解數獨時所用到的假設解法,假設->回溯->假設->回溯……->得解

由於數獨本身的規則所限,每行每列每個小區域都只能是1~9,而不能有任何重複的數字

所以,對應於每一個求解狀態,每一個空格都會存在一組候選數集合,即可以填入該空格的候選數字(可以爲空集)

(1)首先,找到一個候選數最少的空格,逐一假設填入

(2)如果在某一求解狀態下,有空格的候選數集合爲空(即沒有合法數字可供填入),則回溯到上一狀態繼續嘗試

(3)如果某一空格遍歷假設了所有候選數而仍未得到解,則回溯到上一個假設空格繼續嘗試

(4)重複這一過程,直到得到一個解返回結果,或搜索完所有可能的解

(5)爲實現這一算法,還需要一個棧用來存儲過程中的狀態,每次填入數字前入棧,而每次回溯時出棧


2、使用這一算法,實測能夠秒解所有入門級數獨,對於骨灰級的數獨大概需要1到2min的時間

程序函數僞代碼:

Solve_Sudoku(*Map, mode)
{
	if Have_Find_Single_Solution
		return
	if Complete
		Output_Solution()
		if Stack!=empty
			Erase_out()
			Stack_out()
		return
	else
		Update_Candidate()
		Find_MIN_Candidate()
		if MIN==0
			Erase_out()
			Stack_out()
			return;
		else
			for i=1:MIN
				Stack_in()
				Fill_in()
				Solve_Sudoku()
				if Have_Find_Single_Solution
					return
			if Stack!=empty
				Erase_out()
				Stack_out()
			return
}

Update_Candidate()
{
	for Map[i][j]
		if Map[i][j]!=0
			for Candidate[i][:]
				if Map[i][:]==0
					Candidate[i][:][Map[i][j]-1]=0
			for Candidate[:][j]
				if Map[:][j]==0
					Candidate[:][j][Map[i][j]-1]=0
			for Candidate[3*(i//3):3*(i//3)+2][3*(j//3):3*(j//3)+2]
				if Map[:][:]==0
					Candidate[:][:][Map[i][j]-1]=0
}

Find_MIN_Candidate()
{
	Min=9
	for candidate[i][j][:]
		count=sum(candidate[i][j][:])
		if count<Min
			Min=count
			Min_i=i
			Min_j=j
}


Python具體實現代碼:(使用遞歸函數實現,0表示空格,各模塊代碼在註釋中均有比較詳細的說明)

# -*- coding: utf-8 -*-
"""##################
基於候選數的數獨求解
Author: Alex_P @UCAS
##################"""

from numpy import array, zeros, ones, int32

"""#################數獨#################"""
Sudoku = array([[0, 0, 0, 0, 0, 8, 5, 0, 0],
                [2, 0, 0, 9, 0, 0, 0, 3, 0],
                [0, 0, 0, 0, 6, 0, 1, 0, 0],
                [4, 0, 0, 2, 0, 0, 0, 9, 0],
                [0, 0, 0, 0, 0, 1, 0, 0, 0],
                [0, 0, 0, 0, 5, 0, 8, 0, 0],
                [7, 0, 0, 0, 0, 0, 0, 0, 6],
                [0, 3, 9, 7, 0, 0, 0, 0, 0],
                [0, 2, 4, 0, 0, 0, 0, 7, 0]], dtype = int32)
#Notice:座標軸(0~8, 0~8),表示爲range(0, 9)
Solution = zeros((9, 9), dtype = int32)# 解陣列

"""#################棧類#################"""
class Stack:
    # self
    def __init__(self):
        self.items = []
    # 檢查棧是否爲空
    def isEmpty(self):
        return len(self.items) == 0
    # 入棧
    def push(self, item):
        self.items.append(item)
    # 出棧
    def pop(self):
        return self.items.pop()
    # 提取棧頂元素
    def peek(self):
        if not self.isEmpty():
            return self.items[len(self.items) - 1]
    # 獲取棧中元素個數
    def size(self):
        return len(self.items)

"""#################定義棧等#################"""
# 棧類
S = Stack()
# 棧內元素
for p in range(1, 81):
    # S_Map1, S_Map2, S_Map3, ……, S_Map80
    S_Map = "S_Map" + "%d" %p
    exec(S_Map + " = zeros((9, 9), dtype = int32)")
# 單解標識
FSingleSolution = False

"""#################打印函數#################"""
def Print_Sudoku(Map):
    print "—————————"
    for i in range(0, 3):
        for j in range(0, 3):
            Map_str = "|"
            for m in range(0, 3):
                for n in range(0, 3):
                    Map_str = Map_str + " %d" %Map[i * 3 + j][m * 3 + n]
                Map_str = Map_str + " |"
            print Map_str
        print "—————————"

"""#################求解函數#################"""
#Map->Sudoku
#mode->0(僅搜索單解,返回解),非0(搜索所有解,返回未求解原數獨)
def Solve_Sudoku(Map, mode):
    # 單解跳出
    global FSingleSolution
    if FSingleSolution == True:
        return Map
    # 候選數陣列
    Candidates = ones((9, 9, 9), dtype = int32)
    # 檢查是否求解完畢
    FComplete = True
    for i in range(0, 9):
        for j in range(0, 9):
            if Map[i][j] == 0:
                FComplete = False
                break
    # 求解完畢
    if FComplete == True:
        # 輸出解
        print "Solution Accomplished!"
        global Solution
        Solution = Map.copy()# 獨立複製
        Print_Sudoku(Solution)
        # 找到單解後返回
        if mode == 0:
            FSingleSolution = True
            return Map
        # 返回操作並出棧
        if S.isEmpty() == False:
            Map = S.peek().copy()
            S.pop()
            #print "Stack OUT... (size:", S.size(), ")"
        return Map
    # 未求解完畢
    else:
        # 更新候選數陣列
        #print "Updating Candidates..."
        for i in range(0, 9):
            for j in range(0, 9):
                if Map[i][j] != 0:
                    # 列
                    for m in range(0, 9):
                        if Map[m][j] == 0:
                            Candidates[m][j][Map[i][j] - 1] = 0
                    # 行
                    for n in range(0, 9):
                        if Map[i][n] == 0:
                            Candidates[i][n][Map[i][j] - 1] = 0
                    # 單元九宮格
                    for m in range(3 * (i // 3), 3 * (i // 3) + 3):
                        for n in range(3 * (j // 3), 3 * (j // 3) + 3):
                            if Map[m][n] == 0:
                                Candidates[m][n][Map[i][j] - 1] = 0
        # 尋找最少候選數
        #print "Finding MIN Candidates..."
        Min = 9
        for m in range(0, 9):
            for n in range(0, 9):
                count = sum(Candidates[m][n][:])
                if count < Min:
                    Min = count
                    Min_i = m
                    Min_j = n
        #print Min, "candidate(s) available at (", Min_i + 1, ",", Min_j + 1, ")"
        # 候選數爲0(無解分支)
        if Min == 0:
            # 返回操作並出棧
            Map = S.peek().copy()
            S.pop()
            #print "Stack OUT... (size:", S.size(), ")"
            return Map
        # 存在候選數
        else:
            # 逐一假設
            for k in range(1, Min + 1):
                # 入棧
                p = "%d" %(S.size() + 1)
                S_Map = "S_Map" + p
                exec(S_Map + " = Map.copy()")
                exec("S.push(" + S_Map + ")")# S.push(S_Map【S.size()+1】)
                #print "Stack IN... (size:", S.size(), ")"
                # 填入候選數
                count = 0
                for o in range(0, 9):
                    count = count + Candidates[Min_i][Min_j][o]
                    if count == k:
                        break
                Map[Min_i][Min_j] = o + 1
                #Print_Sudoku(Map)
                # 使用遞歸函數進行回溯求解
                Map = Solve_Sudoku(Map, mode)
                # 單解跳出
                if FSingleSolution == True:
                    return Map
            # 返回操作並出棧
            if S.isEmpty() == False:
                Map = S.peek().copy()
                S.pop()
                #print "Stack OUT... (size:", S.size(), ")"
            return Map

"""#################主函數#################"""
def main():
    Print_Sudoku(Sudoku)
    Solve_Sudoku(Sudoku, 0)# 搜索單解
    #Solve_Sudoku(Sudoku, 1)# 搜索所有解
    #Print_Sudoku(Solution)

"""#################main#################"""
if __name__ == '__main__':
    main()

3、使用wxPython編寫程序界面,大致效果如下:(求解完成後可清除並重複使用)


具體實現代碼:(需調用前述Sudoku,各模塊代碼在註釋中均有比較詳細的說明)

# -*- coding: utf-8 -*-
"""###################
基於候選數的數獨求解UI界面
Author: Alex_P @UCAS
###################"""

import wx
import Sudoku
from numpy import zeros, int32

"""#################wx.App類#################"""
class App(wx.App):
    def OnInit(self):
        self.frame = Frame()
        self.frame.Show(True)# 顯示窗口
        #self.frame.Show(False)# 隱藏窗口
        self.SetTopWindow(self.frame)# 設置爲頂層窗口
        return True

"""#################wx.Frame瀛愮被#################"""
class Frame(wx.Frame):
    # 初始化界面
    def __init__(self):
        wx.Frame.__init__(self, None, -1, "An Easy Sukudo Solver", size = (400, 500))
        self.setupMenuBar()# 菜單
        panel = wx.Panel(self, -1)# 面板
        wx.StaticText(panel, -1, "Sukudo", (150, 20), (80, -1), wx.ALIGN_CENTER)
        # (位置), (大小), 居中
        mapPosX = 30# 左上角起始點x座標
        mapPosY = 50# 左上角起始點y座標
        textWidth = 25# 文本框寬度
        idleWidth = 10# 橫向間距
        idleHeight = 35# 縱向間距
        sperateLength = 10# 九宮格間距
        # 初始化數獨輸入框
        for i in range(0, 9):
            for j in range(0, 9):
                Map_i_j = "self.Map_" + "%d" %i + "_" +"%d" %j# self.Map_0_0...Map_i_j...Map_8_8
                exec(Map_i_j + " = wx.TextCtrl(panel, -1, '0', pos = (mapPosX + j * (textWidth + idleWidth) + j // 3 * sperateLength, mapPosY + i * idleHeight + i // 3 * sperateLength), size = (textWidth, -1), style = wx.TE_CENTER)")
        # 初始化按鈕
        self.button = wx.Button(panel, -1, "Solve", pos = (152, 385), size = (80, 30))
        self.Bind(wx.EVT_BUTTON, self.OnClickButton, self.button)# 事件綁定
        self.button.SetDefault()# 設置爲默認按鈕
        # 初始化陣列
        self.Sudoku = zeros((9, 9), dtype = int32)# 數獨陣列
        self.Solution = zeros((9, 9), dtype = int32)# 解陣列
    # 點擊按鈕的事件響應
    def OnClickButton(self, event):
        Label = self.button.GetLabel()# 獲取按鈕狀態
        # 解數獨
        if Label == "Solve":
            self.button.SetLabel("Waiting...")# 按鈕提示等待
            FAllZero = True# 全零標識
            for i in range(0, 9):
                for j in range(0, 9):
                    # self.Map_0_0...Map_i_j...Map_8_8
                    Map_i_j = "self.Map_" + "%d" %i + "_" +"%d" %j
                    # 讀取輸入陣列
                    exec("self.Sudoku[i][j] = " + Map_i_j + ".GetValue()")
                    # 檢驗輸入數獨陣列是否合乎要求(0~9)
                    if self.Sudoku[i][j] != 0 and self.Sudoku[i][j] != 1 and self.Sudoku[i][j] != 2 and self.Sudoku[i][j] != 3 and self.Sudoku[i][j] != 4 and self.Sudoku[i][j] != 5 and self.Sudoku[i][j] != 6 and self.Sudoku[i][j] != 7 and self.Sudoku[i][j] != 8 and self.Sudoku[i][j] != 9:
                        dlg = wx.MessageDialog(self, "Only numbers (0~9, 0 for blank) allowed!", "Error", wx.OK)
                        dlg.ShowModal()
                        dlg.Destroy()
                        self.button.SetLabel("Solve")# 刷新按鈕
                        return
                    # 檢驗數獨陣列是否輸入
                    if FAllZero == True and self.Sudoku[i][j] != 0:
                        FAllZero = False
            # 錯誤提示
            if FAllZero == True:
                dlg = wx.MessageDialog(self, "Please input your Sukudo!", "Error", wx.OK)
                dlg.ShowModal()
                dlg.Destroy()
                self.button.SetLabel("Solve")# 刷新按鈕
                return
            #Sudoku.Print_Sudoku(self.Sudoku)
            self.Solution = Sudoku.Solve_Sudoku(self.Sudoku, 0)# 解數獨
            Sudoku.FSingleSolution = False# 刷新求解函數狀態
            #Sudoku.Print_Sudoku(self.Solution)
            for i in range(0, 9):
                for j in range(0, 9):
                    Map_i_j = "self.Map_" + "%d" %i + "_" +"%d" %j# self.Map_0_0...Map_i_j...Map_8_8
                    exec(Map_i_j + ".SetValue(str(self.Solution[i][j]))")# 輸出解陣列
            self.button.SetLabel("Clear")
        # 清除
        elif Label == "Clear":
            for i in range(0, 9):
                for j in range(0, 9):
                    Map_i_j = "self.Map_" + "%d" %i + "_" +"%d" %j# self.Map_0_0...Map_i_j...Map_8_8
                    exec(Map_i_j + ".SetValue('0')")# 清除數獨陣列
            self.button.SetLabel("Solve")
    # 建立菜單
    def setupMenuBar(self):
        self.CreateStatusBar()# 創建
        Menu = wx.MenuBar()# 總菜單
        programMenu = wx.Menu()# 程序菜單
        # 將關於加入程序菜單
        menuabout = programMenu.Append(wx.ID_ABOUT, "&About", "About this program")
        # 將退出加入程序菜單
        menuexit = programMenu.Append(wx.ID_EXIT, "&Exit", "Exit program")
        Menu.Append(programMenu, "&Menu")# 將程序菜單加入總菜單
        # 事件綁定
        self.Bind(wx.EVT_MENU, self.onAbout, menuabout)# 關於事件
        self.Bind(wx.EVT_MENU, self.onExit, menuexit)# 退出事件
        self.SetMenuBar(Menu)
    # 點擊關於的事件響應
    def onAbout(self, evt):
        dlg = wx.MessageDialog(self, "An Easy Sudoku Solver\n'0' represents blank\n\nAuthor: Alex_Pan @UCAS", "About this program", wx.OK)
        dlg.ShowModal()
        dlg.Destroy()
    # 點擊退出的事件響應
    def onExit(self, evt):
        self.Close(True)

"""#################主函數#################"""
def main():
    app = App()
    app.MainLoop()# 將app作爲主事件消息循環

"""#################main#################"""
if __name__ == '__main__':
    main()


4、【示例求解過程】

輸入待求解數獨:


求解中:


求解結果:


清空:



希望能夠對大家有所幫助啓發~

發佈了30 篇原創文章 · 獲贊 30 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章