算法筆記(VII) X算法與十字鏈表

近畢業論文吃緊,自然上線的時間少了許多。本來想在上個星期做一下關於的Donald Knuth的algorithm X with Dancing link的,不過本着實踐第一的原則,直到編完了整個程序,我纔想寫一寫關於這個算法,抑或僅僅是一個編程技巧的dancing link。

第一聽說dancing link(由於其數據結構過於巧妙,使得數據結構成爲了算法的名字)是從做ACM的師弟那裏聽到得,當時只是感覺這個東西的用途似曾相識,適合的問題將會很多。簡單的說,這個算法是要從0-1矩陣中選出若干的行,使得這些被提取出的行構成的子矩陣的每一列均有且只有一個1。相信做過0-1規劃和約束滿足問題的同學就會意識到這是一個多麼普適的模型。一大類的問題均可以轉化成0-1整數規劃問題,包括exact cover problem(即Knuth基於1979年一篇關於實現回溯算法的技巧論文,而提出Dancing link去求解的原問題)、set cover problem等等。 這些問題都是NP難(最小覆蓋)或NP完全問題(k個點判定問題),而dancing link求解這類問題可以達到很高的效率。

簡單的來說,Dancing Link 是一個循環十字鏈表,每一個鏈表表示0-1矩陣的非零元素,例如我們可以這樣定義每一個節點

struct node {

node* left, *right, *up ,*down; // 十字鏈表的上下左右指針

int row, col; //表示這個節點對應矩陣的行和列號

}

對於上面的結構,其實不難理解。首先十字鏈表的四個指針域是必須的。其次,下面的兩項,對應矩陣的行號和列號。在實際的編程中,這兩個域是非常有用的,行號將作爲最終的答案返回給我們。而列號將方便我們快速的定位到節點所述的列頭節點,如下就是Knuth給出的示意圖:


圖片

其中節點h是整個dancing link 的入口,而A-G是每個列的頭節點,這些節點不對應0-1矩陣的元素,他們的作用是方便我們操作數據結構:dancing link。特別是每一個列的頭結點有一個特別的域用於記錄當前列的非零節點的數目。相比於上述的節點,以下的節點將一一對應一個矩陣的非零點了。如上圖將對應0-1矩陣(6行7列):

  __1 __2 __3__4__5__6__7_

1[   0     0     1    0    1    1    0 ]

2[   1     0     0    1    0    0    1 ] 

3[   0     1     1     0    0    1   0 ]

4[   1     0     0     1    0    0   0 ]

5[   0     1     0     0    0    0   1 ]

 6[  0     0     0     1    1    0   1 ] 

 

以上的0-1矩陣問題參見[wiki].

下面要說一說,dancing link之所以高效的原因了。一是由於其結構的特殊性;二則是其剪枝的條件非常的強,擴展的狀態節點數目相對於原空間要少很多;Dancing link 的框架仍然是DFS回溯搜索,但是由於其特殊的數據結構省去了大量的棧空間以及遞歸調用開銷,因此可以達到很高的效率。在wiki上,有關於algorithm X的框架說明:

  1. If the matrix A is empty, the problem is solved; terminate successfully.
  2. Otherwise choose a column c (確定的).
  3. Choose a row r such that Arc = 1 (非確定的).
  4. Include row r in the partial solution.
  5. For each column j such that Arj = 1,
    for each row i such that Aij = 1,
    delete row i from matrix A;
    delete column j from matrix A.
  6. Repeat this algorithm recursively on the reduced matrix A.

可以看出,上述過程仍然是一個回溯搜索思路。簡單的來說,我們選擇一個非零元素最少的某一列,選擇其上非零節點對應的行,將其加入我們的解中去,顯然一旦加入了這個節點,爲了保證我們的約束能夠滿足,這個行上的非零點對應的列(即約束條件)即獲得了滿足,因此可以講這些列和這些列上的非零元素對應的行統統刪除。因爲我們的約束條件是:滿足性和唯一性。顯然這麼強的剪枝條件,搜索樹的的節點數目大大縮減了。然後,遞歸的做下去。我們遞歸的base condition是矩陣的列都沒了(矩陣爲空,即所有的約束都滿足了),我們得到一個解集。反之,我們發現某一列全是零,顯然我們已經無法滿足這個約束了,我們的求解失敗了。按照回溯的思路,則回到上一層。

但是要高效的完成2,3,5這3個耗時的步驟,就一番考慮了。需要dancing link。

對於2步,因爲我們有了列頭結點,可以方便的得到那個非零元素最少的列。

對於3步,沿着上述的列頭節點,我們可以很快的找到一個節點。

這裏簡單提到,對應於DFA和NFA,我們的2和3步分別是確定的,和非確定的。這一點也就是爲什麼knuth將algorithm X的原因,因爲他認爲,對於第3步,我們可以有不同的策略進行選取,如A*算法等等,這就對應了算法名稱的X。

考慮第5步,是充分展現dancing link 巧妙的操作了。這一點不編程調試,是不會了解其中的巧妙的。就像是dancing link 表的構造一樣,原論文並沒有給出方法,但是確實要考慮一下如何構造dancing link。爲了簡單說明,我們提醒大家注意:在刪除列和行操作上,dancing link 的刪除是保持列和行的結構的情況下,整塊的從dancing link上刪除掉的,之所以保持原先的結構下整塊的刪除,是爲了保證我們可以方便的回到原先的狀態,下面就是刪除的一個示意圖:

 


圖片

值得注意的是,上述灰色區域是整塊刪除的,也就是說,我們的C列中的元素仍然保持着原先的上下鏈接關係,而那灰色的兩行仍然保持着左右的鏈接關係,同時這些元素其上下左右的指針域仍然有效。方便他們回到dancing link中去。至於爲什麼叫做dancing link,knuth聲稱這個一系列巧妙的操作想一個舞者跳着優雅的舞步,不過在編完程序後,我覺得,其實上述操作很類似於我們過去玩的泡泡堂遊戲。另外,這個十字鏈表也非常像我們的跳舞毯:


圖片

操作的細節就是那個奇怪的操作了:L[R[x]] = L[x]; R[L[x]] =R[x]; 恢復操作則是,R[L[x]]=x ; L[R[x]]= x;參見knuth的原論文和dancing link在搜索中的應用一文。這是dancing link最神奇的操作,因爲它有點反常規的思路,爲什麼又把刪去的有變回來呢?因爲回溯的需要,要恢復預先的狀態。在常規編程中是忌諱的操作,反而在這個算法框架中發揮了奇效。

除了上述的兩個原因,還有一個尚未得到證明的結論,dancing link先搜索那些非零元素最少的列(如上圖就是{1,2,3,5}中的任意一個列,即對應Algorithm X的2步),以下是一個擴展非零元較少的列和較多非零元的搜索樹情況:

root —|— node —— ......

           |— node —— ......

擴展含兩個非零元的列的搜索樹

root —|— node —— ......

           |— node —— ......

           |— node —— ......

           |— node —— ......

擴展含四個非零元的列的搜索樹

這一點,我猜測是因爲,越是擴展節點少的列,生成的節點樹越高,但同時,一旦被剪枝,則潛在被去除的節點數目要很大。往往節點樹隨着擴展的深度大量的減小。knuth通過實驗說明這個想象,但理論上的證明還沒有給出。

dancing link 就簡單的介紹,這僅僅是一個在不重複的覆蓋問題上的應用:也就是包含兩個硬約束:

滿足性和唯一性,

這兩個約束方便我們高效的剪枝,但是在實際生活中,我們往往遇到不完全的覆蓋和可重複覆蓋問題,如選址問題等等,顯然,我們的硬約束編成了一個柔性的約束條件(使用0-1規劃的語言):

儘量保證在選取較少的行,這些行使得非空列的數目佔總列數至少超過給定的比例(一個列可以有很多的非零元素,不要求僅有一個非零項)。這一問題可以稱之爲可重複的不完全覆蓋問題。現在,正是我在考慮的問題,我想在Dancing link(也就是 algorithm X)的框架上,給出這個問題的解。

                                                                                                                                                        2011-4-16

 補充 2011-4-25

在TAOCP中有一章節講述可用正交循環表存儲稀疏矩陣。直覺上感覺有一些相關的東西。這裏簡單的提一下,參見TAOCP 2.2.6。其中正交循環表的結構爲:

struct node*{

node* up, *left;

int row,col;

int value;

}

 因爲少了Dancing link對矩陣的修改,而只是爲了快速的對矩陣進行訪問和定位,所以正交循環表只採用單向鏈表結構,所以並不需要right以及down指針。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章