精確覆蓋問題的定義:給定一個由0-1組成的矩陣,是否能找到一個行的集合,使得集合中每一列都剛好包含一個1算法
例如:以下的矩陣數組
就包含了這樣一個集合(第一、四、5行)緩存
如何利用給定的矩陣求出相應的行的集合呢?咱們採用回溯法數據結構
先假定選擇第1行,以下所示:oop
如上圖中所示,紅色的那行是選中的一行,這一行中有3個1,分別是第三、五、6列。編碼
因爲這3列已經包含了1,故,把這三列往下標示,圖中的藍色部分。藍色部分包含3個1,分別在2行中,把這2行用紫色標示出來spa
根據定義,同一列的1只能有1個,故紫色的兩行,和紅色的一行的1相沖突。設計
那麼在接下來的求解中,紅色的部分、藍色的部分、紫色的部分都不能用了,把這些部分都刪除,獲得一個新的矩陣3d
行分別對應矩陣1中的第二、四、5行
列分別對應矩陣1中的第一、二、四、7列
因而問題就轉換爲一個規模小點的精確覆蓋問題
在新的矩陣中再選擇第1行,以下圖所示
仍是按照以前的步驟,進行標示。紅色、藍色和紫色的部分又全都刪除,致使新的空矩陣產生,而紅色的一行中有0(有0就說明這一列沒有1覆蓋)。說明,第1行選擇是錯誤的
那麼回到以前,選擇第2行,以下圖所示
按照以前的步驟,進行標示。把紅色、藍色、紫色部分刪除後,獲得新的矩陣
行對應矩陣2中的第3行,矩陣1中的第5行
列對應矩陣2中的第二、4列,矩陣1中的第二、7列
因爲剩下的矩陣只有1行,且都是1,選擇這一行,問題就解決
因而該問題的解就是矩陣1中第1行、矩陣2中的第2行、矩陣3中的第1行。也就是矩陣1中的第一、四、5行
在求解這個問題的過程當中,咱們第1步選擇第1行是正確的,可是不是每一個題目第1步選擇都是正確的,若是選擇第1行沒法求解出結果出來,那麼就要推倒以前的選擇,從選擇第2行開始,以此類推
從上面的求解過程來看,實際上求解過程能夠以下表示
一、從矩陣中選擇一行
二、根據定義,標示矩陣中其餘行的元素
三、刪除相關行和列的元素,獲得新矩陣
四、若是新矩陣是空矩陣,而且以前的一行都是1,那麼求解結束,跳轉到6;新矩陣不是空矩陣,繼續求解,跳轉到1;新矩陣是空矩陣,以前的一行中有0,跳轉到5
五、說明以前的選擇有誤,回溯到以前的一個矩陣,跳轉到1;若是沒有矩陣能夠回溯,說明該問題無解,跳轉到7
六、求解結束,把結果輸出
七、求解結束,輸出無解消息
從如上的求解流程來看,在求解的過程當中有大量的緩存矩陣和回溯矩陣的過程。而如何緩存矩陣以及相關的數據(保證後面的回溯能正確恢復數據),也是一個比較頭疼的問題(並非沒法解決)。以及在輸出結果的時候,如何輸出正確的結果(把每一步的選擇轉換爲初始矩陣相應的行)。
因而算法大師Donald E.Knuth(《計算機程序設計藝術》的做者)出面解決了這個方面的難題。他提出了DLX(Dancing Links X)算法。實際上,他把上面求解的過程稱爲X算法,而他提出的舞蹈鏈(Dancing Links)實際上並非一種算法,而是一種數據結構。一種很是巧妙的數據結構,他的數據結構在緩存和回溯的過程當中效率驚人,不須要額外的空間,以及近乎線性的時間。而在整個求解過程當中,指針在數據之間跳躍着,就像精巧設計的舞蹈同樣,故Donald E.Knuth把它稱爲Dancing Links(中文譯名舞蹈鏈)。
Dancing Links的核心是基於雙向鏈的方便操做(移除、恢復加入)
咱們用例子來講明
假設雙向鏈的三個連續的元素,A一、A二、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元素:輔助元素,稱列標元素,每列有一個列標元素。本文開始的題目的列標元素分別是C一、C二、C三、C四、C五、C六、C7。每一列的元素的Col份量都指向所在列的列標元素。列標元素的Col份量指向本身(也能夠是沒有)。在初始化的狀態下,Head.Right=C一、C1.Right=C二、……、C7.Right=Head、Head.Left=C7等等。列標元素的份量Row=0,表示是處在第0行。
下圖就是根據題目構建好的交叉十字循環雙向鏈(構建的過程後面的詳述)
就上圖解釋一下
每一個綠色方塊是一個元素,其中Head和C一、C二、……、C7是輔助元素。橙色框中的元素是原矩陣中1的元素,給他們標上號(從1到16)
左側的紅色,標示的是行號,輔助元素所在的行是0行,其他元素所在的行從1到6
每兩個元素之間有一個雙向箭頭連線,表示雙向鏈中相鄰兩個元素的關係(水平的是左右關係、垂直的是上下關係)
單向的箭頭並非表示單向關係,而由於是循環雙向鏈,左側的單向箭頭和右側的單向箭頭(上邊的和下邊的)組成了一個雙向箭頭,例如元素14左側的單向箭頭和元素16右側的單項箭頭組成一個雙向箭頭,表示14.Left=1六、16.Right=14;同理,元素14下邊的單項箭頭和元素C4上邊的單向箭頭組成一個雙向箭頭,表示14.Down=C四、C4.Up=14
接下來,利用圖來解釋Dancing Links是如何求解精確覆蓋問題
一、首先判斷Head.Right=Head?如果,求解結束,輸出解;若不是,求解還沒結束,到步驟2(也能夠判斷Head.Left=Head?)
二、獲取Head.Right元素,即元素C1,並標示元素C1(標示元素C1,指的是標示C一、和C1所在列的全部元素、以及該元素所在行的元素,並從雙向鏈中移除這些元素)。以下圖中的紫色部分。
如上圖可知,行2和行4中的一個必是答案的一部分(其餘行中沒有元素能覆蓋列C1),先假設選擇的是行2
三、選擇行2(在答案棧中壓入2),標示該行中的其餘元素(元素5和元素6)所在的列首元素,即標示元素C4和標示元素C7,下圖中的橙色部分。
注意的是,即便元素5在步驟2中就從雙向鏈中移除,可是元素5的Col份量仍是指向元素C4的,這裏體現了雙向鏈的強大做用。
把上圖中的紫色部分和橙色部分移除的話,剩下的綠色部分就以下圖所示
一會兒空了好多,是否是轉換爲一個少了不少元素的精確覆蓋問題?,利用遞歸的思想,很快就能寫出求解的過程來。咱們繼續完成求解過程
四、獲取Head.Right元素,即元素C2,並標示元素C2。以下圖中的紫色部分。
如圖,列C2只有元素7覆蓋,故答案只能選擇行3
五、選擇行3(在答案棧中壓入3),標示該行中的其餘元素(元素8和元素9)所在的列首元素,即標示元素C3和標示元素C6,下圖中的橙色部分。
把上圖中的紫色部分和橙色部分移除的話,剩下的綠色部分就以下圖所示
六、獲取Head.Right元素,即元素C5,元素C5中的垂直雙向鏈中沒有其餘元素,也就是沒有元素覆蓋列C5。說明當前求解失敗。要回溯到以前的分叉選擇步驟(步驟2)。那要回標列首元素(把列首元素、所在列的元素,以及對應行其他的元素。並恢復這些元素到雙向鏈中),回標列首元素的順序是標示元素的順序的反過來。從前文可知,順序是回標列首C6、回標列首C3、回標列首C2、回標列首C7、回標列首C4。表面上看起來比較複雜,實際上利用遞歸,是一件很簡單的事。並把答案棧恢復到步驟2(清空的狀態)的時候。又回到下圖所示
七、因爲以前選擇行2致使無解,所以此次選擇行4(再無解就整個問題就無解了)。選擇行4(在答案棧中壓入4),標示該行中的其餘元素(元素11)所在的列首元素,即標示元素C4,下圖中的橙色部分。
把上圖中的紫色部分和橙色部分移除的話,剩下的綠色部分就以下圖所示
八、獲取Head.Right元素,即元素C2,並標示元素C2。以下圖中的紫色部分。
如圖,行3和行5均可以選擇
九、選擇行3(在答案棧中壓入3),標示該行中的其餘元素(元素8和元素9)所在的列首元素,即標示元素C3和標示元素C6,下圖中的橙色部分。
把上圖中的紫色部分和橙色部分移除的話,剩下的綠色部分就以下圖所示
十、獲取Head.Right元素,即元素C5,元素C5中的垂直雙向鏈中沒有其餘元素,也就是沒有元素覆蓋列C5。說明當前求解失敗。要回溯到以前的分叉選擇步驟(步驟8)。從前文可知,回標列首C6、回標列首C3。並把答案棧恢復到步驟8(答案棧中只有4)的時候。又回到下圖所示
十一、因爲以前選擇行3致使無解,所以此次選擇行5(在答案棧中壓入5),標示該行中的其餘元素(元素13)所在的列首元素,即標示元素C7,下圖中的橙色部分。
把上圖中的紫色部分和橙色部分移除的話,剩下的綠色部分就以下圖所示
十二、獲取Head.Right元素,即元素C3,並標示元素C3。以下圖中的紫色部分。
1三、如上圖,列C3只有元素1覆蓋,故答案只能選擇行3(在答案棧壓入1)。標示該行中的其餘元素(元素2和元素3)所在的列首元素,即標示元素C5和標示元素C6,下圖中的橙色部分。
把上圖中的紫色部分和橙色部分移除的話,剩下的綠色部分就以下圖所示
1四、由於Head.Right=Head。故,整個過程求解結束。輸出答案,答案棧中的答案分別是四、五、1。表示該問題的解是第四、五、1行覆蓋全部的列。以下圖所示(藍色的部分)
從以上的14步來看,能夠把Dancing Links的求解過程表述以下
一、Dancing函數的入口
二、判斷Head.Right=Head?,如果,輸出答案,返回True,退出函數。
三、得到Head.Right的元素C
四、標示元素C
五、得到元素C所在列的一個元素
六、標示該元素同行的其他元素所在的列首元素
七、得到一個簡化的問題,遞歸調用Daning函數,若返回的True,則返回True,退出函數。
八、若返回的是False,則回標該元素同行的其他元素所在的列首元素,回標的順序和以前標示的順序相反
九、得到元素C所在列的下一個元素,如有,跳轉到步驟6
十、若沒有,回標元素C,返回False,退出函數。
以前的文章的表述,爲了表述簡單,採用面向對象的思路,說每一個元素有6個份量,分別是Left、Right、Up、Down、Col、Row份量。
但在實際的編碼中,用數組也能實現相同的做用。例如:用Left()表示全部元素的Left份量,Left(1)表示元素1的Left份量
在前文中,元素分爲Head元素、列首元素(C一、C2等)、普通元素。在編碼中,三種元素統一成一種元素。如上題,0表示Head元素,1表示元素C一、2表示元素C二、……、7表示元素C7,從8開始表示普通元素。這是統一後,編碼的簡便性。利用數組的下標來表示元素,宛若指針通常。
下面是代碼的講解
一、該類的一些變量
前兩行表示每一個元素的六個份量,用數組表示;_Head表示元素Head,在類中初始化時令其等於0;_Rows表示矩陣的行數,_Cols表示矩陣的列數,_NodeCount表示元素的個數;Ans()用於存放答案
二、求解的主函數,Dance函數,是個遞歸函數,參數K表示當前的調用層數。
其中第一個函數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
三、求解的輔助函數,RemoveCol函數,標示列首函數
首先,利用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份量)開始 。能夠參考以前的圖中的紫色部分。
四、求解的輔助函數,ResumeCol函數,回標列首函數
首先,利用Left(Right(Col)) = Col 和Right(Left(Col)) = Col 把列首元素Col恢復到水平雙向鏈中。再依次把Col所在的列的其他元素的所在行的其他元素恢復到垂直雙向鏈中,利用的是Up(Down(J)) = J 和Down(Up(J)) = J。找尋Col所在列的其他元素的順序是從上邊(Up份量)開始(和以前的RemoveCol函數相反),恢復所在行其他元素的順序是從右邊(Right份量)開始 。
五、類的初始化函數
初始化函數有一個參數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元素,是爲了後面的代碼的直觀性
六、添加矩陣元素的函數
把矩陣的一行元素(包括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,那麼用下面的函數比較方便
該函數的參數是這一行中值爲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算法求解就方便多了。
最後,把該類的完整代碼貼在下方