網(wǎng)站閉站保護網(wǎng)站運營一個月多少錢
一,基本概念
算法簡介?
????????舞蹈鏈算法(Dancing Links,簡稱 DLX)是一種高效解決精確覆蓋問題的算法,實際上是一種數(shù)據(jù)結(jié)構(gòu),可以用來實現(xiàn) X算法,以解決精確覆蓋問題。由高德納(Donald E. Knuth)提出 。
精準覆蓋?
????????什么是精確覆蓋(Exact Cover)問題呢?就是在一個全集X中若干子集的集合為S。S* 是 S的一個子集,當且僅當X中的每一個元素在S*中恰好出現(xiàn)一次時,S*稱之為一個精確覆蓋。在計算機科學中,精確覆蓋問題指找出這樣的一種覆蓋,或證明其不存在。這是一個NP-完全問題。
舞蹈鏈
? ? ? ? 其實是一種特殊的數(shù)據(jù)結(jié)構(gòu),用于高效地實現(xiàn)對精確覆蓋問題的求解。它基于雙向循環(huán)鏈表,每個節(jié)點除了包含指向左右節(jié)點的指針外,還包含指向上方和下方節(jié)點的指針,這種結(jié)構(gòu)使得在搜索過程中能夠快速地對鏈表進行插入、刪除和恢復操作。
數(shù)據(jù)結(jié)構(gòu)設計
每個1的節(jié)點包含四個指針:left
,?right
,?up
,?down
,形成雙向十字鏈表。
每列有一個列頭節(jié)點,記錄該列中1的數(shù)量(用于優(yōu)化搜索順序)。
算法流程
-
選擇列:優(yōu)先選擇當前剩余1最少的列(減少搜索分支)。
-
覆蓋列:刪除該列及其關(guān)聯(lián)的所有行(避免后續(xù)搜索沖突)。
-
遞歸搜索:對剩余矩陣重復上述步驟。
-
回溯恢復:若當前路徑無解,恢復被刪除的列和行,嘗試其他分支。
- 結(jié)束條件:當舞蹈鏈中的所有列都被覆蓋(即矩陣中所有列都被刪除)時,找到了一個精確覆蓋解;如果遍歷完所有可能的分支都沒有找到解,則說明該問題無解。
?二,示例
例如,S = {A,B,C,D,E,F} 是全集 X = {1,2,3,4,5,6,7} 的一個子集的集合,其中:
A = {1, 4, 7}
B = {1, 4}
C = {4, 5, 7}
D = {3, 5, 6}
E = {2, 3, 6, 7}
F = {2, 7}
那么,S的一個子集 S* = {B, D, F} 是X的一個精確覆蓋,因為 X 中的每個元素恰好在S*中出現(xiàn)了一次。
可以用0-1矩陣來表示精確覆蓋問題。我們用矩陣的每行表示S的一個元素,也就是X的一個子集;用矩陣的每列表示X的一個元素。矩陣中的1代表這一列的元素存在于這一行對應的子集中,0代表不存在。那么精確覆蓋問題可以轉(zhuǎn)化成求出矩陣若干行的集合,使得集合中的每一列恰好都有一個1。
比如前面的問題可以用矩陣的形式表示成
那么選擇紅色的B,D,F能滿足每列都恰好包含一個1。
可以用 Knuth 提出的X算法來解決精確覆蓋問題。X算法是一個非確定性的深度優(yōu)先回溯算法。它的具體步驟如下:
1. 如果矩陣
為空(沒有任何列),則當前局部解即為問題的一個解,返回成功;否則繼續(xù)。
2. 根據(jù)一定方法選擇第 c 列。如果某一列中沒有 1,則返回失敗,并去除當前局部解中最新加入的行。
選擇第 r 行,使得
(該步是非確定性的)。
將第 r 行加入當前局部解中。
對于滿足
的每一列j,從矩陣
中刪除所有滿足
的行,最后再刪除第 j 列。
對所得比 A 小的新矩陣遞歸地執(zhí)行此算法。
讓我們用 X算法解決上面的精確覆蓋問題。
首先,當前矩陣不為空,算法繼續(xù)進行。那么先選擇1最少的一列。因為 1,2,3,5,6 列都只有 2 個 1,因此我們隨便選擇 1 個,比如第 1 列。
行 A 和 B 都含有 1,因此要在這兩行中進行選擇。
先嘗試選擇行 A。將行A加入到當前的解中。
行A的 1,4,7 列為 1,根據(jù)第 5 步,需要把所有在 1,4,7 列中含有 1 的行都刪除掉,因此需要刪除掉行A,B,C,E,F,同時刪除掉第 1,4,7 列
刪除之后,矩陣只剩下行 D 和第 2,3,5,6 列:
進入遞歸,回到第 1 步,矩陣非空,算法繼續(xù)執(zhí)行。
再進入第2步,此時選擇 1 最少的第 2 列,里面沒有 1,因此返回失敗,同時將行 A 從當前的解中移除;
算法進入另一個分支,選擇行 B,并將其加入到當前的解中:
行 B 的第 1,4 列為 1,因此要把 1,4 列中包含 1 的行都刪掉。需要刪除掉行 A,B,C,再刪除掉 1,4 列。
此時矩陣變?yōu)?/p>
進入遞歸,回到第 1 步,矩陣非空,因此算法繼續(xù)。
當前包含 1 最少的一列是第 5 列,那么將從第 5 列中選擇含有 1 的行進行搜索。
第 5 列中行 D 含有 1,因此選擇行 D,將其加入當前解中,算法進入新的一層搜索。
行 D 的第 3,5,6 列包含 1,我們要刪掉這幾列中包含 1 的所有行,同時刪掉這幾列
那么我們需要刪掉行 D,E 和第 3,5,6 列,矩陣變?yōu)?/p>
再次遞歸執(zhí)行,回到第 1 步,矩陣非空,因此算法繼續(xù)
選擇當前包含 1 最少的一列,這里選擇第 2 列。第 2 列中只有行 F 包含 1, 因此選擇行 F
將行 F 加入到當前解中,算法進入第 3 層搜索
行 F 中第 2,7列為 1,第 2,7 列中行 F 包含 1,因此移除行 F 和第 2,7 列
算法再次進入遞歸執(zhí)行,回到第 1 步,此時所有的列都被移除了,矩陣為空,因此返回成功,找到了一個解:{B, D, F}
繼續(xù)搜索,沒有其他可以選擇的行,返回上一層;
第2層也沒有其他可以選擇的行,再返回上一層;
第1層也沒有其他可以選擇的行,再返回上一層;
第0層也沒有其他可以選擇的行,算法終止。
以上就是 X 算法的執(zhí)行過程。Knuth 提出 X 算法主要是為了說明舞蹈鏈的作用,他發(fā)現(xiàn)用舞蹈鏈來執(zhí)行 X 算法效率特別高。那么什么是舞蹈鏈呢?它是基于雙向鏈表的一種數(shù)據(jù)結(jié)構(gòu)。
讓我們先來看看雙向鏈表:
上圖是一個簡單的雙向鏈表,每個節(jié)點有兩個指針,分別指向自己的前驅(qū)和后繼節(jié)點。那么如果我們想把其中一個節(jié)點,比如 B 從鏈表中刪掉,只需要執(zhí)行下面的操作:
B.left.right?=?B.rightB.right.left?=?B.left
注意:此時雖然 B 從鏈表中移除了,但它的兩個指針依然保持不變,還是指向之前的前驅(qū)和后繼節(jié)點。
因此,如果我想把 B 再添加到鏈表原來的位置上,此時并不需要修改 B 的指針,只需要再把 B 的前驅(qū)和后繼節(jié)點的指針恢復就可以了:
B.left.right?=?BB.right.left?=?B
理解了這一點之后,讓我們再來看看舞蹈鏈的結(jié)構(gòu)是怎么樣的:
上面這個圖是一個舞蹈鏈的結(jié)構(gòu),描述的是前面 X 算法中用到的矩陣。它由幾部分構(gòu)成:
最上面的藍色部分是一個水平的環(huán)狀雙向鏈表。最左邊是頭節(jié)點,它是整個數(shù)據(jù)結(jié)構(gòu)的根節(jié)點。其余是列頭節(jié)點,每個代表矩陣中的一列。
每一列又是一個縱向的環(huán)狀雙向鏈表。除了最上面的列頭節(jié)點,其他的每個節(jié)點都代表前面的矩陣中的一個 1。這實際上是一個稀疏矩陣,為了優(yōu)化存儲和效率,只保留了值為 1 的節(jié)點,把每個節(jié)點按順序保存到數(shù)組中。最早的 Dancing Links 算法,也就是 Knuth 在 2000 年發(fā)表的論文中,下面的每一行也都是一個雙向鏈表。但后來他發(fā)現(xiàn)每一行在算法執(zhí)行過程中實際上不會發(fā)生變化,因此他把水平的雙向鏈表取消了,只保留了最頂上的列頭節(jié)點之間的水平雙向鏈表。下面的每一行之間的前后節(jié)點可以直接通過數(shù)組的索引得到。兩邊是Space節(jié)點,用來標記一行的開始和結(jié)束。
每個普通節(jié)點 A 都包含 4 個 字段,A.up 和 A.down 代表雙向鏈表的兩個指針,分別指向 A 上面和下面的節(jié)點。還有一個 A.col ,指向 A 所在列的頭節(jié)點,需要根據(jù)這個字段定位到節(jié)點所在的列。另外還有一個 A.row,主要是方便在遞歸的過程中緩存當前的解。
列頭節(jié)點還要再多幾個字段,left 和 right 分別指向水平雙向鏈表的左節(jié)點和右節(jié)點。另外還有一個 count 字段,代表這一列當前一共有幾個元素。X 算法的第 2 步,選擇 1 最少的列時會用到這個字段。
理解了舞蹈鏈的數(shù)據(jù)結(jié)構(gòu)之后,我們再來看看是怎樣用舞蹈鏈來實現(xiàn) X 算法的。這部分算法很精妙,也是舞蹈鏈這個名字的來由,通過對鏈表上的節(jié)點反復刪除和插入實現(xiàn)了遞歸的回溯,就好像一個個鏈表在舞臺上翩翩起舞一樣。
具體的算法實現(xiàn)可以參照 Knuth 的論文,我們還是用圖的方式來說明一下。
(1)首先,判斷鏈表是否為空,可以通過 head.right == head 來判斷。如果為空則返回,并輸出當前的解。
(2)不為空則選擇當前節(jié)點數(shù)最少的列。如果只有列頭節(jié)點,則返回失敗。
遍歷這一列的每個節(jié)點,開始進行覆蓋操作:
(1)首先將節(jié)點所在行作為解的一部分,加入到當前解中;
(2)遍歷這一行的所有節(jié)點,將每個節(jié)點所在列都刪除掉,同時刪除掉與這些列有交集的所有行:
2a. 遍歷節(jié)點所在列的每個節(jié)點,將每個節(jié)點所在行的所有節(jié)點從它所在的列中移除掉,同時將列頭節(jié)點的計數(shù)減 1:
node.up.down?=?node.downnode.down.up?=?node.upcol_node.count?-=?1
2b. 還要將這一列從鏈表中移除:
col_node.left.right?=?col_node.rightcol_node.right.left?=?col_node.left
進入遞歸調(diào)用,判斷鏈表是否為空;
不為空則選擇節(jié)點數(shù)最少的列,再遍歷這一列的節(jié)點,進行覆蓋操作:
移除掉所有節(jié)點之后,進入遞歸調(diào)用,發(fā)現(xiàn)鏈表不為空,但節(jié)點數(shù)最少的列中沒有普通節(jié)點了,返回失敗;
開始做鏈表的還原操作。注意還原的順序需要和移除的順序相反。如果我們是從上至下,從左至右移除節(jié)點,那么還原的時候就從右至左,從下至上。否則的話可能會出現(xiàn)問題,導致一個節(jié)點被還原多次,這樣列中節(jié)點的計數(shù)就不準確了。
node.up.down?=?nodenode.down.up?=?nodecol_node.count?+=?1
并且把刪除的列也取消覆蓋
col_node.left.right?=?col_nodecol_node.right.left?=?col_node
遞歸返回到上一層,還原之后,發(fā)現(xiàn)列中沒有其他節(jié)點可以選擇,再返回到上一層,選擇下一個節(jié)點所在的行。
和之前的方法相同,遍歷這一行的所有節(jié)點,將每個節(jié)點所在列都刪除掉,同時刪除掉與這些列有交集的所有行:
再選擇節(jié)點最少的列,遍歷這一列的所有節(jié)點的所在行:
遍歷這一行的所有節(jié)點,刪除掉每個節(jié)點所在列,以及與這些列有交集的所有行:
再次進入遞歸調(diào)用,判斷矩陣不為空,選擇節(jié)點最少的一列,遍歷每個節(jié)點,刪除掉所在行的所有列,與這些列有交集的所有行,最后我們得到一個空矩陣。
此時將得到的解輸出,并返回,接下來還要進行還原操作,然后搜索下一個解。
三、代碼
class Node:def __init__(self):self.left = self.right = self.up = self.down = selfself.column = None # 列頭節(jié)點self.row = None # 行標識def solve(matrix):# 構(gòu)建舞蹈鏈head = build_dancing_links(matrix)solution = []search(head, solution)def search(head, solution):if head.right == head:# 找到解,輸出結(jié)果return True# 選擇1最少的列col = choose_column(head)cover(col)# 遍歷該列的每一行row_node = col.downwhile row_node != col:solution.append(row_node.row)# 覆蓋該行關(guān)聯(lián)的所有列right_node = row_node.rightwhile right_node != row_node:cover(right_node.column)right_node = right_node.right# 遞歸搜索if search(head, solution):return True# 回溯solution.pop()left_node = row_node.leftwhile left_node != row_node:uncover(left_node.column)left_node = left_node.leftrow_node = row_node.downuncover(col)return False
class Node:def __init__(self):self.left = self.right = self.up = self.down = selfself.col = self.row = Noneclass DLX:def __init__(self):self.root = Node()self.columns = {}self.answer = []def add_column(self, name):node = Node()node.col = nodenode.row = Nonenode.left = self.root.leftnode.right = self.rootself.root.left.right = nodeself.root.left = nodeself.columns[name] = nodedef add_row(self, row_data):first = Nonelast = Nonefor col_name, value in row_data.items():if value == 1:node = Node()node.col = self.columns[col_name]node.row = row_datanode.up = node.col.upnode.down = node.colnode.col.up.down = nodenode.col.up = nodeif first is None:first = nodeelse:last.right = nodenode.left = lastlast = nodefirst.left = lastlast.right = firstdef cover_column(self, col):col.right.left = col.leftcol.left.right = col.righti = col.downwhile i!= col:j = i.rightwhile j!= i:j.down.up = j.upj.up.down = j.downj = j.righti = i.downdef uncover_column(self, col):i = col.upwhile i!= col:j = i.leftwhile j!= i:j.down.up = jj.up.down = jj = j.lefti = i.upcol.right.left = colcol.left.right = coldef search(self, k):if self.root.right == self.root:print("Solution found:", self.answer)return Truec = self.root.righti = c.downmin_size = float('inf')while i!= c:size = 0j = i.rightwhile j!= i:size += 1j = j.rightif size < min_size:min_size = sizec = ii = i.downself.cover_column(c.col)i = c.downwhile i!= c:self.answer.append(i.row)j = i.rightwhile j!= i:self.cover_column(j.col)j = j.rightif self.search(k + 1):return Trueself.answer.pop()i = i.downj = i.leftwhile j!= i:self.uncover_column(j.col)j = j.leftself.uncover_column(c.col)return False
?運行
# 使用示例
dlx = DLX()
dlx.add_column('C1')
dlx.add_column('C2')
dlx.add_column('C3')
dlx.add_row({'C1': 1, 'C2': 0, 'C3': 1})
dlx.add_row({'C1': 0, 'C2': 1, 'C3': 1})
dlx.add_row({'C1': 1, 'C2': 1, 'C3': 0})
dlx.search(0)
四、算法優(yōu)勢
-
高效剪枝:通過列頭節(jié)點統(tǒng)計剩余1的數(shù)量,優(yōu)先選擇約束最強的列,大幅減少搜索空間。
-
快速狀態(tài)恢復:鏈表刪除和恢復的時間復雜度為O(1),回溯代價極低。
-
通用性:適用于所有可轉(zhuǎn)化為精確覆蓋的問題。
五、應用領(lǐng)域
- 數(shù)獨求解:數(shù)獨問題可以很自然地轉(zhuǎn)化為精確覆蓋問題,舞蹈鏈算法能夠快速有效地解決數(shù)獨謎題,無論是人工設計的數(shù)獨題目還是大規(guī)模生成數(shù)獨游戲。
- 計算機視覺:在圖像分割、目標識別等任務中,舞蹈鏈算法可用于解決一些組合優(yōu)化問題,例如將圖像中的像素點精確地劃分到不同的目標區(qū)域。
- 網(wǎng)絡設計:在網(wǎng)絡拓撲設計、資源分配等方面,舞蹈鏈算法可以幫助找到滿足特定要求的最優(yōu)網(wǎng)絡配置方案,例如在保證網(wǎng)絡連通性的前提下,合理分配網(wǎng)絡設備和鏈路資源。
-
N皇后問題:將棋盤轉(zhuǎn)化為精確覆蓋矩陣。
-
拼圖游戲:如俄羅斯方塊填充、多米諾骨牌覆蓋等。
總結(jié)
舞蹈鏈算法通過雙向鏈表的動態(tài)調(diào)整,將精確覆蓋問題的搜索效率提升到極致。盡管實現(xiàn)復雜,但它在處理組合優(yōu)化問題時表現(xiàn)卓越,尤其適合約束嚴格的場景。理解其核心在于掌握鏈表操作與回溯思想的結(jié)合。