转自:http://blog.sina.com.cn/s/blog_6a46cc3f0100s2d4.html
其实在去年就从傅立超大牛学长的QQ空间里看过了Dancing Linksys这么优美的名字,但是一直以为凡是和链表有联系的都是深不可碰的。直到前阵子训练赛做到了zoj3209这个裸的题,别人瞬A而我比赛结束还没一丁点头绪。一问其实是个裸的DLX。于是终于下定决心看下DLX 。而且发现我居然爱上了DLX,我这种懒人就喜欢这种可以套模板的题(*^__^*) 嘻嘻……
一,数据结构与实现:
首先学习了momodi的经典论文《Dancing Links在搜索中的运用》,此外还看了Knuth的PPT和论文,但是还是momodi的易懂嘿。强烈推荐!
Dancing Links 主要是用双向十字链表来存储稀疏矩阵,来达到在搜索中的优化。(关于稀疏矩阵这点,我显然没好好理解,导致spoj1771的N皇后问题不懂得直接构造十字链表而是先构造了一个极其稀疏的矩阵而导致TLE了。
)
Dancing这个词是来形容以下操作的:
1) 删除一个结点:l[r[x]]=l[x], r[l[x]]=r[x];
2) 恢复一个结点:l[r[x]]=x, r[l[x]]=x;
这里巧妙地运用了我们排斥的野指针舞蹈般的操作来快速地实现了结点的删除与恢复。这就是DLX的最重要的数据结构的优化。
实现时,我们用数组来模拟链表,方便建立矩阵同时加快速度。
1) l[x], r[x], u[x], d[x],分别表示该节点的左右上下结点,构成一个双向十字链表。
2) c[x]记录该节点所在的列结点,因为我们在一开始留了一行存总的头指针和各列的头指针。head定义为0指向总的头指针。
3) 有时候根据需要要开个行指针头数组。还有时候需要row[x]记录该节点所在矩阵中的行数。
4) s[x]记录每列列链表中结点的个数。
5) o[x]记录搜索结果。
二,模型解析与模板实现:
下面说下DLX的两个模型。
DLX原理:
这个问题其实算NP问题,Knuth有一个X算法实现,但是还是不够理想,普通的搜索虽然可以解决,但是必须超时。而用DLX从数据结构角度进行优化,可以通过仅保留矩阵中有用部分来提高搜索速度。随着搜索迭代深度的增加,矩阵中不需要地部分被切除而迅速地变得稀疏。还有一些我们没想到的优化,DLX也实现了。
一)精确覆盖模型(Exact Cover Problem):
问题描述:选定最少的(或只是要求某些)行,使得每列有且仅有一个1。
实现思路:首先将当前要覆盖的列以及使得能够覆盖到该列的行全部去掉,再逐行枚举添加的方法。枚举某一行r,则设定当前列的解为该行r,那么该行能够覆盖到的列必然全部都可以不必再搜,因此将该行r覆盖到的列全部去掉。又由于去掉的那些列相当于已经有了解,所以能够覆盖到那些去掉的列的行也应当全部去掉。
模板实现:
//将输入条件转化为01矩阵。
void makegragh(){……}
//将01矩阵转化为十字链表,cnt表示结点个数,01矩阵是n行m列的。
void initial()
{
int i, j, rowh;
memset(s, 0, sizeof(s));
for(i=head; i<=m; i++)
{
r[i]=(i+1)%(m+1);
l[i]=(i-1+m+1)%(m+1);
u[i]=d[i]=i;
}
cnt=m+1;
for(i=1; i<=n; i++)
{
rowh=-1;
for(j=1; j<=m; j++)
if(mat[i][j])
{
s[j]++;
u[cnt]=u[j];
d[u[j]]=cnt;
u[j]=cnt;
d[cnt]=j;
row[cnt]=i;
c[cnt]=j;
if(rowh==-1)
{
l[cnt]=r[cnt]=cnt;
rowh=cnt;
}
else
{
l[cnt]=l[rowh];
r[l[rowh]]=cnt;
r[cnt]=rowh;
l[rowh]=cnt;
}
cnt++;
}
}
}
//删除矩阵中的列及其相应的行
void myremove(const int &cur)
{
l[r[cur]]=l[cur];
r[l[cur]]=r[cur];
for(int i=d[cur]; i!=cur; 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]];
}
}
}
//恢复。顺序和remove相反
void myresume(const int &cur)
{
for(int i=u[cur]; i!=cur; 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[cur]]=cur;
r[l[cur]]=cur;
}
//最原始版本,只需输出一组合法解
bool dfs(const int &k)
{
if(r[head]==head)//不需判断k是否等于N
{
sort(o, o+k);
for(int i=0; i<k; i++)
printf("%d %d\n", no[o[i]].s, no[o[i]].e);
printf("\n");
return true;
}
int ms=INF, cur=0;
for(int t=r[head]; t!=head; t=r[t])
{
if(s[t]<ms)
{
ms=s[t];
cur=t;
}
}
myremove(cur);
for(int i=d[cur]; i!=cur; i=d[i])
{
o[k]=row[i];
for(int j=r[i]; j!=i; j=r[j])
myremove(c[j]);
if(dfs(k+1))return true;
for(int j=l[i]; j!=i; j=l[j])
myresume(c[j]);
}
myresume(cur);
return false;
}
//实现DLX,在main函数里用dfs(0)
//最优版本,输出最小行数,仍然优先考虑列个数最少的
void dfs(const int &k)
{
if(r[head]==head)
{
if(k<num)num=k;
return;
}
if(k>=num)return;
int ms=INF, cur=0;
for(int t=r[head]; t!=head; t=r[t])
{
if(s[t]<ms)
{
ms=s[t];
cur=t;
}
}
myremove(cur);
for(int i=d[cur]; i!=cur; i=d[i])
{
for(int j=r[i]; j!=i; j=r[j])
{
myremove(c[j]);
}
dfs(k+1);//求各种解中最小值仍然从个数最少的列开始,只是这里在找到一个解后不返回,而是继续,dfs函数改为void返回值
for(int j=l[i]; j!=i; j=l[j])
myresume(c[j]);
}
myresume(cur);
return ;
}
解决问题:
Hust 1017 exact cover:
是个精确覆盖的裸题,用来测试模板正确性的。
Zoj3209 treasure map:
求最小精确覆盖数,用上面的第二个dfs模板即可,也是裸题。行n等于拼图块的个数P,列m等于矩形格子数N*M。构图时注意输入的是点数不是格子数,所以要加1。
for(i=1; i<=P; i++)
{
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
for(j=x1+1; j<=x2; j++)//注意加1, 因为给的是点不是格子的位置!!!
for(k=y1+1; k<=y2; k++)
mat[i][(j-1)*M+k]=1;
}
Hdu3663Power Stations
2010年哈尔滨区域赛的题,构图比较麻烦。但是构完图就直接套模板了。
行的定义是每个发电站可行的区域,
比如(1, 5)则为
{(0, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5)
(3, 3), (3, 4), (3, 5), (4, 4), (4, 5), (5, 5)}
再举个例子,(3, 4)则为{(0, 0), (3, 3), (3, 4), (4, 4)}
这是因为每个发电站最多开动一次,且一旦关了就不能再开了,所以只能枚举连续的区域,注意无论是多少都有(0, 0)这一个。
列的定义是N*D+N。即N个城市D天的供电状况和N个发电站的供电状况。+N是因为每个发电站用且只用一次,每列只能被覆盖一次即限制了使用次数为1。
具体实现时我用了前向星存储矩阵,先记录如下:
struct edge
{
int to, next;
}ed[520];
int v[70], num;//前向星表示
//插入from到to的路
void insert(int from, int to)
{
ed[num].to=to;
ed[num].next=v[from];
v[from]=num++;
}
//初始化
memset(v, -1, sizeof(v));
num=0;
数独模型:
N*N的数独,构造的矩阵有N*N*N行,因为一共N*N个小格,每个小格有N中可能性(1—N),每一种可能对应这一行。列则有(N+N+N)*N+N*N列。其中前面3个N分别代表着N行N列和N小块,乘以N表示N中可能,每种可能只可以选一个。N*N表示N*N个小格,限制每一个小格只可以放一个地方。
这里可见精确覆盖模型的转化技巧,由于每一列有且仅有一个1,故我们可以把列分为两类,一类代表着每一个小格的可能性,另一类代表每个区域的某个数的可能性。
Poj3074是个9*9的数独。如果该位置已经确定,则只插入一行,否则插入9行,代表9中可能。
Poj3076是个16*16的矩阵,直接把3074的一些数一改就行了。
N皇后模型:(spoj1771)
定义行列左斜线右斜线为列,共6*N-2列。定义每个小格为行,共N*N行。接着就是精确覆盖模型了。需要注意的是,我们在找列元素最小的c时,判断语句不再是i!=head,而是i<=N,因为前N列表示的是棋盘中的行,行所影响的就能覆盖所有的棋盘,不需要做后面的。
另外由于实际上每一行只有4个1,所以相比建矩阵后建图,要比直接建图花更多的时间,我就这么TLE了。
学习下插入十字链表结点的方法:
void ins_node(int cnt , int c)//表示把第cnt个结点插入到第c列
{
u[d[c]]=cnt;
d[cnt]=d[c];
u[cnt]=c;
d[c]=cnt;
s[c]++;
ncol[cnt]=c;
}
二) 重复覆盖模型
问题描述:选定最少的行,使得每列至少有一个1(即不一定为一个)
实现思路:将当前列去掉,并将选作当前列的解的行能够覆盖到的列全部去掉,因为不要求每列仅有一个1,故不必要把能够覆盖某一列的所有行全部去掉。remove和resume和精确覆盖有所不同。需要注意。
此外,由于删去的少了,所以矩阵密度的下降也变慢了,因此要加个A*剪枝来提高效率。就是利用A*搜索中的估价函数,即对于当前的递归深度K下的矩阵,估计其最好情况下即最少还需要多少步才能出解。即如果将能够覆盖当前列的所有行全部选中,去掉这些行能够覆盖到的列,将这个操作作为一层深度,重复次操作直到所有列全部出解的深度是多少。如果当前深度加上最佳步数已经不可能由于当前最优解,则直接返回不必再算这种情况。感觉和分支限界很像,其实以前也不自觉地经常用。
模板实现:
void remove(int c)
{
for(int i=d[c]; i!=c; i=d[i])
{
r[l[i]]=r[i];
l[r[i]]=l[i];
}
}
void resume(int c)
{
for(int i=d[c]; i!=c; i=d[i])
r[l[i]]=l[r[i]]=i;
}
int h()//估价函数
{
bool has[55];
memset(has, false, sizeof(has));
int ans=0;
for(int i=r[head]; i!=head; i=r[i])
if(!has[i])
{
ans++;
for(int j=d[i]; j!=i; j=d[j])
for(int k=r[j]; k!=j; k=r[k])
has[c[k]]=true;
}
return ans;
}
void dfs(int k)
{
if(k+h()>=res)return;//A* cut
if(r[head]==head)
{
if(k<res)res=k;
return;
}
int ms=INF, cur=0;
for(int t=r[head]; t!=head; t=r[t])
if(s[t]<ms)
{
ms=s[t];
cur=t;
}
for(int i=d[cur]; i!=cur; i=d[i])
{
remove(i);
for(int j=r[i]; j!=i; j=r[j])
{
remove(j);
s[c[j]]--;
}
dfs(k+1);
for(int j=l[i]; j!=i; j=l[j])
{
resume(j);
s[c[j]]++;
}
resume(i);
}
}
构图和建十字链表还是和精确覆盖一样。依旧
makegragh();
initial();
dfs(0);
三步实现。
解决题目:
Fzj1686 神龙的难题
这题一开始没看懂题意,纠结了一会。看了sample才知道所谓每次攻击a行b列实际即每次能攻击一个a*b的小矩阵。这么久好了,直接枚举小矩阵,然后以小矩阵个数为行,怪兽个数为列,建图+模板就可。
Hdu3529 Bomberman-Just Search!
虽然说了just search,但是用DLX显然会更优化,而且也很好建图。空地为行,普通墙为列,搜一遍建图即可。还是蛮裸的。
Hdu3498 Whosyourdady
没玩过这个游戏,但是个人感觉这名字真别扭。。。
也是很裸的题,只是居然无语把行列写倒了导致无语的错误。
Hdu2295 Radar
很经典的题吧,二分加重复覆盖DLX。但是还是好想的。需要一点点别的处理。
三,TIPS:
其实DLX蛮好想的,只有说到全部消灭全部覆盖什么“全部”的都可以往上面想想。注意区分精确覆盖和重复覆盖,觉得也是很好区分的哈。
建图一般也好,只要牢记要求每列有一个1就大致知道该把什么安排为列了,对于只用一次什么的限制条件也可以转化为列限制。
需要注意的是数组的大小需要好好考虑,开小了什么问题都可能出现,比如TLE什么的。开大了又MLE,所以要估算好,比赛时别太多罚时。
初始化必须做好,多组数据时就不是默认都是0了,因为会有之前数据的遗留问题!
个人很喜欢DLX,觉得就是建模+模板=AC!