好困难啊,很久以前就想看,没看的一个,比以前看到感觉好一点,但是还是决定以后再看。。。
模板代码表示还无法完全理解(引用一个大神的博客)
浅入 dancing links x(舞蹈链算法)
abastract:利用dancing links 解决精确覆盖问题,例如数独,n皇后问题;以及重复覆盖问题。
要学习dacning links 算法,首先要先了解该算法适用的问题,精确覆盖问题和重复覆盖问题等,下面先了解精确覆盖问题和重复覆盖问题。
精确覆盖问题
何为精确覆盖问题
在一个全集X中若干子集的集合为S,精确覆盖(Exactcover)是指,S的子集S*,满足X中的每一个元素在S*中恰好出现一次。
定义
S*中任意两个集合没有交集,即X中的元素在S*中出现最多一次
S*中集合的全集为X,即X中的元素在S*中出现最少一次
合二为一,即X中的元素在S*中出现恰好一次。
N={}
O={1,3}
E={2,4}
P={2,3}.
其中一个子集{O,E}是X的一个精确覆盖,因为O={1,3}而E={2,4}的并集恰好是X={1,2,3,4}。同理,{N,O,E}也是X.的一个精确覆盖。空集并不影响结论。
精确覆盖问题的表示方式
一般的,我们用一个集合s包含s中的元素的单向关系表示精确覆盖问题。常用的有以下两种方法:
- 矩阵表示法
包含关系可以用一个关系矩阵表示。.矩阵每行表示S的一个子集,每列表示X中的一个元素。矩阵行列交点元素为1表示对应的元素在对应的集合中,不在则为0。
通过这种矩阵表示法,求一个精确覆盖转化为求矩阵的若干个行的集合,使每列有且仅有一个1。同时,该问题也是精确覆盖的典型例题之一。
下表为其中一个例子:
S*={B,D,F}便是一个精确覆盖。
- 图论表示法
可将精确覆盖问题转化为一个二分图,左侧为集合,右侧为元素,左侧集合若与右侧元素有包含关系则连边,通过将左侧节点与其所有边保留与否求解一个右侧的每一个节点恰好有一条边的匹配。
重复覆盖问题
即选取一个01矩阵中的几行,使这几行组成的新矩阵的每一列至少有一个1。 该问题在精确覆盖问题上减少了一个约束条件。
接下来就是dancing links x算法了。
Dancing Links X 算法
历史
X算法是高德纳提出的解决精确覆盖问题的算法,而dancing links X算法则是DonKnuth(《计算机程序设计艺术》的作者)提出的对X算法的一种高效实现,这种实现建立在如上所说的矩阵表示法上。
算法思想
由如上精确覆盖问题的矩阵表示法中,我们知道dancing links x 是用来求解一个 01矩阵中选取哪几行可以使得这几行每一列都有且仅有一个1(就是每个元素在这几个子集里有且仅有出现过一次)。
先不管他的实际意义,我们需要做的就是在一个01矩阵的选取某几行使之符合上述条件。
我们很容易就想到枚举,然后判断符不符合条件,但是这个做法实在是太消耗时间。
dacing links x就是一个高效的求解该类问题的算法,而这种算法,基于交叉十字循环双向链的数据结构。
例如:如下的矩阵
就包含了这样一个集合(第1、4、5行)
如何利用给定的矩阵求出相应的行的集合呢?我们采用回溯法
先假定选择第1行,如下所示:
如上图中所示,红色的那行是选中的一行,这一行中有3个1,分别是第3、5、6列。
由于这3列已经包含了1,故,把这三列往下标示,图中的蓝色部分。蓝色部分包含3个1,分别在2行中,把这2行用紫色标示出来
根据定义,同一列的1只能有1个,故紫色的两行,和红色的一行的1相冲突。
那么在接下来的求解中,红色的部分、蓝色的部分、紫色的部分都不能用了,把这些部分都删除,得到一个新的矩阵
行分别对应矩阵1中的第2、4、5行
列分别对应矩阵1中的第1、2、4、7列
于是问题就转换为一个规模小点的精确覆盖问题
在新的矩阵中再选择第1行,如下图所示
还是按照之前的步骤,进行标示。红色、蓝色和紫色的部分又全都删除,导致新的空矩阵产生,而红色的一行中有0(有0就说明这一列没有1覆盖)。说明,第1行选择是错误的
那么回到之前,选择第2行,如下图所示
按照之前的步骤,进行标示。把红色、蓝色、紫色部分删除后,得到新的矩阵
行对应矩阵2中的第3行,矩阵1中的第5行
列对应矩阵2中的第2、4列,矩阵1中的第2、7列
由于剩下的矩阵只有1行,且都是1,选择这一行,问题就解决
于是该问题的解就是矩阵1中第1行、矩阵2中的第2行、矩阵3中的第1行。也就是矩阵1中的第1、4、5行()
(例子引用自http://www.cnblogs.com/grenet/p/3145800.html)
而对于重复覆盖问题,在选定某一行之后只需删除该行含1的所在列,并不需要再删除所在列上含1的行。
除此之外,运用这个dancing links 的这个模板,还可以解决数独,n皇后问题。
浅谈数独解法
我们在做数独时一般会使用枚举法。在某个格子枚举当前情况下的所有可填入数字,而在枚举其中一个可填入数字后递归到下一层,也就是下一个格子,枚举上一个格子数字确定下来的九宫格在该格子的所有可填入数字,以此类推。直到九宫格完全填满时,这时候的解为该九宫格的一个可行解,可继续递归返回上一层寻找下一个可行解。这个做法效率不错,但是若直接枚举,写起来就又臭又长(好吧我承认其实DLX也挺长的),若九宫格过于稀疏,该做法的时间效率也会指数级上升。我们选择用DLX把每一个已填格子和未填格子的信息转化成行,接下来dance一遍找出符合条件的行,再转化回九宫格信息,就是一个可行解了。最多729行进行DLX(9*9*9,后面你会明白的),速度较直接枚举也快很多,除去DLX的结构体,其他代码长度也比直接枚举短很多(打模板比较快hhh)。
那么怎么把九宫格上的信息转化成行呢? 首先数独中可行的解需满足:每一行,每一列,每一个宫里面不能有相同的数字,且只能使用1-9这九个数字。
把它转化为符合精确覆盖问题的条件,即为:
- 每一个格只能填一个数字。
- 每个数字在每一行只能出现一次。
- 每个数字在每一列只能出现一次。
- 每个数字在每一宫只能出现一次。
对于第一个条件:
第一列表示填入(1,1)这一格。
第二列表示填入(1,2)这一格。
第三列表示填入(1,3)这一格。
……
第(m-1)*9+n列表示填入(m,n)这一格。(1<=m<=9,1<=n<=9)
……
第9*9-1列表示填入(9,8)这一格。
第9*9列表示填入(9,9)这一格。
设inf1=81;
以此类推……
第二个条件:
第inf+1列表示在第一行填入1。
第inf+2列表示在第一行填入2。
第inf+3列表示在第一行填入3。
……
第inf+(n-1)*9+m列表示在第n行填入m(1<=n<=9,1<=m<=9)。
……
第inf+9*9表示在第九行填入9。
设inf2=inf1+81;
对于第三个条件也是如此,inf3=inf2+81;
对于第四个条件,inf4=inf3+81;
至此,已经把所有的条件都转化成了列。
而对于一个有数字的格子(假设位于(m,n)数字为k,位于第p宫)只需把它转化为一行,该行于(m-1)*9+n,inf1+(m-1)*9+k,inf2+(n-1)*9+k,inf3+(p-1)*9+k这四列为1,其余为0。
对于一个没有数字的格子(假设位于(m',n')第p'宫),则需把他转化为九行,分别代表在这一格填入1-9各个数字,每一列的填列原理和有数字的格子一样。
在把所有的格子转化完毕后,先把所有确定的有数字的格子的行以及该行含1的列以及该列上含1的行从矩阵中删除,然后dance一遍,找出符合的行,再把行转化成每个格子的信息,填入九宫格中,就是符合的一个解了。
2016-08-27 17:07:53
举个栗子
poj3704点击打开链接
#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<algorithm>
using namespace std;
// 列:(行+列+块)*9种可能+9*9个格子
// 行: 9*9*9 表示第i行第j列填k
const int MAXN=(9+9+9)*9+9*9+9*9*9*9*9*4+10;
#define INF 0xFFFFFF
int size;
int head,sz;
int U[MAXN],D[MAXN],L[MAXN],R[MAXN];
int H[MAXN],ROW[MAXN],C[MAXN],S[MAXN],O[MAXN];
void remove(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])
{
U[D[j]]=U[j];
D[U[j]]=D[j];
--S[C[j]];
}
}
}
void resume(int 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;
}
bool dfs(int k)
{
if(R[head]==head)
{
sort(O,O+9*9);
int p=0;
for(int i=0;i<9;i++)
{
for(int j=0;j<9;j++)
{
int num=O[p++];
//cout<<num<<endl;
num=num-(i*9+j)*9;
printf("%d",num);
}
}
printf("\n");
return true;
}
int s=INF,c;
for (int t=R[head];t!=head;t=R[t])
{
if (S[t]<s)
{
s=S[t];
c=t;
}
}
remove(c);
for(int i=D[c];i!=c;i=D[i])
{
O[k]=ROW[i];
for(int j=R[i];j!=i;j=R[j])
remove(C[j]);
if(dfs(k+1))
return true;
for(int j=L[i];j!=i;j=L[j])
resume(C[j]);
}
resume(c);
return false;
}
void initDL(int n)
{
head=0;
for(int i=0;i<=n;i++)
{
U[i]=i;D[i]=i;
L[i]=i-1;R[i]=i+1;
S[i]=0;
}
R[n]=0;L[0]=n;S[0]=INF+1;
sz=n+1;
memset(H,0,sizeof(H));
}
void insert(int i, int j)
{
if(H[i])
{
L[sz]=L[H[i]];
R[sz]=H[i];
L[R[sz]]=sz;
R[L[sz]]=sz;
}
else
{
L[sz]=sz;
R[sz]=sz;
H[i]=sz;
}
U[sz]=U[j];
D[sz]=j;
U[D[sz]]=sz;
D[U[sz]]=sz;
C[sz]=j;
ROW[sz]=i;
++S[j];
++sz;
}
char str[200];
void build()
{
int p=0;
initDL(9*9*4);
for(int i=0;i<9;i++)
for(int j=1;j<=9;j++,p++)
{
int base=(i*9+j-1)*9;
if(str[p]=='.')
{
for(int k=1;k<=9;k++)
{
int r;
r=base+k;
//第i行有数字k
insert(r,i*9+k);
//第j列有数字k
insert(r,9*9+(j-1)*9+k);
//第k块有数字k
int block=(j-1)/3*3+i/3;
insert(r,9*9*2+block*9+k);
//第i行j列有一个数字(限制一个格子只填一个数)
insert(r,9*9*3+i*9+j);
}
}
else
{
int k=str[p]-'0';
int r=base+k;
//第i行有数字k
insert(r,i*9+k);
//第j列有数字k
insert(r,9*9+(j-1)*9+k);
//第k块有数字k
int block=(j-1)/3*3+i/3;
insert(r,9*9*2+block*9+k);
//第i行j列有一个数字(限制一个格子只填一个数)
insert(r,9*9*3+i*9+j);
}
}
}
int main()
{
size=9; //9*9数独
while(~scanf("%s",str))
{
if(strcmp(str,"end")==0)
break;
build();
dfs(0);
}
return 0;
}
n皇后解法
类似于数独问题,在n皇后问题中,不冲突的n皇后的棋盘上符合以下五个个条件:
- 每一个格子最多只能有一个皇后。
- 每一行最多只能有一个皇后。
- 每一列最多只能有一个皇后。
- 每一个从左上往右下的斜边最多只能有一个皇后。
- 每一个从右上往左下的斜边最多只能有一个皇后。
这五个条件看起来不太好用dlx的做法去做,但如果把整个棋盘中有皇后的位置用1表示,没有皇后的位置用1表示,则可以转化为:
- 每个格子只能填一个数字。
- 每一行有且仅能填一个1。
- 每一列有且仅能填一个1。
- 每一个从左上往右下的斜边有且仅能填一个1。
- 每一个从右上往左下的斜边有且仅能填一个1。
按照数独的过程进行DLX,找出填1的行数大于等于n的解。等于n的解即为n皇后问题的解,大于n的解删去几行使填1的行数为n即为n皇后问题的解。
重复覆盖问题
顾名思义,即为在01矩阵中,选出几行,使得在这几行组成的新矩阵中,每一列都有1。
下面是hdu上一个重复覆盖的二分问题,并且给出重复覆盖问题的代码部分:
举个栗子Example2:hdu 2295
Radar
Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 2280 Accepted Submission(s): 897
Each of the last M lines consists of the coordinate of a radar station.
All coordinates are separated by one space.
Technical Specification
1. 1 ≤ T ≤ 20
2. 1 ≤ N, M ≤ 50
3. 1 ≤ K ≤ M
4. 0 ≤ X, Y ≤ 1000
该题用二分查找radar的半径,然后用重复覆盖的DLX进行最多选取k行该半径是否完全覆盖所有城市的判断(f()函数和dep一起判断),最后精确到小数点后六位。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<cmath>
#define clr(x) memset(x,0,sizeof(x))
#define clrlow(x) memset(x,-1,sizeof(x))
#define maxnode 3000
#define maxn 60
using namespace std;
struct DLX//dancing links
{
int U[maxnode],D[maxnode],L[maxnode],R[maxnode],col[maxnode],row[maxnode];//元素上下左右对应列对应行的指针
int S[maxn],H[maxn],V[maxn];//S为每列元素个数,H指向每行末尾的元素,V是dep()函数的标记数组。
int n,m,size,all;//all为解的行数的最大值
void init(int _n,int _m,int _all)
{
n=_n;
m=_m;
all=_all;
for(int i=0;i<=m;i++)
{
L[i]=i-1;
R[i]=i+1;
U[i]=i;
D[i]=i;
row[i]=0;
col[i]=i;
}
clr(S);
clrlow(H);
L[0]=m;
R[m]=0;
size=m;
return ;
}
//初始化
void push(int r,int c)
{
D[++size]=D[c];
col[size]=U[size]=c;
U[D[c]]=size;
D[c]=size;
row[size]=r;
S[c]++;
if(H[r]<0)
{
H[r]=L[size]=R[size]=size;
}
else
{
L[size]=H[r];
R[size]=R[H[r]];
L[R[H[r]]]=size;
R[H[r]]=size;
}
return ;
}
//加入元素
void del(int c)
{
S[col[c]]--;
for(int i=D[c];i!=c;i=D[i])
{
R[L[i]]=R[i];
L[R[i]]=L[i];
S[col[i]]--;
}
return ;
}
//删除一列
void reback(int c)
{
for(int i=U[c];i!=c;i=U[i])
{
S[col[R[L[i]]=L[R[i]]=i]]++;
}
S[col[c]]++;
return ;
}
//恢复一列
int dep( )
{
clr(V);
int deep=0;
for(int i=R[0];i!=0;i=R[i])
if(!V[i])
{
deep++;
for(int j=D[i];j!=i;j=D[j])
for(int k=R[j];k!=j;k=R[k])
V[col[k]]=1;
}
return deep;
}
//之后到达的最大深度
//d为当前深度
bool dancing(int d)
{
if(d+dep()>all) return false;
int c=R[0];
if(c==0)
{
return d<=all;
}
for(int i=R[0];i!=0;i=R[i])
if(S[i]<S[c])
c=i;
for(int i=D[c];i!=c;i=D[i])
{
del(i);
for(int j=R[i];j!=i;j=R[j])
del(j);
if(dancing(d+1)) return true;
for(int j=L[i];j!=i;j=L[j])
reback(j);
reback(i);
}
return false;
}
//dancing
}dlx;
struct point
{
int x,y;
}station[maxn],city[maxn];
double dis(point a,point b)
{
return (a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y);
}
int main()
{
int n,m,k,kase;
double lt,rt,mid;
double eps=1e-8;
scanf("%d",&kase);
while(kase--)
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++)
scanf("%d%d",&city[i].x,&city[i].y);
for(int i=1;i<=m;i++)
scanf("%d%d",&station[i].x,&station[i].y);
lt=0;
rt=1e8;
while(rt-lt>=eps)
{
dlx.init(m,n,k);
mid=(rt+lt)/2;
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
if(dis(city[j],station[i])<mid*mid-eps)
dlx.push(i,j);
if(dlx.dancing(0))
rt=mid-eps;
else
lt=mid+eps;
}
printf("%.6lf\n",lt);
}
return 0;
}
蓝书 算法训练p406