舞蹈链—精确覆盖问题
在正式介绍舞蹈链之前,先让我们明白其应用的问题—精确覆盖问题
精确覆盖问题
该问题是关于判断一个集合是否是一个精确覆盖的问题
满足以下条件的集合为一个精确覆盖:
- S是全集X的部分子集的集合
- S中任意两个集合没有交集
- S中所有集合的并集为全集X
举例
那么
若
用01矩阵表示子集与全集的关系
- 列是全集元素,
- 行是子集
上述例子用01矩阵表示
我们可以看到,精确覆盖在矩阵中表现出一种特点
- 每一列在被选中的行(子集)中,有且仅有一个1
交叉十字循环双向链
在舞蹈链算法中,我们使用交叉十字循环双向链存储矩阵
这有几点好处
- 由于01矩阵很可能是稀疏矩阵,所以只为值为1的位置创建节点能节省空间
- 假如我们有A、B、C三个连续节点,删除B只需进行操作
而在回溯恢复B时,只需进行操作
舞蹈链求解
把上述例子用交叉十字循环双向链表示
- 方形节点是我们实际中要创建的节点,包括头结点、列首节点与所有的1节点
- 所有1节点中将额外保存其行标和列首节点的指针
- 找到解的条件是头结点的左右节点是他自己(这代表着所有列元素都被覆盖,会在接下来的步骤继续解释)
步骤一
- 取得Header右节点,Col1且Col1有其余节点
- 为了覆盖Col1,那么行1、3、4中一定有一个是解的一部分,把行1先放入答案
- 行1覆盖了列1、列3,为了不重复覆盖,我们先删除同样覆盖列1、列3的行
得到
步骤二
重复上述步骤,将行2放入答案
得到
步骤三
由于Header的左右节点是其自己表示已找到解
If
这看起来很顺利,那如果第一次选择的答案是行3,会是如何呢
- 行3覆盖了列1、列2、列3
- 删除对应行与列
得到
这时,我们发现Header右节点Col4下没有其余节点,这表示查找失败,准备回溯
代码细节
代码基于Python3
- 在删除时,会将列首节点与左右断开,同时将非本列的节点与上下断开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17def RemoveCol(self, c):
'''
删除列首节点及对应行
:param c: 列首节点
'''
c.right.left = c.left
c.left.right = c.right
i = c.down
while i!=c:
j = i.right
while j!=i:
j.down.up = j.up
j.up.down = j.down
j.col.size-=1
j = j.right
i=i.down - 恢复节点是与删除完全相同的逆操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16def ResumeCol(self, c):
'''
恢复列首节点及对应行
:param c: 列首节点
'''
i = c.up
while i != c:
j = i.left
while j != i:
j.up.down = j
j.down.up = j
j.col.size+=1
j = j.left
i = i.up
c.right.left = c
c.left.right = c - 主要递归函数,每次获取元素最少的列有利于加快速度,每次递归中,被删除的列的顺序与被恢复的列的顺序也是相逆的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33def Solve(self):
'''
递归解决问题
'''
if self.Header.right == self.Header:
for i in self.ans:
self.matrix[i[0]][i[1]] = chr(ord('0')+i[2])
return True
c = self.get_min_size_col()
self.RemoveCol(c)
i = c.down
while i!=c:
self.ans.append(i.grid)
j = i.right
while j!=i:
self.RemoveCol(j.col)
j = j.right
if self.Solve():
return True
self.ans.pop(-1)
j = i.left
while j!=i:
j = j.left
i = i.down
self.ResumeCol(c)
return False