一,舞蹈链简介(参考这里)
舞蹈链是Donald Knuth提出的技术,也叫做DLX,目的是快速实现他提出的X算法,这是一个递归,不确定的,深度优先,回溯的算法,这个算法主要用来解决精确覆盖的问题,简而言之,就是给定一个0和1组成的矩阵,是否有一些行使得每个列中1仅仅出现一次。如果改成每一列至少含有一个1,那么这就变成了重复覆盖问题。X算法可以用来快速的解决精确覆盖问题和重复覆盖问题。如下边的矩阵的一个精确覆盖的解是行1,4,5。
舞蹈链中最重要的思想就是将一个结点从双向链表中删除和将这个结点恢复。假设x是一个双向链表的结点,令L[x]和R[x]指向x的前驱和x的后继结点,那么操作
下面介绍一下X算法的主要步骤:
If A is empty, the problem is solved; terminate successfully.
Otherwise choose a column, c (deterministically).//这里选择1个数最小的列可以较快得到结果
Choose a row, r, such that A[r, c] = 1 (nondeterministically).
Include r in the partial solution.
For each j such that A[r, j] = 1,
delete column j from matrix A;
for each i such that A[i, j] = 1,
delete row i from matrix A.
Repeat this algorithm recursively on the reduced matrix A.
下面介绍一个X算法的具体实现, 在矩阵中的一个1用数据对象x的五个字段表示L[x],R[x],U[x],D[x],C[x],矩阵的行通过L和R字段形成一个双向循环链表,列通过U和D字段形成一个双向循环链表,每一个列链表还包含一个特殊的数据对象叫做链头,这些链头是一个称作列对象的大型对象的一部分,每个列对象y包含一个普通数据对象的5个字段L,R,U,D,C,另外加上两个字段S和N,分别表示一个列中1的个数和标示输出答案的符号,每个数据对象的C字段应该指向相应列头的列对象。这里表头的L和R连接着所有需要被覆盖的列。
下边是一个可行的伪代码,这个函数叫做dance(k),起始k=0:
如果R[h] = h,打印当前的解,并返回。
否则选择一个列对象c。
覆盖列c。//先将c列中为1的行删除,这些行只能有一个
对于每一个r=D[c],D[D[c]],...,当r!=c,
设置ans[k] = r。
对于每一个j=R[r],R[R[r]],...,当j!=r,//这里不需要再覆盖元素r所在的列了,因为这个列就是c。
覆盖列j。
dance(k + 1);
对于每一个j=L[r],L[L[r]],...,当j!=r,
取消列j的覆盖。
取消列c的覆盖。
下面介绍选择列c的操作,我们可以简单的设置c=R[h],这样选择的是最左边没有被覆盖的列。如果我们希望分支数最小,就要选择1的个数最小的列,S字段就是用来这样选择列c的,如果不使用这样的方法,那么S字段是没有用的。
s = INF;
for (int i = R[h]; i != h; i = R[i]) {
if (S[i] < s) {
s = S[i];
c = i;
}
}
覆盖c的操作就是将c从表头删除,并从其他链表中删除列c的所有行。
L[R[c]] = L[c];
R[L[c]] = R[c];
for (int i = D[c]; i != c; i = D[i]) {
for (int j = R[i]; j != i; j = R[j]) {
--S[C[j]];
U[D[j]] = U[j];
D[U[j]] = D[j];
}
}
下面是还原已经删除的c,这里是舞蹈链的精髓:
for (int i = U[c]; i != c; i = U[i]) {
for (int j = L[i]; j != i; j = L[j]) {
++S[C[j]];
U[D[j]] = j;
D[U[j]] = j;
}
}
L[R[c]] = c;
R[L[c]] = c;
上边还原操作执行顺序与覆盖操作的顺序相反,对于j可以以任何顺序来穿过第i行,但是其他的顺序是很重要的(这是Knuth的论文中说的,但是我确实不知道这样做的原因是什么)。
在实际的应用中,我们不仅需要上边的字段,还需要一些其他的字段,比如用Row[x]来表示结点x所在的行,上边用C[x]表示结点x的列,我们这里换做Col[x]表示。上边的算法仅仅是给定一个矩阵来求解答案,我们在应用的过程中还需要建立这个矩阵,需要一个字段来H[r]表示第r行的第一个结点,这样便于建立矩阵。
另外还需要做的是对这些元素进行初始化,比如初始化列表头和每一列的1的个数。
下面是一个实现,可以过hustoj上的一个不用建模题目1017(参考kuangbin的模板):
/*************************************************************************
> File Name: my_dancing_links.cpp
> Author: gwq
> Mail: gwq5210@qq.com
> Created Time: 2015年09月14日 星期一 23时43分43秒
************************************************************************/
#include <cmath>
#include <ctime>
#include <cctype>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <set>
#include <queue>
#include <stack>
#include <string>
#include <vector>
#include <sstream>
#include <iostream>
#include <algorithm>
#define INF (INT_MAX / 10)
#define clr(arr, val) memset(arr, val, sizeof(arr))
#define pb push_back
#define sz(a) ((int)(a).size())
using namespace std;
typedef set<int> si;
typedef vector<int> vi;
typedef map<int, int> mii;
typedef pair<int, int> pii;
typedef long long ll;
const double esp = 1e-5;
#define N 1010 // 矩阵行列个数
#define M 100010 // 结点个数
struct DLX {
int L[M], R[M], U[M], D[M]; // 分别表示结点的左边,右边,上边,下边的结点,其中R[0]表示讨论中的h,表示链表头的开始
int Row[M], Col[M]; // 分别表示结点所在行和所在列
int H[N], S[N]; // 分别表示第x行的第一个元素和第y列有多少个1
int ansd, ans[N]; // 分别表示表示一个解中行的数量和某一个解
int n, m, size; // 分别表示行列的个数,size表示当前元素的个数,size的当前值表示下一个结点
void init(int a, int b)
{
n = a;
m = b;
for (int i = 0; i <= m; ++i) {
R[i] = i + 1;
L[i] = i - 1;
U[i] = D[i] = i;
S[i] = 0;
}
L[0] = m;
R[m] = 0;
size = m + 1;
for (int i = 1; i <= n; ++i) {
H[i] = -1;
}
}
void link(int r, int c)
{
++S[c];
Col[size] = c;
Row[size] = r;
U[size] = U[c];
D[size] = c;
D[U[c]] = size;
U[c] = size;
if (H[r] < 0) {
H[r] = L[size] = R[size] = size;
} else {
L[size] = L[H[r]];
R[size] = H[r];
R[L[H[r]]] = size;
L[H[r]] = size;
}
++size;
}
void eremove(int c)
{
L[R[c]] = L[c];
R[L[c]] = R[c];
for (int i = D[c]; i != c; i = D[i]) {
for (int j = R[i]; j != i; j = R[j]) {
--S[Col[j]];
U[D[j]] = U[j];
D[U[j]] = D[j];
}
}
}
void eresume(int c)
{
for (int i = U[c]; i != c; i = U[i]) {
for (int j = L[i]; j != i; j = L[j]) {
++S[Col[j]];
U[D[j]] = j;
D[U[j]] = j;
}
}
L[R[c]] = R[L[c]] = c;
}
bool edance(int d)
{
if (R[0] == 0) {
ansd = d;
return true;
}
int c = R[0];
for (int i = R[0]; i != 0; i = R[i]) {
if (S[i] < S[c]) {
c = i;
}
}
eremove(c);
for (int i = D[c]; i != c; i = D[i]) {
ans[d] = Row[i];
for (int j = R[i]; j != i; j = R[j]) {
eremove(Col[j]);
}
if (edance(d + 1)) {
return true;
}
for (int j = L[i]; j != i; j = L[j]) {
eresume(Col[j]);
}
}
eresume(c);
return false;
}
};
DLX g;
int n, m;
int main(int argc, char *argv[])
{
while (scanf("%d%d", &n, &m) != EOF) {
g.init(n, m);
for (int i = 1; i <= n; ++i) {
int cnt = 0;
scanf("%d", &cnt);
for (int j = 0; j < cnt; ++j) {
int tmp;
scanf("%d", &tmp);
g.link(i, tmp);
}
}
if (!g.edance(0)) {
printf("NO\n");
} else {
printf("%d", g.ansd);
for (int i = 0; i < g.ansd; ++i) {
printf(" %d", g.ans[i]);
}
printf("\n");
}
}
return 0;
}
下面来解决另外一个精确覆盖问题,zoj 3209 Treasure Map,类似砖块的覆盖。有一个矩形的区域,现在有一些小的矩形,问能不能找到一些矩形可以刚好组成这个大的矩形。矩形用一个直角坐标系表示,这里需要建立一个模型,将这个问题转化成精确覆盖的问题,因为小的矩形只能选择一次,所以用没一行表示一个小矩形,我们把这个大矩形的每一个方格看成一列,如果某一个小矩形覆盖了这些方格,那么这些列就为1,否则就为0,那么我们得到的是一个p行,n*m列的矩阵,第i个行表示第i个小矩形可以覆盖的方格,这样我们就需要把n*m个小方格映射到1到n*m个数(也就是把[1,1]-[n,m]的二元组变成数字,那么就是(x - 1) * m + y - 1 + 1),这样就建立了一个01矩阵,这个问题就变成了精确覆盖问题。
代码如下,其实最重要的是建模:
/*************************************************************************
> File Name: my_3209.cpp
> Author: gwq
> Mail: gwq5210@qq.com
> Created Time: 2015年09月16日 星期三 23时13分39秒
************************************************************************/
#include <cmath>
#include <ctime>
#include <cctype>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <set>
#include <queue>
#include <stack>
#include <string>
#include <vector>
#include <sstream>
#include <iostream>
#include <algorithm>
#define INF (INT_MAX / 10)
#define clr(arr, val) memset(arr, val, sizeof(arr))
#define pb push_back
#define sz(a) ((int)(a).size())
using namespace std;
typedef set<int> si;
typedef vector<int> vi;
typedef map<int, int> mii;
typedef pair<int, int> pii;
typedef long long ll;
const double esp = 1e-5;
#define N 1000
#define M 1000000
struct DLX {
int L[M], R[M], U[M], D[M];
int Row[M], Col[M];
int H[N], S[N];
int ansd, ans[N];
int n, m, size;
void init(int a, int b)
{
n = a;
m = b;
for (int i = 0; i <= m; ++i) {
S[i] = 0;
L[i] = i - 1;
R[i] = i + 1;
U[i] = D[i] = i;
}
L[0] = m;
R[m] = 0;
size = m + 1;
for (int i = 1; i <= n; ++i) {
H[i] = -1;
}
}
void link(int r, int c)
{
++S[c];
Row[size] = r;
Col[size] = c;
U[size] = U[c];
D[size] = c;
D[U[c]] = size;
U[c] = size;
if (H[r] < 0) {
H[r] = L[size] = R[size] = size;
} else {
L[size] = L[H[r]];
R[size] = H[r];
R[L[H[r]]] = size;
L[H[r]] = size;
}
++size;
}
void eremove(int c)
{
L[R[c]] = L[c];
R[L[c]] = R[c];
for (int i = D[c]; i != c; i = D[i]) {
for (int j = R[i]; j != i; j = R[j]) {
S[Col[j]]--;
U[D[j]] = U[j];
D[U[j]] = D[j];
}
}
}
void eresume(int c)
{
for (int i = U[c]; i != c; i = U[i]) {
for (int j = L[i]; j != i; j = L[j]) {
++S[Col[j]];
U[D[j]] = D[U[j]] = j;
}
}
L[R[c]] = R[L[c]] = c;
}
bool edance(int d)
{
// 剪枝
if (ansd != -1 && ansd <= d) {
return true;
}
if (R[0] == 0) {
if (ansd == -1) {
ansd = d;
} else if (ansd > d) {
ansd = d;
}
return true;
}
int c = R[0];
for (int i = R[0]; i != 0; i = R[i]) {
if (S[i] < S[c]) {
i = c;
}
}
eremove(c);
for (int i = D[c]; i != c; i = D[i]) {
ans[d] = Row[i];
for (int j = R[i]; j != i; j = R[j]) {
eremove(Col[j]);
}
edance(d + 1);
for (int j = L[i]; j != i; j = L[j]) {
eresume(Col[j]);
}
}
eresume(c);
return false;
}
};
DLX g;
int main(int argc, char *argv[])
{
int t;
scanf("%d", &t);
while (t--) {
int n, m, p;
scanf("%d%d%d", &n, &m, &p);
g.init(p, n * m);
for (int i = 1; i <= p; ++i) {
int x1, x2, y1, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
for (int j = x1 + 1; j <= x2; ++j) {
for (int k = y1 + 1; k <= y2; ++k) {
g.link(i, (j - 1) * m + k);
}
}
}
g.ansd = -1;
g.edance(0);
printf("%d\n", g.ansd);
}
return 0;
}