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、【示例求解過程】
輸入待求解數獨:
求解中:
求解結果:
清空:
希望能夠對大家有所幫助和啓發~