1. Danclink links 算法解Sudoku的基本思想:
將Sudoku問題轉化爲等價的01矩陣問題,然後用dlx算法求解。從一個由0 1組成的矩陣中找到一個行的集合,使得集合中每列恰好包含一個1,稱爲01矩陣問題。解決它的dancling links 算法利用雙向鏈表的一個性質,巧妙的剪枝,獲得了較好的性能
A) 構造01矩陣
01矩陣問題是什麼?
01矩陣是這樣一個問題::給定一個由0和1組成的矩陣,是否能找到一個行的集合,使得集合中每一列都恰好包含一個1?
例如,下面這個矩陣
|
(3) |
包含了這樣一個集合(第1,4,5行)。
第1行:0 0 1 0 1 1 0
第4行:1 0 0 1 0 0 0
第5行:0 1 0 0 0 0 1
每1列恰1個1,符合條件。
如何用Sudoku構造01矩陣
Sudoku問題可以轉化爲01矩陣問題。可以知道,Sudoku有如下四個約束條件
1)每一個數(1到9)在每一行出現且僅出現一次。
2)每一個數(1到9)在每一列出現且僅出現一次。
3)每一個數(1到9)在每一個九宮格出現且僅出現一次。
4)每一個格子出現且僅出現一個數。
注意:第四個條件很容易漏掉,但這是必不可少的。
下面是Sudoku轉化爲01矩陣的方法
1)令r表示行,c表示列,b表示九宮格,k表示值,i表示一個格子,那麼rk,ck,bk, i
即可表示4個約束條件,(例如rk=15,表示第一行只有1個5,bk=32,表示第3個九宮格只有1個2,i=32表示第32個格子只有一個數。因爲r, c , b, k 範圍爲1到9,i範圍爲1到81,所以總共有9*9*3+81=324種情形,即表示01矩陣中有324列)
2)考慮每一個格子的情況,如果第2行第4列是5(rck=245)那麼可知rk=25,ck=45, bk=25, i=13這四列下的1找到了。(爲什麼bk=25,因爲按從左到右,從上到下的順序,第2行第四列恰爲第2個九宮格,同理i=13也是這個道理),那麼可以生成這樣一行01序列
0 0 0 0……1…0 0 0…1…0 0 0…1…0 0 0 0 0 0 0 0…1……0 0 0 0
這一行總共324個數,表示324行,其中4個1分別位於rk=25,ck=45, bk=25, i=13這四列。
假如第4行第2列是未知的。那麼可知第4行第2列可能取值爲1到9,共九種可能。(即rck=421, 422,423, 424,……429)這9種可能可以生成01矩陣中的9行。例如rck=422可以生成這樣一行0 0 0 0 ……1…0 0 0…1…0 0 0…1…0 0 0 0 0 0 0 0…1……0 0 0 0。其中,四個一位於rk=42,ck=22, bk=42, i=29這四列下。
根據以上可構造出1個01矩陣,可知每一列下只可恰好包含一個1。如果我們選出一些行(確切來說是81行)使符合這個條件,那麼就可以得到Sudoku的結果。那麼danclink link X 算法是如何求解的呢?
B)dancing link X 解01矩陣
考慮下列的求解過程:
如果01矩陣A是空的,問題解決;成功終止。
否則,選擇一個列c(確定的)。
選擇一個行r,滿足 A[r,
c]=1 (不確定的。注意這裏的r c是矩陣A的行與列,與上面不同)。
把r包含進部分解。(假設這第r行是所求行集合的一部分)
對於所有滿足 A[r,j]=1的j,
從矩陣A中刪除第j列;
對於所有滿足 A[i,j]=1的i,
從矩陣A中刪除第i行。
在不斷減少的矩陣A上遞歸地重複上述算法。
如果我們刪到最後得到A是空的。那麼我們就得到了01矩陣問題的一個解。問題是當我們得不到A是空的時候,我們必須回溯,我們必須把已經錯誤刪掉的一些行與列恢復。這就會用到雙向鏈表的一個有用性質。
以下詳細算法:
一個實現X算法的好方法就是將矩陣A中的每個1用一個有5個域L[x]、R[x]、U[x]、D[x]、C[x]的數據對象(data
object)x來表示。矩陣的每行都是一個經由域L和R(“左”和“右”)雙向連接的環狀鏈表;矩陣的每列是一個經由域U和D(“上”和“下”)雙向連接的環狀鏈表。每個列鏈表還包含一個特殊的數據對象,稱作它的表頭(list
header)。
這些表頭是一個稱作列對象(column object)的大型對象的一部分。每個列對象y包含一個普通數據對象的5個域L[y]、R[y]、U[y]、D[y]和C[y],外加兩個域S[y](大小)和N[y](名字);這裏“大小”是一個列中1的個數,而“名字”則是用來標識輸出答案的符號。每個數據對象的C域指向相應列頭的列對象。
表頭的L和R連接着所有需要被覆蓋的列。這個環狀鏈表也包含一個特殊的列對象稱作“根”,h,它相當於所有活動表頭的主人。而且它不需要U[h]、D[h]、C[h]、S[h]和N[h]這幾個域。
舉個例子,(3)中的0-1矩陣將用這些數據對象來表示,就像圖2展示的那樣,我們給這些列命名爲A、B、C、D、E、F和G(這個圖表在上下左右處“環繞扭曲”。C的連線沒有畫出,因爲他們會把圖形弄亂;每個C域指向每列最頂端的元素)。
|
圖2 完全覆蓋問題(3)的四方向連接表示法 |
我們尋找所有精確覆蓋的不確定性算法現在可以定型爲下面這個明析、確定的形式,即一個遞歸過程search(k),它一開始被調用時k=0:
如果 R[h]=h,打印當前的解(見下)並且返回。
否則選擇一個列對象c(見下)。
覆蓋列c(見下)。
對於每個r←D[c],D[D[c]],……,當 r!=c,
設置 Ok<-r;
對於每個j←R[r],R[R[r]],……,當 j!=r,
覆蓋列j(見下);
search(k+1);
設置 r←Ok且 c←C[r];
對於每個j←L[r],L[L[r]],……,當 j!=r,
取消列j的覆蓋(見下)。
取消列c的覆蓋(見下)並且返回。
輸出當前解的操作很簡單:我們連續輸出包含O0、O1、……、Ok-1的行,這裏包含數據對象O的行可以通過輸出N[C[O]]、N[C[R[O]]]、N[C[R[R[O]]]]……來輸出。
爲了選擇一個列對象c,我們可以簡單地設置c<-R[h];這是最左邊沒有覆蓋的列。或者如果我們希望使分支因數達到最小,我們可以設置s<-無窮大,那麼接下來:
對於每個j←R[h],R[R[h]],……,當
j!=h,
如果 S[j]<s 設置 c←j且 s←S[h]。
那麼c就是包含1的序數最小的列(如果不用這種方法減少分支的話,S域就沒什麼用了)。
覆蓋列c的操作則更加有趣:把c從表頭刪除並且從其他列鏈表中去除c鏈表的所有行。
設置 L[R[c]]←L[c]且 R[L[c]]←R[c]。
對於每個i←D[c],D[D[c]],……,當 i!=c,
對於每個j←R[i],R[R{i]],……,當 j!=i,
設置 U[D[j]]←U[j],D[U[j]]←D[j],
並且設置 S[C[j]]←S[C[j]]-1。
操作(1),就是我在本文一開始提到的,在這裏他被用來除去水平、豎直方向上的數據對象。
最後,我們到達了整個算法的尖端,即還原給定的列c的操作。這裏就是鏈表舞蹈的過程:
對於每個i←U[c],U[U[c]],……,當
j!=i,
對於每個j←L[i],L[L[i]],……,當 j!=i,
設置 S[C[j]]←S[C[j]]+1,
並且設置 U[D[j]]←j,D[U[j]]←j。
設置 L[R[c]]←c且 R[L[c]]←c。
注意到還原操作正好與覆蓋操作執行的順序相反,我們利用操作(2)來取消操作(1)。(其實沒必要嚴格限制“後執行的先取消”,由於j可以以任何順序穿過第i行;但是從下往上取消對行的移除操作是非常重要的,因爲我們是從上往下把這些行移除的。相似的,對於第r行從右往左取消列的移除操作也是十分重要的,因爲我們是從左往右覆蓋的。)
|
圖3 圖2中第A列後面的鏈表被覆蓋 |
考慮一下,例如,對圖2表示的數據(3)執行search(0)會發生什麼。通過從其他列移除A的行來將其覆蓋;那麼現在整個結構就成了圖3的樣子。注意現在D列出現了不對稱的鏈接:上面的元素首先被刪除,所以它仍然指向初始的鄰居,但是另一個被刪除的元素指向了列頭。
繼續search(0),當r指向(A,D,G)這一行的A元素時,我們也覆蓋D列和G列。圖4展示了我們進入search(1)時的狀態,這個數據結構代表削減後的矩陣
|
(4) |
現在search(1)將覆蓋B列,而且C列將沒有“1”。因此search(2)將什麼也找不到。接着search(1)會找不到解並返回,圖4的狀態會恢復。外部的過程,search(0),將把圖4變回圖3,而且它會讓r前進到(A,D)行的A元素處。
|
圖4 圖3中D列和G列後的鏈被覆蓋 |
很快就能找到解,並輸出
A |
D |
|
B |
G |
|
C |
E |
F |
如果在選擇c的時候無視S域,會輸出
A |
D |
|
E |
F |
C |
B |
G |
|
以上內容參考了網上很多資料,在此表示謝意