问题
集合s=[x1,x2,…,xm]拥有m个元素,集合t=[s1,s2,…,sn]拥有n个元素,其中任意si是集合s的一个子集,si包含s中元素的数量为ni(1≤i≤n,0≤ni≤m)。
集合p是t的一个子集,且p中的子集所包含的s的元素可以覆盖集合s。设集合q是p中所有元素所包含的s中元素组成的集合。例如
stpq====[1,2,3,4,5,6,7,8,9][s1,s2,s3,s4,s5][s2,s4,s5][2,3,4,5,7,8,9] 其中
s1s2s3s4s5=====[1,2,3][2,3,4,5][6][7,8][8,9] 重复覆盖:对于∀x∈s,存在至少一个∃si∈p使得x∈si。例如集合s=[0,1,2,3],子集s1=[0,1],s2=[1,2],s3=[1,3]组成p=[s1,s2,s3],称这样的p是s的重复覆盖。显然p允许两个si⋂sj=∅。
精确覆盖:对于∀x∈s,存在且只存在一个∃si∈p使得x∈si。例如集合s=[0,1,2,3],子集s1=[0,1],s2=[1,2],s3=[2,3]组成p=[s1,s2],称这样的p是s的精确覆盖。显然p必然满足∀si,sj∈p,si⋂sj=∅,i=j。
求集合s和子集集合t=[s1,s2,…,sn]的重复覆盖和精确覆盖。
重复覆盖
设初始时重复覆盖p=∅,p的所有子集中的元素所组成的集合为q=∅。对于∀x∈s,若x∈/q,则在t中寻找si满足x∈si,将该子集加入重复覆盖中p=[si],将所有∀y∈si都加入q。每个子集只能加入p中一次,不能重复加入。当然也可以用染色来标记所有子集t=[s1,s2,…,sn]中已经加入p的那些元素,防止重复。
遍历完成后p即为重复覆盖。求重复覆盖的时间复杂度为O(n×m)。
精确覆盖
设初始时精确覆盖p=∅,p的所有子集中的元素所组成的集合为q=∅。
对于∀x∈s,每个包含x的子集都可能是p的一员,我们用递归的方式寻找所有可能。利用p中任意两子集的交集为空的特性,若x∈si且si∈p,那么只要sj⋂si=∅,那么必然sj∈/p。
初始时将t中的所有子集标记为白色(未被使用过)。遍历s的所有元素∀x∈s,进行以下步骤:
若x∈/q,这时有k个候选子集[si,sj,…](非红色,即未被使用过),它们都包含x。遍历这k个候选子集,假定选择si加入p,将所有∀y∈si加入q。然后将t中所有包含si中元素的其他子集染红(标记为已被使用);
重复上述过程只有两种结果:
(1) 所有x都已经加入q,这时的p即为精确覆盖;
(2) 所有x还没都加入q,这时所有的子集t=[s1,s2,…,sn]都已经被染红。说明上次在候选子集中做出的选择是错的。
我们需要记录每次选择时染红了哪些子集,以及当时遍历到的元素x。将si这次选择回退,把当时染红的子集重新染回白色,然后选择下一个候选者sj,再重复上述过程,指导找到精确覆盖。该过程是递归的。
下面演示舞蹈链算法:
sts1s2s3s4s5s6========[1,2,3,4,5,6,7][s1,s2,s3,s4,s5,s6][1,3,5,6][1,4,7][2,6,7][2,3,6][4,5,7][5] 其中n=6,m=7。
用行来表示子集,列来表示集合中的元素,可得n行m列矩阵:
舞蹈链算法用十字链表这种特别的数据结构将所有元素串联起来,坐标为[i,j]的节点下标是i×m+j,表示j∈s,j∈si。沿着十字链表的行可以遍历子集si的所有元素,沿着十字链表的列可以遍历所有包含j的子集。
(1) 对于上图中的示例,元素x=1的候选子集有s1,s2,假定选择s1,可得p=[s1],q=[1,3,5,6],然后将所有包含q中元素的子集染红(s1,s2,s3,s4,s5,s6);
(2) 接着考虑元素x=2,其候选子集有s3,s4,但它们都被染红了无法使用,这时不存在一个可用的子集,说明上一轮选择错误。不选择上一轮的s1,将s1,s2,s3,s4,s5,s6染回白色,假定选择s2,可得p=[s2],q=[1,4,7],染红s1,s2,s3,s5;
(3) 再次考虑元素x=2,其候选子集有s4(s3已被染红),假定选择s3,可得p=[s2,s4],q=[1,2,3,4,6,7],染红s1,s3,s4(s1,s3上一轮已被染红);
(4) 元素3,4∈q,因此可以直接跳过;
(5) 考虑元素x=5,其候选子集有s6(s5已被染红),假定选择s6,可得p=[s2,s4,s6],q=[1,2,3,4,5,6,7],染红s5,s6(s5之前已被染红);
(6) 元素6,7∈q,可以直接跳过。至此s=q,所有元素都被覆盖到,并且p=[s2,s4,s6]中任意两子集的交集为空,算法结束。
其实十字链表并不需要“染红”这个操作来标记一个子集是否可以使用,而是用添加、删除来操作链表上的节点。例如元素x=1选择子集s2=[1,4,7]时,将节点1从头节点一行中删除,将包含x=1的子集[s1,s2](包括s2自己)的元素也从所在的列中删除。如图所示:
仔细观察可知,删除之后仍然能够判定s1,s2包含元素1([1,8,15]拥有列关系),且可知s1=[1,3,5,6],s2=[1,4,7]([8,10,12,13],[15,18,21]拥有行关系)。这些遗留的列表指针可以恢复错误选择。对于子集s2=[1,4,7]上的所有元素进行相同的链表操作,等价于将s1,s2,s3,s5染红:
若尝试所有子集组合后仍无法找出精确覆盖,则说明该条件下不存在精确覆盖。舞蹈链算法的时间复杂度与递归的时间复杂度一样是O(nm)。
源码
DancingLink.h
DancingLink.cpp
测试
DancingLinkTest.cpp