Dancing Links 在搜索中的應用

                                        momodi

                                   2008 年7 月8  日

1.1    Dancing Links是什麼

     Dancing Links  是knuth 在近幾年寫的一篇論文,在我看來是一類搜索

問題的通用優化, 因此我把它寫下來,希望能在競賽中得到一定的應用。

1.2    Dancing  Links的主要原理是什麼

     Dancing Links 主要是用雙向十字鏈表來存儲稀疏矩陣,來達到在搜索

中的優化。在搜索問題中,所需要存儲的矩陣往往隨着遞歸的加深會變得越

來越稀疏,這種情況用Dancing Links 來存儲矩陣,往往可以取得非常好的

效果。

1.3    這篇論文與knuth論文的不同

     本篇論文是對Dancing Links 在競賽的應用的一個介紹,並列舉了幾個

競賽中的例題。原文可以在此下載到:

     http://www.ocf.berkeley.edu/ jchu/publicportal/sudoku/0011047.pdf

     相對於本文,我更建議去讀者去看knuth的原文,本文可以當作一個參

考。本文還介紹了Sukudo問題和一個Dancing Links的一個變種問題。

                             2     雙向鏈表

2.1    雙向鏈表的存儲結構

     雙向鏈表的存儲結構往往是有一個空的結點來表示頭指針。然後每一

個結點有一個pre值域和一個next值域。如果是十字鏈表的話,結點中指針

的值域會變成四個。

2.2    雙向鏈表的應用

     衆所周知,在單向鏈表中刪除一個結點是非常麻煩,而且低效的。雙向

鏈表有着優美的結構,可以方便的刪除任意一個結點。有如下操作:

                     L[R[x]]  =  L[x],  R[L[x]] =   R[x];                  (1)

3   EXACT COVER PROBLEM                                                    3
相信有寫過雙向鏈表經驗的人對這兩句話一定不會陌生.
2.3    雙向鏈表的恢復
     在雙向鏈表中刪除一個結點只要用(1) 就可以了,然後來看下面這一段
代碼:
                        L[R[x]] =   x, R[L[x]]  =  x;                     (2)
這段代碼會將已經刪除掉的結點,重新添加回雙向鏈表中。第一次看到這段
代碼的你是不是會感覺非常奇妙?你有可能會說在鏈表中進行刪除操作但
是不釋放內存的話是不好的編程習慣,會造成內存溢出錯誤。但是在這裏,
我們要的就是這樣的效果。
     我們在鏈表中刪除,只是進行一個標記,並不進行真正的清空內存操
作。這樣我們後來便可以用(2) 進行恢復操作了。
     你也有可能會說這段代碼會有什麼用?下面就讓我們來真正看看(2) 強
大的威力。(2)纔是Dancing Links  的核心.
                  3    Exact  Cover Problem
3.1    Description
     上節所說的雙向鏈表的恢復操作會在本節中有重要的作用。下面先讓
我們來看一個問題(Exact Cover Problem)。
     給定一個01矩陣, 現在要選擇一些行,使得每一列有且僅有一個1.  例子
如下:
                          0   0  1   0  1  1   0 
                          1   0  0   1  0  0   1
                          0   1  1   0  0  1   0                   (3)
                          1   0  0   1  0  0   0
                          0   1  0   0  0  0   1 
                          0   0  0   1  1  0   1 
對於如上所示的矩陣(3),我們取行集合{1, 4, 5} 便可使每一列有且僅有一
個1。
3.2    Solving  an  exact  cover problem
     很明顯,exact cover problem 是一個無法用多項式算法解決的問題,解
決方法只有搜索!  且看如下搜索代碼:

3  EXACT COVER PROBLEM                                                   4

if  (A is  empty)  { //A  is  the 0-1  matrix.

     the problem   is solved;

     return  ;

}

choose  a  column,  c,

     that  is unremoved   and  has fewest   elements.;      ...(1)

remove  c  and each  row  i that  A[i][c]   == 1  from  matrix  A;     ...(2)

for  (all  row,  r, that  A[r][c]  ==  1)  {

     include  r  in the  partial  solution.;

     for each  j  that  A[r][j]  ==  1 {

         remove   column  j and  each  row  i

              that  A[i][j]   == 1 from  matrix   A. ...(3)

     }

     repeat  this  algorithm   recursively   on the  reduced   matrix  A.

     resume  all  column  j and  row  i that  was  just  removed;  ...(4)

}

resume  c  and i  we removed   at step  (2)    ...(5);

上述代碼是一個普通的搜索過程,如果你覺得比較陌生,可能是因爲跟你平

時所用的搜索方法有些不同,但本質上必定是相同的。

                                                                       1

     我們要做的,是去優化這段代碼,但這段代碼的算法框架已經固定 ,

幾乎沒有可優化的餘地,就算用非遞歸來優化也不會得到多少的效率提升,

反而會使編碼複雜度和出錯率大大增加。

     如何去實現代碼行(1) (2) (3) (4) 是優化的關鍵,也是Dancing Links發

揮其作用的地方。我們可以直觀的去想到一個簡單的方法:

 Step (1):  把矩陣進行掃描,選出未做刪除標記的列中元素個數最少的。如

     果你不理解爲什麼選最少的,可以當它是隨便選取的,不影響正確性。

 Step (2):  對當前列和行做標記,標記其已經刪除。

 Step (3): 同Step (2)

 Step (4):  把相應的標記刪除。

 Step (5): 同Step (4)

   1Step  (1)中有一個巨大的優化,就是選取最少元素的列,但這是搜索優化問題,與我們討論的重

點Dancing  Links沒多大關係,固不做過多論述

3   EXACT COVER PROBLEM                                                    5
然而,這個方法很明顯是非常低效的,做標記是快速的。但是相應的帶來
的問題,是查找的時候會變得非常慢。複雜度始終是O(n)(n爲矩陣大小)。
在step (1) (2) (3) (4) (5)  中我們都用到了查找操作,所以我們不能忽視查
找的複雜度。在這個實現方式中,把查找的效率提升多少,整個程序便可提
升多少。
     我們再做觀察,還可發現,矩陣隨着遞歸的深入便會變得越來越稀疏。
而我們所對應的查找操作,卻沒有利用這一特性,一直在盲目的進行查找。
     或許有讀者會自然而然的想到鏈表。鏈表可以高效的支持刪除操作,鏈
表的長度也是隨着刪除操作的執行而變短,不會存的上述做法的問題。
     從前面的論述,我們知道了程序的瓶頸在於查找操作。因爲矩陣會越來
越稀疏,所以我們可以直觀的感覺到把鏈表用在此處,會及大的提高效率。
     有了鏈表,我們的查找操作只用到了遍歷鏈表,因爲已經被標記刪除的
結點是不存在在鏈表中的。很顯然用鏈表的話查找操作的效率已經提升到
了極致,不可能再提升了。
     有人可能會發現問題,我們在鏈表中刪除結點是容易,可是恢復容易
嗎?Step (4) (5) 該如何實現呢?哈哈,請看本論文開頭對雙向鏈表的介紹,
看似無用的雙向鏈表的恢復操作,在這裏不是正好用上嗎?
     好了,到這裏,已經把Dancing Links基本介紹完了。如果你真正理解了,
可能會驚歎,這麼簡單呀!這就是Dancing  Links嗎?但是Dancing  Links的
強大之處還得等到自己實踐之後才能體會到。下面我們來細細展開Dancing
Links。
3.3    The  Dance  Steps
     一個比較好的實現方式是用數組來模擬鏈表,這樣也可方便的建立矩
陣,也可以加快運行速度。對每一個對象,記錄如下幾個信息:
   *  L[x], R[x], U[x], D[x], C[x];
   *  雙向十字鏈表用LRUD來記錄,LR來記錄左右方向的雙向鏈表,UD來
      記錄上下方向的雙向鏈表。
   *  head 指向總的頭指針,head通過LR來貫穿的列指針頭。
   *  每一列都有列指針頭。C[x]是指向其列指針頭的地址。行指針頭可有可
      無,在我的實現中沒有顯示的表示出來。在某些題目中,加個行指針頭還
      是很有必要的。
   *  另外,開兩個數組S[x], O[x];  S[x]記錄列鏈表中結點的總數。O[x]用來
      記錄搜索結果。
3   EXACT COVER PROBLEM                                                      6
                             圖1:  矩陣A的表示
0-1矩陣(3)的存儲圖形如圖(1)所示。
   *  h代表總的頭鏈表head.
   *  ABCDEFG爲列的指針頭。
     Exact Cover Problem的完整c代碼如下所示,調用過程爲dfs(0)。
void  remove(const     int  &c)  {
//remove   column   c  and  all  row  i that  A[i][c]   ==  1
     L[R[c]]   =  L[c];
     R[L[c]]   = R[c];
     //remove   column   c;
     for  (int  i  = D[c];   i !=  c;  i =  D[i])  {
     //remove   i  that  A[i][c]   ==  1
          for  (int  j  = R[i];   j !=  i;  j =  R[j])  {
               U[D[j]]   = U[j];
               D[U[j]]   = D[j];
               --S[C[j]];
               //decrease    the  count  of  column   C[j];
3   EXACT COVER PROBLEM                                                   7
          }
     }
}
void  resume(const    int &c)  {
     for  (int  i = U[c];   i !=  c; i  = U[i])  {
          for (int  j  = L[i];  j  != i;  j =  L[j])  {
              ++S[C[j]];
              U[D[j]]   = j;
              D[U[j]]   = j;
          }
     }
     L[R[c]]  =  c;
     R[L[c]]  =  c;
}
bool  dfs(const   int  &k)  {
     if  (R[head]  ==  head)  {
          One of  the  answers  has  been  found.
          return  true;
     }
     int  s(maxint),   c;
     for  (int  t = R[head];   t  != head;  t  = R[t])  {
     //select   the column   c which  has  the  fewest  number   of element.
          if (S[t]   < s)  {
               s = S[t];
               c = t;
          }
     }
     remove(c);
     for  (int  i = D[c];   i !=  c; i  = D[i])  {
          O[k]  = i;//record   the  answer.
          for (int  j  = R[i];  j  != i;  j =  R[j])  {
              remove(C[j]);
          }
          if (dfs(k   + 1))  {
4   SUDOKU TO EXACT COVER PROBLEM                                          8
               return  true;
          }
          for  (int  j =  L[i];  j  != i;  j =  L[j])  {
               resume(C[j]);
          }
     }
     resume(c);
     return  false;
}
函數remove刪除了矩陣中的列c及其對應的行。而函數resume則是還原它們,
可能有些讀者會注意到這兩個函數裏面順序的問題。在這裏我遵循的原則
是先刪除的後還原,後刪除的先還原。當然,改變其中的某些順序是不影響
正確性的。這樣寫是爲了遵循一定的原則。
     另外還有一個小問題是我覺得應該提及的。在dfs過程中我們如何選
擇column  c,是一個非常重要的問題。這個程序中我們選擇的是元素最少
的c,這個優化是顯然的。
     但如果是平局,我們該選擇哪一個呢?在這個程序中,這一點並沒有體
現出來,如果是平局的話,那這段程序相當於隨便選取了一列。這個問題在
平時應該用可能會極大的影響效率。只能根據具體題目具體來判定如何解
決了。做題的時候如果你發現你寫的Dancing  Links比別人的慢一些,那很
有可能就是因爲這裏的問題了。
3.4    Hust  Online Judge Problem 1017
     http://acm.hust.edu.cn/thanks/problem.php?id=1017
     這是一道exact cover problem的一個題目。想測試代碼正確性的話可以
在此提交測試。
          4    Sudoku to exact cover problem
4.1    What  is  Sudoku?
     Sudoku是一個最近幾年非常流行的益智遊戲。它的中文名是數獨。
     Sudoku問題是在一個9 * 9的格子上,內部有3 * 3的9個小塊。你的目標
是把每一個小格填上1-9中的某一個數,使得每一行、每一列、每一個小塊都
4   SUDOKU TO EXACT COVER PROBLEM                                          9
包含1-9每個數各一次。在初始情況下,有一些小格是已經填好了的,你所要
做的是把剩下的填好。
     Sudoku的規則非常簡單,但是卻很有挑戰性。
     當今衆多手機中已經預裝了Sudoku遊戲。很多linux桌面系統,比
如Gnome,也預裝了Sudoku。在網上也有很多Sudoku的網站,可以在網
上搜索”Sudoku”來找到一些。今年的ICPC final總決賽的時候,每天的新聞
紙背面也是一道數獨題。答案在第二天的新聞紙中給出。
4.2    How to  Solve Sudoku  Puzzle?
     Sudoku的解法很多,大體上是分爲:
   *  搜索
   *  構造
   *  搜索+構造
     對於普通的9 * 9的Sudoku,最簡單的搜索過程是肯定會超時的。我們
需要加優化,搜索中比較常用的,優化搜索順序。在這裏可以起到非常關鍵
的作用。
下面介紹一種搜索策略:
     每次判斷81個小格的可能性,從中選出可能性最少的來搜索。通
過位運算等種種優化常數的方法。還是可以達到一個比較理想的效果
的。pku3074可以在600ms左右用這種方法ac。
     我們在這個策略的基礎上增加一種搜索方式:
     判斷某個數在某個區域內的可能性。
     加上這兩個搜索方式後,Sudoku就可以比較快的出解了。對於9  *
9的Sudoku幾乎是瞬出。pku3074可以做到200ms以內。(常數優化的比較
好)。
     對於Sudoku也的構造過程,這裏不作過多介紹。Sudoku的構造方法非
常多,但大多是人工構造的方法。用計算機去構造,是一個非常繁重的過程,
根本不適合於OI和ICPC。
     對於在搜索過程中加上部分構造過程增加效率。這裏也不作過多介紹。
方法也是很多,有興趣的可以去網上查看相關論文。
     據說今年MCM (美國數學建模競賽)的B類題目就是Sudoku問題。聽我
們集訓隊的yyt說,他們便是寫了一個構造Sudoku的程序得了一等獎,不過
程序實在是太長了,1k行+?
4   SUDOKU TO EXACT COVER PROBLEM                                         10
4.3    Dancing  Links在解決Sudoku這類問題上的優勢
     很多這種覆蓋性的搜索問題都可以轉化爲exact cover problem。然後我
們便可以用Dancing Links來解決之。這也是Dancing Links強大的地方。如
果你認爲Dancing Links只是解決一些特殊問題的特殊方法,那你就大錯特
錯了。非常多問題可以轉化成exact cover problem或類似於exact cover  的問
題。
     Dancing Links有什麼優勢呢?
   *  Dancing  Links是一種通用的搜索方法,不用你自己去實現搜索過程。
      不用你對每一道題目都設計數據結構,來設計搜索方法。你所要做的
      只是去建一個模型。
   *  Dancing Links不用你自己去想優化搜索的方法。你在建模的和轉化的
      過程中,Dancing Links往往已經爲你想好了你都想不到的優化。
   *  Dancing Links可以減小常數,甚至可以在複雜度上去除一個係數。極
      大的提高了運行效率。
   *  Dancing Links代碼非常短,方便在比賽現場敲,極大的提高了代碼速
      度。也減少了debug的時間。
搜索問題千變萬化,剪枝非常難想,就算想到了,也很難知道這是不是有用
的剪枝。在用Dancing  Links來解決這類問題的時候,你會發現這一類的很
多問題都可以轉化成你以前熟悉或做過的題目。那剪枝就不再是問題了。因
爲你已經把剪枝提交想好了。
     實現也不再是問題了,只要轉化正確,再加上Dancing Links的模版。代
碼正確性和實現速度都有了質的提升。做搜索題就彷彿做網絡流題目一樣
了。
     建模+ 模版== AC
     哈哈,是不是很讓人心動。那就來看一下如何轉化sudoku吧。
4.4    轉化模型
     對於一個9 * 9的數獨,建立如下的矩陣。:
     行:
     一共9 * 9 * 9 == 729行。一共9 * 9小格,每一格有9種可能性(1 - 9),
每一種可能都對應着一行。
     列:
5   EXACT COVER PROBLEM 變種                                                  11
     一共(9 + 9 + 9) * 9 + 81 == 324 種前面三個9分別代表着9行9列和9小
塊。乘以9的意思是9種可能,因爲每種可能只可以選擇一個。81代表着81個
小格,限制着每一個小格只可以放一個地方。
     這樣我們把矩陣建立起來,把行和列對應起來之後,行i可以放在列j上
就把A[i][j]設爲1否則設爲0。然後套用Exact Cover Problem的定義:選擇一
些行,使得每一列有且僅有一個1。哈哈,是不是對應着sudoku的一個解?
     前面我已經說過Sudoku的搜索模型,現在再結合轉化後的模型,你會不
會覺得本質上是一樣的呢?其實是一樣的。
     請注意每一列只能有一個1,而且必需有一個1。
     我們把列分成兩類的話,一類是代表着每一個小格的可能性,另一類是
代表着每個區域的某個數的可能性。第一類是式子中的81,第二類是(9 + 9
+ 9) * 9這一部分。
     這樣我們所選擇的行就對應着答案,而且因爲列的限制,這個答案也是
符合Sudoku的要求的。
     那你也有可能會說,Dancing Links的優化體現在哪裏呢?試想,這個矩
陣是非常大的(324 * 729),如果不用Dancing Links來解,可能出解嗎?
4.5    Pku  Online  Judge Problem 3076  3074
     http://acm.pku.edu.cn/JudgeOnline/problem?id=3074
     http://acm.pku.edu.cn/JudgeOnline/problem?id=3076
     3074是一個9 * 9的數獨。
     3076是一個16 * 16的數獨。
     這兩道題目用前文論述的方法來解,效率非常高。Problem Status裏面
前幾名的AC Code基本都是用Dancing Links來解的。(不信你發mail問問他
們咯).
     3076裏面有些人是0ms AC的。而且代碼長度非常短,我十分懷疑他們
是直接交的數據。也希望POJ的管理員能夠把數據變一下吧。
               5     Exact cover problem  變種
5.1    Abstract
     瞭解本節需要知道A*的一些知識。如果你沒有做過有關A*的題目,可
能在看本節的時候有些困難。因爲篇幅,時間和精力都不允許我再去介
紹A*。所以我下面所介紹的知識就默認你已經很精通A*啦,不會的集訓隊
5   EXACT COVER PROBLEM 變種                                                 12
隊員自己去找資料吧。但是我推薦在你對A*有所瞭解之後,再回過頭來看
一下lrj的書對A*在競賽中應用的介紹。
     推薦學習A*所要做的題目:
   *  第k短路
   *  15數碼
   *  第k短簡單路。
5.2    Description
     我們把exact cover problem的定義改一下:在0-1矩陣中選取最少的行,
使得每一列最少有一個1。
     我們可以想成一種二分圖的支配集的模型:
左邊的點可以支配右邊的點,然後我們要選最少的左邊的點,使得右邊的點
都被支配了。
     我們用A* + Dancing Links來解決此題。
     A*是算法的框架,Dancing Links來實現數據結構,和優化常數。
     A*中比較重要的是h函數的設計,h函數最好設計成離線的,因爲計
算h函數的複雜度也在很大程度上決定了程序的效率。那h函數如果想不出
離線的呢?那我們就應該在保證h函數儘量單調的情況下,來減少計算h函數
的常數。
     因爲h函數是與矩陣有關係的,所以對於稀疏的矩陣來說,選擇Dancing
Links是非常有必要的。隨着遞歸的深入,Dancing Links的優勢會體現的越
來越明顯。
5.3    H  function
     對於上節所描述的模型,我設計的h函數是這樣的:
     ans來記錄h函數的數值。
     對當前矩陣來說,選擇一個未被控制的列,很明顯該列最少需要1個行
來控制,所以我把ans++。該列被控制後,我把它所對應的行,全部設爲已
經選擇,並把這些行對應的列也設爲被控制。繼續選擇未被控制的列,直到
沒有這樣的列。
     通過這樣的操作,就可以求出一個粗略的h函數。
5   EXACT COVER PROBLEM 變種                                                  13
 5.4   Method
     有了h函數,剩下的就好說了。不管用A*也好,用IDA*也好,實現是比
較簡單的。
     我的實現方法是IDA*,因爲對於一個矩陣進行判重和hash還是比較麻
煩的。再加上本題答案都比較小。所以IDA*是一個比較好的選擇。
 5.5   Pku  Online Judge  Problem  1084
     http://acm.pku.edu.cn/JudgeOnline/problem?id=3076f
     這道題目也是lrj書上一道習題。題目模型就是我上面所說的。
     我寫的程序實驗可以過6 0這樣的數據。推薦寫個程序來實踐一下。

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/code_beauty/archive/2010/07/07/5717918.aspx

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