跳到主要內容

Kill Sudoku

前陣子斷斷續續的玩了一陣子數獨(sudoku),才終於把安裝在我的野火機裡的OpenSudoku的全部謎題解到只剩下一題VeryHard等級的謎題留作紀念。

在解這些題目的過程中一直有個想法,就是想要也自己寫個可以解數獨的小程式。數獨解答機在網路上很多,大多是使用回溯法為基礎的演算法製作的,也就是基於試誤法或窮舉法的方式,和我們一般在解題時使用的方法不同。我想要寫的是一個可以像人類一樣,使用各種技巧來解數獨謎題的程式,解題過程的每一個步驟都要清楚條列出來,這樣這個程式也可以用來作學習用途。

這個想法一直到最近才動手,目前還只是初步階段,寫了一支文字模式的小程式,可以從文字檔案裡讀出一個數獨謎題,將解題過程一個一個步驟都顯示出來,如下圖所示。將來可以寫成圖形化的APP或者是Web的型式,以更豐富的圖式方式來呈現。(HTML5 KillSudoku)


這個程式是基於使用候選數法來製作的,所以從上圖可以看到個一個還未解出答案的格子裡都會列出全部的可能的數字,這些數字就叫作候選數。

~~~

一道數獨謎題是由9(row)x9(column)=81個格子(cell)組成,每一個cell裡可以填入1~9中的一個數字。這9x9個格子裡面,又可以再分成較小的9(box)個3x3的格子。


如上的一個9x9宮格圖所示,左側數字1~9為每一列(row)的編號,上方小寫英文字母a~i為每一行(column)的編號,b1~b9為每一個小九宮(box)的編號。

以上定義了幾個基本名詞:cell, row, column, box,再加上前面所提到過的候選數(candidate),就是我們全部需要知道的幾個定義。至於數獨的規則部份,這裡就不再複述。

~~~

這個程式的基本原理很單純,只是把我們平常在解數獨題時所用到的手法程式化而已。例如要找唯一數存不存在,就一行一行一列一列的去找看看某數是不是在這一行或這一列裡面是唯一的,如果是的話那就把這個數填入。目前版本的程式還很陽春,只實作了7種pattern,下面一一介紹。使用這7種找pattern的方法最多可以解出初級到中級左右的題目。

在開始找pattern前,需要把輸入資料作一些轉換成內部資料的處理。謎題的輸入是81個數字,由左至右,由上而下,一列一列的由上往下輸入。每一個數字範圍為0~9,0表示這個cell是空的,待填的,1~9則表示這個cell的確定數。

這81個數字讀入後,在程式裡面使用一個一維陣列儲存(int cell[]),同時再以另一個同樣維度的陣列,用來儲前面提到過的候選數(int candidate[]; // bitset)。如果cell[n] != 0,那candidate[n]就不看。如果cell[n] == 0,那就是說cell[n]是空的,那candidate[n]裡存的就是cell[n]的候選數,其中n = 0~80。

整支程式基本上作的事情就是根據程式定義的pattern,想辨法去消減候選數,當某個cell的候選數消減到只剩下一個的時候,那就可以將這個最後的數填入這個cell,變成確定數。程式執行時會不斷的重複找pattern的動作,直找無法再找到任何pattern為止。結果有二種,一種是全部解完,一種是卡關,需要加入更多pattern才能繼續。這是程式的基本運作概念。


Single

Single是唯一候選數的pattern,這個pattern有二種情況:

  1. 顯式的(naked):在這個情況下,這個候選數是這個cell裡唯一的一個候選數。
  2. 隱式的(hidden):這表示說,在這個cell裡面還有其它的候選數存在,但因為在這個cell所在的column或row或box裡面,這個候選數是唯一的,所以可以忽略其它候選數,填入這個唯一數。

如上圖,cell f2裡面只有一個候選數8,所以可以將8填入f2。


如上圖,在紅線框的這個box裡面,候選數5只存在cell c1裡面,所以可以將5填入c1。


Claiming

在一個row或column裡面,如果某一數的候選數都集中在同一個box裡面,則在這個box裡面在這個row或column以外的候選數都可以消除。


如上圖所示,候選數7在row9裡面都集中在box7裡,這表示說在box7裡面候選數7只可能出現在row9裡面,因此在box7裡面row9以外的候選數7都可以消除。


Pointing

這個技巧和Claiming類似,差別只在於Pointing是先由box的角度來看。如果在一個box裡面的某個候選數都集中在同一個row或column的話,則在這個row或column上的其它候選數都可以消除。


如上圖,在box7裡候選數3只存在row8裡面,也就是說在整個row8上面候選數3只可能出現在box7裡面,因此在row8上在box7以外的候選數3都可以被消除。


Set

Set(或Subset)是在找數字組合的pattern,Set的長度可以是2~4。長度為n的Set可以定義一組由這幾個數字構成的Subset,Subset的長度可以是2~n。

例:3及9二個數字構成的Set,長度為2,Subset只有一種39。
例:1,4及7三個數字構成的Set,長度為3,它的Subset可以是14、17、47及147。

Set可以是顯式及隱式二種。


對於顯式的Set,假如構成長度為n的Set的Subset只出現在n個不同的cell裡面的話,則其它cell裡面的Set候選數都可消除。如上圖所示為一個顯式的Set的例子。這是由5及7構成的Set,出現在box3裡的g2及i3 cell裡面。換句話說,5及7只可能出現在這二個cell裡面,因此其它的cell裡面的5或7都可以消除。


對於隱式的Set,假如構成長度為n的Set的Subset只出現在n個不同的cell裡面的話,則在這n個cell裡面的其它候選數都可以消除。如上圖是一個隱式Set的例子,在這個例子裡面2及8只出現在d4及f6二個cell裡面,因此構成了一個Set的pattern,所以除了2和8以外的數字都可以從d4及f6二個cell裡面消除,只留下2及8。


X Wings

若某個候選數同時出現在二個row和二個column上共4個cell裡面,並且這4個cell都處在不同的cell裡面。若其中二個row或二個column裡的這個候選數都是唯二的,那麼在對應的二個column或二個row裡面的這個候選數都可以被消除。


如上圖,在column e和column h都各有二個候選數5,並且這二對5也同時存在於row 2和row 5上,因此構成XWings pattern。

因為在column e上候選數5要嘛出現在e2要嘛出現在e5,在column h上候選數5要嘛出現在h2要嘛出現在h5。當5出現在e2時,h5必為5,e5及h2就不能為5(互斥);當5出現在e5時,h2必為5,e2及h5就不能為5(互斥)。

無論5出現在那個地方,在row 2和row 5上其它地方也不可能出現5(也被排斥掉了),所以column e和column h上以外的5都可以被消除。


Finned X Wings

這是XWings的變化型態,如下圖所示。


如果h3 != 4的話就構成上述的XWings pattern,那麼i2的4就可以被消除。如果在box3裡面h3=4的話,那麼i2就被排除不能為4。結合這二個pattern,不論那個情況發生,i2都不可能為4,所以可以把候選數4消除。


XY Wings

這是在找由XYZ三個數構成的XY / XZ / YZ的組合,分別存在三個不同的cell裡面,其中二個cell在同一row或同一column,並且其中二個cell必須在同一個box中。


如上圖所示,由245三個數構成的三個數對分別在e8 g9 h8三個cell裡,其中e8和h8在同一個row上,而g9和h8在同一個box裡面,所以構成XY Wings pattern。

現在來分析:如果h8=2的話,那麼g9=5;如果h8=4的話,那麼e8=5。無論那種情況發生,同時被這三個cell看到的5都會被排除。


留言

這個網誌中的熱門文章

猜數字遊戲 (電腦猜人)

前幾天午睡時突然被告知要參加公司內部的程式設計比賽,題目是用C寫一支文字模式的4位數字猜數字遊戲,由使用者來猜電腦的數字。在上星期時其實就已經有公佈了,但我沒有注意到所以是臨時加入,還好這是個簡單的題目,不用花多少時間就可以寫出來。 規則: - 這是一對一比賽,雙方各選擇一4位數字,不讓對方知道。 - 4位數字由數字0至9組成,每位數不得重複。 - 雙方輪流猜對方的數字,直到一方猜中為止。 - A方猜B方的數字後,B方根據A方的猜測回答幾A幾B。 - 一個A表示猜中一個數字且位置正確,一個B表示猜中一個數字但位置不正確。 - 當一方猜中4A0B時即表示猜中對方全部4個數字且位置正確,贏得比賽。 - 例:B的謎底是4208,底下箭頭左測是A的猜測,箭頭右測是B的回答。    1234 ==> 1A1B    5678 ==> 1A0B    2406 ==> 1A2B    ...    4208 ==> 4A0B ; 寫個程式讓玩家來猜電腦的數字不難,不過我從來沒有寫過讓電腦來猜玩家數字的版本,所以花了點時間想想怎麼寫。 研究後歸納出二個點。 1, 使用窮舉法將所有可能數字組合列出。 2, 每次猜測後根據結果排除不可能是答案的組合,重複這個動作直到猜中答案為止。 第1點只是實作問題,第2點概念也很簡單,但要過濾不是答案的組合根據的是什麼?乍看之下沒什麼頭緒,不過想通之後就非常簡單了。 它的基本原理如下:假如謎底是4561,如果猜1524則會得到1A2B。從相反的角度來看,如果謎底是1524,則猜4561時也會得到1A2B的回答。 利用這個方法,每一次猜測一個數字X後,再以這個數字當作答案,來和所有剩下來的候選答案作比對,如果得到的結果(幾A幾B)和數字X是一樣的話,就把這個數字保留下來繼續作為候選答案,否則就過把這個數字過濾掉。下一把,繼續從候選答案裡選一個出來猜,重複上面的動作,直到猜中為止。 ; C++ STL的algorithm裡有個叫作next_permutation的函數,可以用來生成排列。 #include <iostream> #include <algorithm> using namespace std; int main () {   int myints[] = {1,2,3};  ...

KillSudoku 4顆星精彩數獨詳解 - 鍊技巧

這題數獨(sudoku)題目估計為4+顆星,有點難度。解題需要應用多種技巧,過程非常精彩有趣,是個好題。 底下使用 KillSudoku 作詳細圖解。 1,使用基本排除法則,可以簡單填入6個數字。到此為止,開始使用 候選數法 來解題。如下所示,為填入6個數字後的狀態圖。 2,如下圖,使用進階排除法,在第9列和第4行可以先排除幾個候選數。 3,如圖,在第2行有一個 Naked Subset (3,4),可以對3,4候選數作排除。附帶提一下,反過來看在同一行裡面也可以說有另一個Hidden Subset(2,5,8)存在。Naked Subset和Hidden Subset常是一體二面同時存在,只不過對我們來說,Naked Subset是相對比較容易看的出來。 排除第2行的3,4後,又可以對第2列以外的3作排除,如下圖。 4,接著,在第5行又發現了一個 Naked Subset (3,7,8)。 對第5行三個Subset以外的候選數3,7,8作排除後,又接著產生可以對第5行以外的3作排除。 5,這一題解到此為止,開始進入高潮。大部份能解到3顆星題目的人,猜想應該就此卡住。以下開始需要應用更高級的鍊技巧,才能夠繼續進行。 應用X-Chains鍊技巧,可以找到一條由4條強連結組成的鍊,可以排除候選數2。這裡的鍊指的是由2條以上的強連結組成,而所謂的強連結是指在同一行、或同一列或同一個Box裡,由唯二的候選數構成的連結。如上圖中的第9行中,只有二個2,這二個2構成一條強連結。為什麼說這是一條強連結?因為在這條連結的AB二個端點中,肯定會有一個2存在,要麼是A點要麼是B點。鍊技巧就是將多條強連結串連起來作候選數排除的技巧,而X-Chains是高級的鍊技巧裡面的基本技巧。 接上圖,這樣一來就又可以應用基本排除方法,填入3個數字,如下圖所示。 6,接下來就是本題最精彩的部份,以下需要連續找到3條鍊,才能繼續往下解。 7,找出3條鍊後,剩下來的部份就沒什麼特別的了,只需要應用基本法就能把所有剩餘數字填完。

KillSudoku 4顆星精彩數獨 (三) - XY-Chains

這是數獨解題技巧裡面的高級技巧,比X-Chains還再高一點點。會這個技巧的話,就可以解4或5顆星的題目了。 這個用來測試的題目,用 KillSudoku 來解可以解出,中間使用了2次Naked Subset,1次 W-Wings ,1次 X-Chains ,2次 XY-Chains 。所以算起來,這一題應該是有5顆星的題目。 附帶一提,目前找鍊的演算法並沒有去找一條最短的鍊,所以可以看到用 KillSudoku 解的時候,第36的步驟找到一條超長的鍊,這條鍊足足由13條連線構成,要是沒練過的話,絕對頭昏眼花,找不出這樣的鍊來的。 實際上在這個步驟裡,是可以找到另一條更短的鍊。不過目前以先能work,之後有空會再改進演算法的部份。