众所周知,覆盖问题是NP问题,也就是只能通过暴力的手段解决,无法通过数据结构优化时间复杂度,而Dancing Links(DLX、舞蹈链、十字链表)可以以很高的效率处理类似的问题。
首现给出两种问题的定义:(本文只讲精确覆盖问题)
1、精确覆盖问题
给定一个01矩阵,我们需要选出最少的行数,使得每一列恰好只有一个1
2、重复覆盖问题
给定一个01矩阵,我们需要选出最少的行数,使得每一列至少有一个1
设01矩阵有n行m列,对于第一类问题,如果用暴力做法,每次枚举选出那些行,然后判断每种选法是否可行,复杂度为选择,判断
,总复杂度为
,非常之高。
不过我们仔细观察不难发现:
1、可以先看1的数量少的列,因为这些我们必须要在其中选择一行,1越少枚举的次数就越少
2、当我们选中某一行后,其他的带有当前列1的行都要删去,因为已经选定了,不能在选择更多的1了
3、我们选中的当行的其他列的1也可以通过这种方式删去,原理同上
举个例子:
对于当前这个6x7的矩阵,我们观察每一列1的数量,第1、4、7行有3个1,其他都是2个1,那么选择第二列,第二列的第3、5行为1,选择第三行,然后划去重复的行和确定的列:
那么在选完第三行处理完毕后的矩阵变成了
随后继续选取1最少的列第5列
选择第六行并划去重复的行和确定的列,当划去重复的行与列时,剩下的矩阵为空矩阵,那么就代表所有的列都被覆盖到了,返回递归的层数即为选择的行数。记录此时选择的行数,然后回到上一步,此时要把刚才删除的行与列都恢复
然后选择第五列中的其他行,也就是第一行继续递归操作
…………
这样的流程便是X算法。为了满足X算法大量删除、恢复行与列的操作,Donald E. Knuth 想到了用双向十字链表来维护这些操作。而在双向十字链表上不断跳跃的过程被形象地比喻成「跳跃」,因此被用来优化 X 算法的双向十字链表也被称为「Dancing Links」。
十字链表,顾名思义,有四个指针,分别指向上下左右的下一个为1的地址,下面是将一个矩阵存进十字链表的步骤:
void add(int& hh, int& tt, int x, int y)
{
row[idx] = x, col[idx] = y, s[y] ++ ;
u[idx] = y, d[idx] = d[y], u[d[y]] = idx, d[y] = idx;
r[hh] = l[tt] = idx, r[idx] = tt, l[idx] = hh;
tt = idx ++ ;
}
for (int i = 1; i <= n; i ++ )
{
int hh = idx, tt = idx;
for (int j = 1; j <= m; j ++ )
{
int x;
scanf("%d", &x);
if (x) add(hh, tt, i, j);
}
}
总结一下X算法的流程:
1、对于现在的矩阵M,选择并标记一行r,将r添加至S中;
2、如果尝试了所有的 r 却无解,则算法结束,输出无解;
3、标记与 r 相关的行
和
(相关的行和列与 X 算法 中第 2 步定义相同,下同);
4、删除所有标记的行和列,得到新矩阵 M';
5、如果 M' 为空,且 r 为全1,则算法结束,输出被删除的行组成的集合S;
如果 M' 为空,且 r 不全为1,则恢复与 r 相关的行
以及列
,跳转至步骤 1;
如果 M' 不为空,则跳转至步骤 1。
有了Dancing-Links,删除和恢复行在递归里就很容易写了:
bool dfs()
{
if (!r[0]) return true;
int p = r[0];
for (int i = r[0]; i; i = r[i])
if (s[i] < s[p])
p = i;
remove(p);
for (int i = d[p]; i != p; i = d[i])
{
ans[ ++ top] = row[i];
for (int j = r[i]; j != i; j = r[j]) remove(col[j]);
if (dfs()) return true;
for (int j = l[i]; j != i; j = l[j]) resume(col[j]);
top -- ;
}
resume(p);
return false;
}
再来看一题DLX的常用题型。在搜索一章中求解数独问题时,当时用的是搜索+剪枝优化,无论是代码量还是难度都是非常之大的。学过DLX算法之后就可以用十字链表来处理数独问题了: