dance links 中文版
http://sqybi.com/works/dlxcn/
精确覆盖问题
给定一个由0和1组成的矩阵,是否能找到一个行的集合,使得集合中每一列都恰好包含一个1?例如,下面这个矩阵
就包含了这样一个集合(第1,4,5行)。我们把列想象成全集的一些元素,而行看作全集的一些子集;或者我们可以把行想象成全集的一些元素,而把列看作全集的一些子集;那么这个问题就是要求寻找一批元素,它们与每个子集恰好有一个交点。不管怎么说,这都是一个很难的问题,众所周知,当每行恰包含3个1时,这是个一个NP-完全问题。自然,作为首选的算法就是回溯了。
解决精确覆盖问题。对于接下来的非确定性算法,由于我们没有想到更好的名字,我们将称之为X算法,它能够找到由特定的01矩阵A定义的精确覆盖问题的所有解。X算法是实现试验——错误这一显而易见的方法的一段简单的语句(确实,一般来说,我想不到别的合理的方法来完成这个工作)。
如果A是空的,问题解决;成功终止。
否则,选择一个列c(确定的)。
选择一个行r,满足 A[r, c]=1 (不确定的)。
把r包含进部分解。
对于所有满足 A[r,j]=1 的j,
从矩阵A中删除第j列;
对于所有满足 A[i,j]=1 的i,
从矩阵A中删除第i行。
在不断减少的矩阵A上递归地重复上述算法。
对r不确定的选择意味着这个算法本质上把自身复制给许多独立的子算法;每个子算法继承了当前的矩阵A,但在考虑不同行r的同时对其进行了删减。如果列c全部是0,那么就不存在子算法而且这个过程会不成功地终止。很自然地,所有的子算法搭建了一棵搜索树,其根部就是初始问题,并且第k层的每个子算法对应k个选择的行。回溯就是前序遍历这棵树的过程,即“深度优先”。
hust acm Exact cover使用dance links求解
/*
从昨天下午就开始搞,今天中午才搞定,Dancing Links 果然高深
神奇的双向环形十字链表······转的头晕了-_-||不过蛮有成就感的^v^
记录一下解题过程,先反反复复看了一遍Donald E.Knuth的论文,就开始攻hust 1017这道题目,
题目意思很明确:
给定一个由0和1组成的矩阵,是否能找到一个行的集合,使得集合中每一列都恰好包含一个1。
论文中有详细解答,具体思路就是:
1. 任意选择一列c,然后删除它(这个删除并不是普通的把这一列删除,而是要将这一列中有1的格子所在的行全部删除)
2. 然后对这一列上有1的每一行进行枚举,当枚举到第i行时
该行上所有有1的列j全部删除(同上删除法)
3. 然后递归进入下一次,直到所有的列均被删除则有解
4. 恢复对j的删除
5. 恢复对c的删除
*/
#include <iostream>
using namespace std;
struct point {
int L;
int R;
int U;
int D;
int Sum;
int x, y;
}p[ 1010 * 1010 ];
int n, m;
int i, j, k;
int map[1001][1001];
int sor[1001];
int flag;
int stack[1001], top;
int Num(int x, int y) {
return x * 1001 + y;
}
//删除c列
void CoverCol(int c) {
int i, j;
p[ p[ c ].R ].L = p[ c ].L;
p[ p[ c ].L ].R = p[ c ].R;
//删除c列中每个有1的行
i = c;
for(i = p[i].D; i != c; i = p[i].D) {
j = i;
p[ p[i].y ].Sum --;
for(j = p[j].R; j != i; j = p[j].R) {
p[ p[j].D ].U = p[ j ].U;
p[ p[j].U ].D = p[ j ].D;
}
}
}
//恢复c列
void Release(int c) {
int i, j;
p[ p[ c ].R ].L = c;
p[ p[ c ].L ].R = c;
//恢复c列中每个有1的行
i = c;
for(i = p[i].U; i != c; i = p[i].U) {
j = i;
p[ p[i].y ].Sum ++;
for(j = p[j].L; j != i; j = p[j].L) {
p[ p[j].D ].U = j;
p[ p[j].U ].D = j;
}
}
}
int dfs(int k) {
int i, j, l, m;
if(flag) return 1;
//得解输出
if(p[ 0 ].R == 0) {
printf("%d", top);
for(i = 0; i < top; i++)
printf(" %d", stack[i]);
puts("");
flag = 1;
return 1;
}
int c = 0; //每次取出没有被覆盖的并且1的个数最小的一列
int Min = INT_MAX;
i = c;
for(i = p[i].R; i ; i = p[i].R) {
if(p[ p[i].y ].Sum < Min) {
Min = p[ p[i].y ].Sum;
c = i;
}
}
//将这一列删除
CoverCol(c);
i = c;
//枚举c列中的每一行
for(i = p[i].D; i != c; i = p[i].D) {
//p[i].x 作为当前枚举的行,进栈
stack[ top++ ] = p[i].x;
j = i;
//对于该枚举的行,删除该行上1的格子所在的列
for(j = p[j].R; j != i; j = p[j].R) {
CoverCol(p[j].y);
}
if ( dfs(k+1) )
return 1;
//对于该枚举的行,恢复该行上1的格子所在的列
j = i;
for(j = p[j].L; j != i; j = p[j].L) {
Release(p[j].y);
}
top --;
}
//恢复c
Release(c);
return 0;
}
int main() {
int T = 0;
while(scanf("%d %d", &n, &m) != EOF) {
T ++;
for(i = 1; i <= n; i++) {
scanf("%d", &k);
for(j = 0; j < k; j++) {
scanf("%d", &sor[j]);
map[i][sor[j]] = T;
}
int lef = Num(i, sor[0]);
int rig = Num(i, sor[k-1]);
p[ lef ].L = rig;
p[ lef ].x = i;
p[ lef ].y = sor[0];
for(j = 1; j < k; j++) {
int cur = Num(i, sor[j]);
p[ Num(i, sor[j-1]) ].R = cur;
p[ cur ].L = Num(i, sor[j-1]);
p[ cur ].x = i;
p[ cur ].y = sor[j];
}
p[ rig ].R = lef;
}
p[0].R = 1;
for(i = 1; i <= m; i++) {
int No = Num(0, i);
if(i + 1 <= m)
p[ No ].R = Num(0, i+1);
else
p[ No ].R = 0;
p[ No ].L = Num(0, i-1);
p[ No ].x = 0;
p[ No ].y = i;
p[ No ].Sum = 0;
int last = No;
for(j = 1; j <= n; j++) {
if( map[j][i] == T ) {
p[ last ].Sum ++;
int now = Num(j, i);
p[ No ].D = now;
p[ now ].U = No;
No = now;
}
}
p[ No ].D = Num(0, i);
p[ Num(0, i) ].U = No;
if( !p[ last ].Sum ) {
printf("NO/n");
break;
}
}
if(i == m + 1) {
flag = 0;
top = 0;
dfs(0);
if(!flag)
puts("NO");
}
}
}