跳到主要內容

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};  ...

單人撲克牌遊戲 - 蒙地卡羅

新增一個簡單的單人撲克牌遊戲: 蒙地卡羅 ,簡單介紹一下玩法。 下載 事先排列好5x5張牌。 每次移動一張可以配對的牌,並消除這對牌。在上下、左右及斜向相隣的二張牌,只要擁有同樣數字(不計花色),即可配對。 消除二張配對的牌後,剩餘的牌以往左往上的方式補滿空隙,接著在發新牌補滿後面的空格。 重覆步驟2~3,直到沒有牌可以配對及發完所有牌為止。 結果有二種。一個是勝利,成功的消除掉所有牌。另一個是Gameover沒有牌可以再作配對。

以lex/yacc實作算式計算機

前面我們透過 手工的方式 實作了一個簡易的算式計算機,現在我們要開始使用工具來作同樣的事,比較看看手工和使用工具有什麼不同的差別。首先要介紹的就是lex&yacc。 lex & yacc lex(Lexical Analyzar)及yacc(Yet Another Compiler Compiler)是用來輔助程式設計師製作語法剖析器的程式工具。lex的工作就是幫助我們將輸入的資料文字串流分解成一個個有意義的token,而yacc的工作就是幫我們分析這些token和我們定義的規則作匹配。下圖中所表示的是使用lex及yacc的一般工作流程。 首先看到yacc會讀入一個.y檔案,這裡.y檔案的內容就是我們使用類似(E)BNF語法定義的語法規則,yacc會分析這些語法規則後,幫我們產生可以用來解析這些規則的程式碼,而這個檔案一般名稱預設為y.tab.c,產生的程式碼裡面最重要的一個的函式叫作yyparse。 同yacc類似,lex也會讀入一個.l的檔案,這個檔案裡面定義的是如何從文字流裡解出token的規則,使用的方法是常規表示式(regular expression)。在圖的左側中間我們還可以看到有一個叫作y.tab.h的檔案從yacc產生出來並餵給lex作輸入,這個檔案是yacc根據在讀入的.y檔裡面所定義的token代號所產生出來的一個header,這樣yacc及lex產生出來的程式碼裡面就可以使用共通定義的代碼而不必各寫個的。lex分析過.l檔案後也會產生一個一般預設叫作lex.yy.c的原始碼檔案,裡頭最重要的一個函式叫作yylex。 最後,我們把yacc產生出來的y.tab.c還有lex產生出來的lex.yy.c,以及其它我們自己撰寫的原始碼檔案一起拿來編譯再作連結,最後產生出來的就是一個可以用來解析我們定義的語法的解析器工具。以上是整個lex及yacc的使用流程概觀。 常規表示式 在正式使用lex之前,我們首先來對常規表示法作一個基本的認識。常規表示法是一種用來表示字串樣式(pattern)的中繼語言,就好比前文所介紹的(E)BNF表示式一樣,都是用來描述其它語言的語言,只不過用途不太一樣罷了。 常規表示式使用一些中繼符號(meta-symbol)以及ASCII字元定義字串樣式,以下列出一些常規表示式所使用的符號。 . 表示除了換行字元...