Dancing Links算法(舞蹈鏈)

原文鏈接:跳躍的舞者,舞蹈鏈(Dancing Links)算法——求解精確覆蓋問題

作者:萬倉一黍

出處:http://grenet.cnblogs.com/

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

算法實踐——舞蹈鏈(Dancing Links)算法求解數獨

精確覆蓋問題的定義:給定一個由0-1組成的矩陣,是否能找到一個行的集合,使得集合中每一列都恰好包含一個1

例如:如下的矩陣

clip_image002

就包含了這樣一個集合(第1、4、5行)

 

如何利用給定的矩陣求出相應的行的集合呢?我們採用回溯法

 

矩陣1:clip_image002

 

先假定選擇第1行,如下所示:

clip_image002[4]

如上圖中所示,紅色的那行是選中的一行,這一行中有3個1,分別是第3、5、6列。

由於這3列已經包含了1,故,把這三列往下標示,圖中的藍色部分。藍色部分包含3個1,分別在2行中,把這2行用紫色標示出來

根據定義,同一列的1只能有1個,故紫色的兩行,和紅色的一行的1相沖突。

那麼在接下來的求解中,紅色的部分、藍色的部分、紫色的部分都不能用了,把這些部分都刪除,得到一個新的矩陣

矩陣2:clip_image002[6]

行分別對應矩陣1中的第2、4、5行

列分別對應矩陣1中的第1、2、4、7列

 

於是問題就轉換爲一個規模小點的精確覆蓋問題

 

在新的矩陣中再選擇第1行,如下圖所示

clip_image002[8]

還是按照之前的步驟,進行標示。紅色、藍色和紫色的部分又全都刪除,導致新的空矩陣產生,而紅色的一行中有0(有0就說明這一列沒有1覆蓋)。說明,第1行選擇是錯誤的

 

那麼回到之前,選擇第2行,如下圖所示

clip_image002[10]

按照之前的步驟,進行標示。把紅色、藍色、紫色部分刪除後,得到新的矩陣

矩陣3:clip_image002[12]

行對應矩陣2中的第3行,矩陣1中的第5行

列對應矩陣2中的第2、4列,矩陣1中的第2、7列

 

由於剩下的矩陣只有1行,且都是1,選擇這一行,問題就解決

於是該問題的解就是矩陣1中第1行、矩陣2中的第2行、矩陣3中的第1行。也就是矩陣1中的第1、4、5行

 

在求解這個問題的過程中,我們第1步選擇第1行是正確的,但是不是每個題目第1步選擇都是正確的,如果選擇第1行無法求解出結果出來,那麼就要推倒之前的選擇,從選擇第2行開始,以此類推

 

從上面的求解過程來看,實際上求解過程可以如下表示

1、從矩陣中選擇一行

2、根據定義,標示矩陣中其他行的元素

3、刪除相關行和列的元素,得到新矩陣

4、如果新矩陣是空矩陣,並且之前的一行都是1,那麼求解結束,跳轉到6;新矩陣不是空矩陣,繼續求解,跳轉到1;新矩陣是空矩陣,之前的一行中有0,跳轉到5

5、說明之前的選擇有誤,回溯到之前的一個矩陣,跳轉到1;如果沒有矩陣可以回溯,說明該問題無解,跳轉到7

6、求解結束,把結果輸出

7、求解結束,輸出無解消息

 

從如上的求解流程來看,在求解的過程中有大量的緩存矩陣和回溯矩陣的過程。而如何緩存矩陣以及相關的數據(保證後面的回溯能正確恢復數據),也是一個比較頭疼的問題(並不是無法解決)。以及在輸出結果的時候,如何輸出正確的結果(把每一步的選擇轉換爲初始矩陣相應的行)。

 

於是算法大師Donald E.Knuth(《計算機程序設計藝術》的作者)出面解決了這個方面的難題。他提出了DLX(Dancing Links X)算法。實際上,他把上面求解的過程稱爲X算法,而他提出的舞蹈鏈(Dancing Links)實際上並不是一種算法,而是一種數據結構。一種非常巧妙的數據結構,他的數據結構在緩存和回溯的過程中效率驚人,不需要額外的空間,以及近乎線性的時間。而在整個求解過程中,指針在數據之間跳躍着,就像精巧設計的舞蹈一樣,故Donald E.Knuth把它稱爲Dancing Links(中文譯名舞蹈鏈)。

 

Dancing Links的核心是基於雙向鏈的方便操作(移除、恢復加入)

我們用例子來說明

假設雙向鏈的三個連續的元素,A1、A2、A3,每個元素有兩個分量Left和Right,分別指向左邊和右邊的元素。由定義可知

A1.Right=A2,A2.Right=A3

A2.Left=A1,A3.Left=A2

在這個雙向鏈中,可以由任一個元素得到其他兩個元素,A1.Right.Right=A3,A3.Left.Left=A1等等

 

現在把A2這個元素從雙向鏈中移除(不是刪除)出去,那麼執行下面的操作就可以了

A1.Right=A3,A3.Left=A1

那麼就直接連接起A1和A3。A2從雙向鏈中移除出去了。但僅僅是從雙向鏈中移除了,A2這個實體還在,並沒有刪除。只是在雙向鏈中遍歷的話,遍歷不到A2了。

那麼A2這個實體中的兩個分量Left和Right指向誰?由於實體還在,而且沒有修改A2分量的操作,那麼A2的兩個分量指向沒有發生變化,也就是在移除前的指向。即A2.Left=A1和A2.Right=A3

 

如果此時發現,需要把A2這個元素重新加入到雙向鏈中的原來的位置,也就是A1和A3的中間。由於A2的兩個分量沒有發生變化,仍然指向A1和A3。那麼只要修改A1的Right分量和A3的Left就行了。也就是下面的操作

A1.Right=A2,A3.Left=A2

 

仔細想想,上面兩個操作(移除和恢復加入)對應了什麼?是不是對應了之前的算法過程中的關鍵的兩步?

移除操作對應着緩存數據、恢復加入操作對應着回溯數據。而美妙的是,這兩個操作不再佔用新的空間,時間上也是極快速的

 

在很多實際運用中,把雙向鏈的首尾相連,構成循環雙向鏈

 

Dancing Links用的數據結構是交叉十字循環雙向鏈

而Dancing Links中的每個元素不僅是橫向循環雙向鏈中的一份子,又是縱向循環雙向鏈的一份子。

因爲精確覆蓋問題的矩陣往往是稀疏矩陣(矩陣中,0的個數多於1),Dancing Links僅僅記錄矩陣中值是1的元素。

 

Dancing Links中的每個元素有6個分量

分別:Left指向左邊的元素、Right指向右邊的元素、Up指向上邊的元素、Down指向下邊的元素、Col指向列標元素、Row指示當前元素所在的行

 

Dancing Links還要準備一些輔助元素(爲什麼需要這些輔助元素?沒有太多的道理,大師認爲這能解決問題,實際上是解決了問題)

Ans():Ans數組,在求解的過程中保留當前的答案,以供最後輸出答案用。

Head元素:求解的輔助元素,在求解的過程中,當判斷出Head.Right=Head(也可以是Head.Left=Head)時,求解結束,輸出答案。Head元素只有兩個分量有用。其餘的分量對求解沒啥用

C元素:輔助元素,稱列標元素,每列有一個列標元素。本文開始的題目的列標元素分別是C1、C2、C3、C4、C5、C6、C7。每一列的元素的Col分量都指向所在列的列標元素。列標元素的Col分量指向自己(也可以是沒有)。在初始化的狀態下,Head.Right=C1、C1.Right=C2、……、C7.Right=Head、Head.Left=C7等等。列標元素的分量Row=0,表示是處在第0行。

 

下圖就是根據題目構建好的交叉十字循環雙向鏈(構建的過程後面的詳述)

image

就上圖解釋一下

每個綠色方塊是一個元素,其中Head和C1、C2、……、C7是輔助元素。橙色框中的元素是原矩陣中1的元素,給他們標上號(從1到16)

左側的紅色,標示的是行號,輔助元素所在的行是0行,其餘元素所在的行從1到6

每兩個元素之間有一個雙向箭頭連線,表示雙向鏈中相鄰兩個元素的關係(水平的是左右關係、垂直的是上下關係)

單向的箭頭並不是表示單向關係,而因爲是循環雙向鏈,左側的單向箭頭和右側的單向箭頭(上邊的和下邊的)組成了一個雙向箭頭,例如元素14左側的單向箭頭和元素16右側的單項箭頭組成一個雙向箭頭,表示14.Left=16、16.Right=14;同理,元素14下邊的單項箭頭和元素C4上邊的單向箭頭組成一個雙向箭頭,表示14.Down=C4、C4.Up=14

 

接下來,利用圖來解釋Dancing Links是如何求解精確覆蓋問題

1、首先判斷Head.Right=Head?若是,求解結束,輸出解;若不是,求解還沒結束,到步驟2(也可以判斷Head.Left=Head?)

2、獲取Head.Right元素,即元素C1,並標示元素C1標示元素C1,指的是標示C1、和C1所在列的所有元素、以及該元素所在行的元素,並從雙向鏈中移除這些元素)。如下圖中的紫色部分。

image

如上圖可知,行2和行4中的一個必是答案的一部分(其他行中沒有元素能覆蓋列C1),先假設選擇的是行2

 

3、選擇行2(在答案棧中壓入2),標示該行中的其他元素(元素5和元素6)所在的列首元素,即標示元素C4標示元素C7,下圖中的橙色部分。

注意的是,即使元素5在步驟2中就從雙向鏈中移除,但是元素5的Col分量還是指向元素C4的,這裏體現了雙向鏈的強大作用。

image

 

把上圖中的紫色部分和橙色部分移除的話,剩下的綠色部分就如下圖所示

image

一下子空了好多,是不是轉換爲一個少了很多元素的精確覆蓋問題?,利用遞歸的思想,很快就能寫出求解的過程來。我們繼續完成求解過程

 

4、獲取Head.Right元素,即元素C2,並標示元素C2。如下圖中的紫色部分。

image

如圖,列C2只有元素7覆蓋,故答案只能選擇行3

 

5、選擇行3(在答案棧中壓入3),標示該行中的其他元素(元素8和元素9)所在的列首元素,即標示元素C3標示元素C6,下圖中的橙色部分。

image

把上圖中的紫色部分和橙色部分移除的話,剩下的綠色部分就如下圖所示

image

 

6、獲取Head.Right元素,即元素C5,元素C5中的垂直雙向鏈中沒有其他元素,也就是沒有元素覆蓋列C5。說明當前求解失敗。要回溯到之前的分叉選擇步驟(步驟2)。那要回標列首元素(把列首元素、所在列的元素,以及對應行其餘的元素。並恢復這些元素到雙向鏈中),回標列首元素的順序是標示元素的順序的反過來。從前文可知,順序是回標列首C6回標列首C3回標列首C2回標列首C7回標列首C4。表面上看起來比較複雜,實際上利用遞歸,是一件很簡單的事。並把答案棧恢復到步驟2(清空的狀態)的時候。又回到下圖所示

image

 

7、由於之前選擇行2導致無解,因此這次選擇行4(再無解就整個問題就無解了)。選擇行4(在答案棧中壓入4),標示該行中的其他元素(元素11)所在的列首元素,即標示元素C4,下圖中的橙色部分。

image

把上圖中的紫色部分和橙色部分移除的話,剩下的綠色部分就如下圖所示

image

 

8、獲取Head.Right元素,即元素C2,並標示元素C2。如下圖中的紫色部分。

image

如圖,行3和行5都可以選擇

 

9、選擇行3(在答案棧中壓入3),標示該行中的其他元素(元素8和元素9)所在的列首元素,即標示元素C3標示元素C6,下圖中的橙色部分。

image

把上圖中的紫色部分和橙色部分移除的話,剩下的綠色部分就如下圖所示

image

 

10、獲取Head.Right元素,即元素C5,元素C5中的垂直雙向鏈中沒有其他元素,也就是沒有元素覆蓋列C5。說明當前求解失敗。要回溯到之前的分叉選擇步驟(步驟8)。從前文可知,回標列首C6回標列首C3。並把答案棧恢復到步驟8(答案棧中只有4)的時候。又回到下圖所示

image

 

11、由於之前選擇行3導致無解,因此這次選擇行5(在答案棧中壓入5),標示該行中的其他元素(元素13)所在的列首元素,即標示元素C7,下圖中的橙色部分。

image

把上圖中的紫色部分和橙色部分移除的話,剩下的綠色部分就如下圖所示

image

 

12、獲取Head.Right元素,即元素C3,並標示元素C3。如下圖中的紫色部分。

image

 

13、如上圖,列C3只有元素1覆蓋,故答案只能選擇行3(在答案棧壓入1)。標示該行中的其他元素(元素2和元素3)所在的列首元素,即標示元素C5標示元素C6,下圖中的橙色部分。

image

把上圖中的紫色部分和橙色部分移除的話,剩下的綠色部分就如下圖所示

image

 

14、因爲Head.Right=Head。故,整個過程求解結束。輸出答案,答案棧中的答案分別是4、5、1。表示該問題的解是第4、5、1行覆蓋所有的列。如下圖所示(藍色的部分)

image

 

從以上的14步來看,可以把Dancing Links的求解過程表述如下

 

1、Dancing函數的入口

2、判斷Head.Right=Head?,若是,輸出答案,返回True,退出函數。

3、獲得Head.Right的元素C

4、標示元素C

5、獲得元素C所在列的一個元素

6、標示該元素同行的其餘元素所在的列首元素

7、獲得一個簡化的問題,遞歸調用Daning函數,若返回的True,則返回True,退出函數。

8、若返回的是False,則回標該元素同行的其餘元素所在的列首元素,回標的順序和之前標示的順序相反

9、獲得元素C所在列的下一個元素,若有,跳轉到步驟6

10、若沒有,回標元素C,返回False,退出函數。

 

 

 

之前的文章的表述,爲了表述簡單,採用面向對象的思路,說每個元素有6個分量,分別是Left、Right、Up、Down、Col、Row分量。

但在實際的編碼中,用數組也能實現相同的作用。例如:用Left()表示所有元素的Left分量,Left(1)表示元素1的Left分量

在前文中,元素分爲Head元素、列首元素(C1、C2等)、普通元素。在編碼中,三種元素統一成一種元素。如上題,0表示Head元素,1表示元素C1、2表示元素C2、……、7表示元素C7,從8開始表示普通元素。這是統一後,編碼的簡便性。利用數組的下標來表示元素,宛若指針一般。

 

 

下面是代碼的講解

 

1、該類的一些變量


    

Private Left() As Integer, Right() As Integer, Up() As Integer, Down() As Integer 
    Private Row() As Integer, Col() As Integer 

    Private _Head As Integer 

    Private _Rows As Integer, _Cols As Integer, _NodeCount As Integer 
    Private Ans() As Integer

 

前兩行表示每個元素的六個分量,用數組表示;_Head表示元素Head,在類中初始化時令其等於0;_Rows表示矩陣的行數,_Cols表示矩陣的列數,_NodeCount表示元素的個數;Ans()用於存放答案

 

 

2、求解的主函數,Dance函數,是個遞歸函數,參數K表示當前的調用層數。
    

Public Function Dance() As Integer() 
        Return IIf(Dance(0) = True, Ans, Nothing) 
    End Function  

    Private Function Dance(ByVal K As Integer) As Boolean 

        Dim C1 As Integer = Right(_Head) 
        If (C1 = _Head) Then 
            ReDim Preserve Ans(K - 1) 
            Return True 
        End If


        RemoveCol(C1) 

        Dim I As Integer, J As Integer 

        I = Down(C1) 
        Do While I <> C1 
            Ans(K) = Row(I) 

            J = Right(I) 
            Do While J <> I 
                RemoveCol(Col(J)) 
                J = Right(J) 
            Loop 

            If Dance(K + 1) Then Return True 

            J = Left(I) 
            Do While J <> I 
                ResumeCol(Col(J)) 
                J = Left(J) 
            Loop 

            I = Down(I) 
        Loop  

        ResumeCol(C1) 
        Return False 
    End Function

其中第一個函數Dance是對外開放的函數,它通過調用Dance(0)來求解問題,根據返回值來決定返回答案(當爲True的時候)還是返回空(當爲False的時候)

第二個函數是求解的主函數。首先通過Right(_Head)獲得_Head元素的右元素。判斷是否等於自身,若是,求解結束,因爲答案保存在Ans(0)到Ans(K-1)中,所以先把答案數組中多餘的部分去除(利用Redim語句)。

RemoveCol函數是用來標示列首元素的,ResumeCol函數用來回標列首元素的,其中通過Col(J)獲得J元素的列首元素。在函數中有個很聰明的設計,在標示列首元素時,順序是從I元素的右側元素開始;而在回標列首元素時,順序是從I元素的左側元素開始,正好順序和標示列首元素的順序相反。

在調用Dance(K+1)前,把當前選中的行保存到Ans(K)中,當Dance(K+1)返回True時,說明遞歸調用獲得正確的解,那直接返回True;返回False時,說明當前選擇的行不正確,回標列首元素,獲得下一個元素。

當元素C1中所在的列的其餘元素所選定的行沒有求解正確的遞歸函數時(包括C1列沒有其餘的元素),說明當前的求解失敗,回標列首元素C1,返回False

 

 

3、求解的輔助函數,RemoveCol函數,標示列首函數
    

Public Sub RemoveCol(ByVal Col As Integer) 

        Left(Right(Col)) = Left(Col) 
        Right(Left(Col)) = Right(Col) 

        Dim I As Integer, J As Integer 

        I = Down(Col) 
        Do While I <> Col 
            J = Right(I) 
            Do While J <> I 
                Up(Down(J)) = Up(J) 
                Down(Up(J)) = Down(J) 
                J = Right(J) 
            Loop 

            I = Down(I) 
        Loop 

    End Sub

首先,利用Left(Right(Col)) = Left(Col) 和Right(Left(Col)) = Right(Col) 把列首元素Col從水平雙向鏈中移除出去。再依次把Col所在的列的其餘元素的所在行的其餘元素從垂直雙向鏈中移除出去,利用的是Up(Down(J)) = Up(J) 和Down(Up(J)) = Down(J)。找尋Col所在列的其餘元素的順序是從下邊(Down分量)開始,移除所在行其餘元素的順序是從右邊(Right分量)開始 。可以參考之前的圖中的紫色部分。

 

4、求解的輔助函數,ResumeCol函數,回標列首函數


    

Public Sub ResumeCol(ByVal Col As Integer) 

        Left(Right(Col)) = Col 
        Right(Left(Col)) = Col 

        Dim I As Integer, J As Integer 

        I = Up(Col) 

        Do While (I <> Col) 
            J = Right(I) 
            Do While J <> I 
                Up(Down(J)) = J 
                Down(Up(J)) = J 
                J = Right(J) 
            Loop 
            I = Up(I) 
        Loop 

    End Sub

 

首先,利用Left(Right(Col)) = Col 和Right(Left(Col)) = Col 把列首元素Col恢復到水平雙向鏈中。再依次把Col所在的列的其餘元素的所在行的其餘元素恢復到垂直雙向鏈中,利用的是Up(Down(J)) = J 和Down(Up(J)) = J。找尋Col所在列的其餘元素的順序是從上邊(Up分量)開始(和之前的RemoveCol函數相反),恢復所在行其餘元素的順序是從右邊(Right分量)開始 。

 

5、類的初始化函數


   

 Public Sub New(ByVal Cols As Integer) 
        ReDim Left(Cols), Right(Cols), Up(Cols), Down(Cols), Row(Cols), Col(Cols), Ans(Cols) 
        Dim I As Integer 

        Up(0) = 0 
        Down(0) = 0 
        Right(0) = 1 
        Left(0) = Cols 

        For I = 1 To Cols 
            Up(I) = I 
            Down(I) = I 
            Left(I) = I - 1 
            Right(I) = I + 1 
            Col(I) = I 
            Row(I) = 0 
        Next 

        Right(Cols) = 0 

        _Rows = 0 
        _Cols = Cols 
        _NodeCount = Cols 
        _Head = 0 
    End Sub

 

初始化函數有一個參數Cols,表示這個矩陣的列數。

初始化的時候,由於沒有傳入矩陣元素的信息。因此,在該函數中先把輔助元素完成

0表示Head元素,1-Cols表示Cols個列的列首元素

第一句,重定義六個分量的數組,表示Head元素和列首元素的六個分量。

Right(0) = 1表示Head元素的Right分量指向列首元素1(第1列的列首元素);Left(0) = Cols表示Head元素的Left分量指向列首元素Cols(第Cols列的列首元素)

後面的一段循環,給每個列首元素指定六個分量。Up和Down分量指向自己,Left分量指向左邊的列首元素(I-1),Right分量指向右邊的列首元素(I+1),Col分量指向自己,Row分量爲0,參看前面的圖。最後Right(Cols)=0,Cols列的列首元素的Right分量指向Head元素

其後是一些變量的賦值。把_Head賦值爲0,表示0爲Head元素,是爲了後面的代碼的直觀性

 

 

6、添加矩陣元素的函數


    

Public Sub AppendLine(ByVal ParamArray Value() As Integer) 
        _Rows += 1 
        If Value.Length = 0 Then Exit Sub 

        Dim I As Integer, K As Integer = 0 

        For I = 0 To Value.Length - 1 
            If Value(I) = 1 Then 
                _NodeCount += 1 
                ReDim Preserve Left(_NodeCount) 
                ReDim Preserve Right(_NodeCount) 
                ReDim Preserve Up(_NodeCount) 
                ReDim Preserve Down(_NodeCount) 
                ReDim Preserve Row(_NodeCount) 
                ReDim Preserve Col(_NodeCount) 
                ReDim Preserve Ans(_NodeCount) 
                If K = 0 Then 
                    Left(_NodeCount) = _NodeCount 
                    Right(_NodeCount) = _NodeCount 
                    K = 1 
                Else 
                    Left(_NodeCount) = _NodeCount - 1 
                    Right(_NodeCount) = Right(_NodeCount - 1) 
                    Left(Right(_NodeCount - 1)) = _NodeCount 
                    Right(_NodeCount - 1) = _NodeCount 
                End If 

                Down(_NodeCount) = I + 1 
                Up(_NodeCount) = Up(I + 1) 
                Down(Up(I + 1)) = _NodeCount 
                Up(I + 1) = _NodeCount  

                Row(_NodeCount) = _Rows 
                Col(_NodeCount) = I + 1 
            End If 
        Next 

    End Sub

 

把矩陣的一行元素(包括0和1)添加到類中

在前文中介紹了Dancing Links中只存儲1的元素(稀疏矩陣),因此,在添加的時候,先判斷值是否是1。

那實際上問題是如何把元素添加到雙向鏈中,在添加的過程中,自左向右添加。

 

先考量如何把元素添加到水平雙向鏈中

當添加這一行的第一個元素時,由於還沒有雙向鏈,首先構造一個只有一個元素的雙向鏈。Left(_NodeCount) = _NodeCount和Right(_NodeCount) = _NodeCount。這個元素的Left和Right分量都指向自己。

從第二個元素開始。問題就轉換爲把元素添加到水平雙向鏈的末尾,實際上需要知道之前的水平雙向鏈的最左邊的元素和最右邊的元素,可以肯定的是最右邊的元素是_NodeCount-1,最左邊的元素是什麼?之前並沒有緩存啊。由於是循環雙向鏈,Right(_NodeCount-1)就是這雙向鏈的最左邊的元素。Left(_NodeCount) = _NodeCount - 1,把當前元素的Left分量指向最右邊的元素即_NodeCount-1;Right(_NodeCount) = Right(_NodeCount - 1) ,把當前元素的Right分量指向最左邊的元素即Right(_NodeCount-1);Left(Right(_NodeCount - 1)) = _NodeCount,把最左邊的元素即Right(_NodeCount-1)的Left分量指向當前元素;Right(_NodeCount - 1) = _NodeCount,把最右邊的元素即_NodeCount-1的Right分量指向當前元素

 

再考量如何把元素添加到垂直雙向鏈

同樣,問題就轉換爲把元素添加到垂直雙向鏈的末尾,實際上需要知道之前的垂直雙向鏈的最上邊的元素和最下邊的元素。和水平雙向鏈的不同,我們沒法知道最下邊的元素,但是我們可以利用列首元素知道最上邊的元素(列首元素就是該雙向鏈中最上邊的元素)。因此,最上邊的元素是I+1(因爲I是從0開始的,故相應的列就是I+1,相應的列首元素就是I+1),那麼最下邊的元素就是Up(I+1)。Down(_NodeCount) = I + 1,把當前元素的Down分量指向最上邊的元素即I+1;Up(_NodeCount) = Up(I + 1) ,把當前元素的Up分量指向最下邊的元素即Up(I+1);Down(Up(I + 1)) = _NodeCount,把最下邊元素即Up(I+1)的Down分量指向當前元素;Up(I + 1) = _NodeCount,把最上邊元素即I+1的Up分量指向當前元素

 

至此,完成了把當前元素添加到兩個雙向鏈的過程

最後,給當前元素的Row分量和Col分量賦值

 

在文首的題目中,添加第一行的數據,如下調用

AppendLine(0,0,1,0,1,1,0)

 

 

如果一行中有大量的0,那麼用下面的函數比較方便


    

Public Sub AppendLineByIndex(ByVal ParamArray Index() As Integer) 
        _Rows += 1 
        If Index.Length = 0 Then Exit Sub 

        Dim I As Integer, K As Integer = 0  

        ReDim Preserve Left(_NodeCount + Index.Length) 
        ReDim Preserve Right(_NodeCount + Index.Length) 
        ReDim Preserve Up(_NodeCount + Index.Length) 
        ReDim Preserve Down(_NodeCount + Index.Length) 
        ReDim Preserve Row(_NodeCount + Index.Length) 
        ReDim Preserve Col(_NodeCount + Index.Length) 
        ReDim Preserve Ans(_NodeCount + Index.Length) 

        For I = 0 To Index.Length - 1 

            _NodeCount += 1 

            If I = 0 Then 
                Left(_NodeCount) = _NodeCount 
                Right(_NodeCount) = _NodeCount 
            Else 
                Left(_NodeCount) = _NodeCount - 1 
                Right(_NodeCount) = Right(_NodeCount - 1) 
                Left(Right(_NodeCount - 1)) = _NodeCount 
                Right(_NodeCount - 1) = _NodeCount 
            End If 

            Down(_NodeCount) = Index(I) 
            Up(_NodeCount) = Up(Index(I)) 
            Down(Up(Index(I))) = _NodeCount 
            Up(Index(I)) = _NodeCount 

            Row(_NodeCount) = _Rows 
            Col(_NodeCount) = Index(I) 
        Next  

    End Sub

該函數的參數是這一行中值爲1的元素的所在列的下標,具體就不再解釋了。和AppendLine函數類似。

在文首的題目中,添加第一行的數據,如下調用

AppendLineByIndex(3,5,6)

和AppendLine(0,0,1,0,1,1,0)效果相同。

 

下面的代碼是調用該類求解文首題目的代碼

Dim tS As New clsDancingLinks(7)


tS.AppendLineByIndex(3, 5, 6) 
tS.AppendLineByIndex(1, 4, 7) 
tS.AppendLineByIndex(2, 3, 6) 
tS.AppendLineByIndex(1, 4) 
tS.AppendLineByIndex(2, 7) 
tS.AppendLineByIndex(4, 5, 7)



Dim Ans() As Integer = tS.Dance 

 

Ans()數組中的值是4,5,1

 

 

至此,求解精確覆蓋問題的Dancing Links算法就介紹完了。利用十字循環雙向鏈這個特殊的數據結構,不可思議的完成了緩存矩陣和回溯矩陣的過程,十分優雅,十分高效。故Donald E.Knuth把它稱爲Dancing Links(舞蹈鏈)。我更喜歡跳躍的舞者這個名字

 

有很多問題都能轉換爲精確覆蓋問題,再利用Dancing Links算法求解就方便多了。

 

 

最後,把該類的完整代碼貼在下方



 

Public Class clsDancingLinks 
    Private Left() As Integer, Right() As Integer, Up() As Integer, Down() As Integer 
    Private Row() As Integer, Col() As Integer 

    Private _Head As Integer 

    Private _Rows As Integer, _Cols As Integer, _NodeCount As Integer 
    Private Ans() As Integer 

    Public Sub New(ByVal Cols As Integer) 
        ReDim Left(Cols), Right(Cols), Up(Cols), Down(Cols), Row(Cols), Col(Cols), Ans(Cols) 
        Dim I As Integer 

        Up(0) = 0 
        Down(0) = 0 
        Right(0) = 1 
        Left(0) = Cols 

        For I = 1 To Cols 
            Up(I) = I 
            Down(I) = I 
            Left(I) = I - 1 
            Right(I) = I + 1 
            Col(I) = I 
            Row(I) = 0 
        Next 

        Right(Cols) = 0 

        _Rows = 0 
        _Cols = Cols 
        _NodeCount = Cols 
        _Head = 0 
    End Sub 

    Public Sub AppendLine(ByVal ParamArray Value() As Integer) 
        _Rows += 1 
        If Value.Length = 0 Then Exit Sub 

        Dim I As Integer, K As Integer = 0 

        For I = 0 To Value.Length - 1 
            If Value(I) = 1 Then 
                _NodeCount += 1 
                ReDim Preserve Left(_NodeCount) 
                ReDim Preserve Right(_NodeCount) 
                ReDim Preserve Up(_NodeCount) 
                ReDim Preserve Down(_NodeCount) 
                ReDim Preserve Row(_NodeCount) 
                ReDim Preserve Col(_NodeCount) 
                ReDim Preserve Ans(_NodeCount) 
                If K = 0 Then 
                    Left(_NodeCount) = _NodeCount 
                    Right(_NodeCount) = _NodeCount 
                    K = 1 
                Else 
                    Left(_NodeCount) = _NodeCount - 1 
                    Right(_NodeCount) = Right(_NodeCount - 1) 
                    Left(Right(_NodeCount - 1)) = _NodeCount 
                    Right(_NodeCount - 1) = _NodeCount 
                End If 

                Down(_NodeCount) = I + 1 
                Up(_NodeCount) = Up(I + 1) 
                Down(Up(I + 1)) = _NodeCount 
                Up(I + 1) = _NodeCount  

                Row(_NodeCount) = _Rows 
                Col(_NodeCount) = I + 1 
            End If 
        Next 

    End Sub 

    Public Sub AppendLineByIndex(ByVal ParamArray Index() As Integer) 
        _Rows += 1 
        If Index.Length = 0 Then Exit Sub 

        Dim I As Integer, K As Integer = 0  

        ReDim Preserve Left(_NodeCount + Index.Length) 
        ReDim Preserve Right(_NodeCount + Index.Length) 
        ReDim Preserve Up(_NodeCount + Index.Length) 
        ReDim Preserve Down(_NodeCount + Index.Length) 
        ReDim Preserve Row(_NodeCount + Index.Length) 
        ReDim Preserve Col(_NodeCount + Index.Length) 
        ReDim Preserve Ans(_NodeCount + Index.Length) 

        For I = 0 To Index.Length - 1 

            _NodeCount += 1 

            If I = 0 Then 
                Left(_NodeCount) = _NodeCount 
                Right(_NodeCount) = _NodeCount 
            Else 
                Left(_NodeCount) = _NodeCount - 1 
                Right(_NodeCount) = Right(_NodeCount - 1) 
                Left(Right(_NodeCount - 1)) = _NodeCount 
                Right(_NodeCount - 1) = _NodeCount 
            End If 

            Down(_NodeCount) = Index(I) 
            Up(_NodeCount) = Up(Index(I)) 
            Down(Up(Index(I))) = _NodeCount 
            Up(Index(I)) = _NodeCount 

            Row(_NodeCount) = _Rows 
            Col(_NodeCount) = Index(I) 
        Next 


    End Sub 

    Public Function Dance() As Integer() 
        Return IIf(Dance(0) = True, Ans, Nothing) 
    End Function 


    Private Function Dance(ByVal K As Integer) As Boolean 

        Dim C1 As Integer = Right(_Head) 
        If (C1 = _Head) Then 
            ReDim Preserve Ans(K - 1) 
            Return True 
        End If 
        RemoveCol(C1) 

        Dim I As Integer, J As Integer 

        I = Down(C1) 
        Do While I <> C1 
            Ans(K) = Row(I) 

            J = Right(I) 
            Do While J <> I 
                RemoveCol(Col(J)) 
                J = Right(J) 
            Loop 

            If Dance(K + 1) Then Return True 

            J = Left(I) 
            Do While J <> I 
                ResumeCol(Col(J)) 
                J = Left(J) 
            Loop 

            I = Down(I) 
        Loop  

        ResumeCol(C1) 
        Return False 
    End Function 

    Public Sub RemoveCol(ByVal Col As Integer) 

        Left(Right(Col)) = Left(Col) 
        Right(Left(Col)) = Right(Col) 

        Dim I As Integer, J As Integer 

        I = Down(Col) 
        Do While I <> Col 
            J = Right(I) 
            Do While J <> I 
                Up(Down(J)) = Up(J) 
                Down(Up(J)) = Down(J) 
                J = Right(J) 
            Loop 

            I = Down(I) 
        Loop 

    End Sub 

    Public Sub ResumeCol(ByVal Col As Integer) 

        Left(Right(Col)) = Col 
        Right(Left(Col)) = Col 

        Dim I As Integer, J As Integer 

        I = Up(Col) 

        Do While (I <> Col) 
            J = Right(I) 
            Do While J <> I 
                Up(Down(J)) = J 
                Down(Up(J)) = J 
                J = Right(J) 
            Loop 
            I = Up(I) 
        Loop 

    End Sub 
End Class 

 

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