Dancing links是一种能高效实现Knuth的X算法的技术,它可以使很多搜索问题得到极大的优化。
假设x是一个双向链表中的一个节点,L[x]表示X的前驱,R[x]表示x的后继,
则R[L[x]] = R[x], L[R[x]] = L[x]这一操作可以把x从链表中移除,这是众所周知的,
当然,一个细致的程序员还会用 L[x] = R[x] = x或 L[x] = R[x] = NULL这样的操作来清除x,
以免发生内存泄露,所以只有很少的程序员意识到,若不清除x则 R[L[x]] = x, L[R[x]] = x
这个操作可以把刚才移除的x恢复到之前的链表中,而这样的恢复功能在有些时候是很有用的,
例如,在一个交互性的程序中,用户很可能想撤销他所做的一个或一系列操作,恢复到先前的状态。
另一个典型的应用是在回溯程序中。
精确覆盖问题:给出一个集合S={a,b,c,d,e,f,g}的一些子集S1={c,e,f}, S2={a,d,g},
S3={b,c,f}, S4={a,d},S5={b,g}, S6={d,e,g},选取哪些子集可以使得A中的元素全部出现并只出现一次,
即用那些子集可以完全覆盖全集S,并且保证没有元素会被重复覆盖?
({S1={c,e,f},S4={a,d},S5={b,g}}是一组解)
我们可以用一个0-1矩阵来表示这些集合:
矩阵M中每一列对应全集中的一个元素,若M[row=i][col=j]=1则表示集合i包含j元素,
如:第一行S1的c、e、f列为1表示S1包含元素c、e、f。
因此我们可以把之前的问题描述为:给定一个由0和1组成的矩阵M,是否能找到一个行的集合,
使得集合中每一列都恰好包含一个1?
不管怎么说,这都是一个很难的问题,当每行恰包含3个1 时,这是个一个NP-完全问题,自然,
作为首选的算法就是回溯了。
解决精确覆盖问题: Knuth 的X算法,它能够找到由特定的01矩阵定义的精确覆盖问题的所有解。
http://blog.csdn.net/sunny606/article/details/7833551
详细的解说:
http://www.cnblogs.com/steady/archive/2011/03/15/1984791.html
这里有很多例题:
http://www.cppblog.com/notonlysuccess/archive/2009/07/10/89701.html
选了poj的两道题:
http://blog.csdn.net/dicpu/article/details/7407163
http://blog.sina.com.cn/s/blog_62ebd4410100g53u.html
http://scturtle.is-programmer.com/posts/27861.html
看了比较久了,还是不会,等以后有时间再搞吧。
poj3740:精确不覆盖分配
题意:
照着别人代码自己写了一下:
注意数组大小要开大点。
#include<cstdio> #define maxn 700 #define maxm 6666 #define max 0x7fffffff int n,m,size; int L[maxm],R[maxm],U[maxm],D[maxm]; int H[maxn],C[maxm],S[maxn]; void init() { for(int i=0;i<=m;i++) { S[i]=0; D[i]=U[i]=i; R[i]=i+1,L[i+1]=i; } R[m]=0; size=m+1; } void link(int r,int c) { ++S[C[size]=c];//新建一个点 D[size]=D[c]; U[D[c]]=size; U[size]=c; D[c]=size; if(H[r]<0) H[r]=L[size]=R[size]=size; else { R[size]=R[H[r]]; L[R[size]]=size; L[size]=H[r]; R[H[r]]=size; } size++; } void remove(int c) { R[L[c]]=R[c]; L[R[c]]=L[c]; for(int i=D[c];i!=c;i=D[i]) for(int j=R[i];j!=i;j=R[j])//? U[D[j]]=U[j],D[U[j]]=D[j],--S[C[j]];//? } void resume(int c) { R[L[c]]=L[R[c]]=c; for(int i=U[c];i!=c;i=U[i]) for(int j=L[i];j!=i;j=L[j]) U[D[j]]=D[U[j]]=j,++S[C[j]]; } bool dancing(int k) { if(R[0]==0) { puts("Yes, I found it"); return 1; } int tmp,c; for(int tmp=max,i=R[0];i;i=R[i]) { if(S[i]<tmp) tmp=S[c=i]; } remove(c); for(int i=D[c];i!=c;i=D[i]) { for(int j=R[i];j!=i;j=R[j]) remove(C[j]);//? if(dancing(k+1)) return 1; for(int j=L[i];j!=i;j=L[j]) resume(C[j]); } resume(c); return 0; } int main() { while(scanf("%d%d",&n,&m)!=EOF) { init(); for(int i=1; i<=n; i++) { H[i]=-1; for(int j=1; j<=m; j++) { int a; scanf("%d",&a); if(a) link(i,j); } } if(!dancing(0)) puts("It is impossible"); } return 0; }
我想了很久才想清楚:
删除的程序是按行,列,行来进行的。先找S【】最小的列,然后找有哪些行与此列交叉,遍历每个行,把除此列外的交叉点,本行其他的所有交叉点所在行去掉,然后再吧所在列去掉,最后吧此列上的交叉点的行在全部去掉。