Dancing Links X 学习笔记
X 算法
用于解决精确覆盖问题,即给定一个零一矩阵,选出若干行让每一行恰有一个一。
这是一个矩阵。
先随便选择一行删掉,假设选择了第一行。那么第三、六、七列可以删掉,因为这些列上已经有数;同时包含第三、六、七列的行,也就是第三、六行也要删掉。那么矩阵就变成了这样:
用相同的方法求解这个子问题。
此时假如删掉第一行,会导致行被删完,但第二列没有被删,无解。所以当矩阵为空时,要求最后被删的行全为一。
Dancing Links X
X 算法中有大量删除行、列的操作。可以用循环链表维护矩阵中的一。
每个一都有四个指针指向上下左右的一。每一行,每一列都有一个头指针,特别地,列的头指针是虚点 \(0\sim m\)。
build
初始化列的头指针。
void build(int b){
cnt=b;
for(int i=0;i<=b;i++)l[i]=i?i-1:b,r[i]=i==b?0:i+1,u[i]=d[i]=i;
}
insert
插入时可以直接把这个点放在行和列的头指针后面,实际上链表的相对顺序是不重要的,只要能遍历所在行和列就行了。
void insert(int a,int b){
sz[b]++,cnt++,x[cnt]=a,y[cnt]=b,u[cnt]=b,d[cnt]=d[b],u[d[cnt]]=d[u[cnt]]=cnt;
if(!head[a])head[a]=l[cnt]=r[cnt]=cnt;
else l[cnt]=head[a],r[cnt]=r[head[a]],l[r[cnt]]=r[l[cnt]]=cnt;
}
remove 和 recover
表示删除和恢复一列及相关的行。其中还要维护每列一的个数,在之后会用到。
void remove(int a){
l[r[a]]=l[a],r[l[a]]=r[a];
for(int i=d[a];i!=a;i=d[i])for(int j=r[i];j!=i;j=r[j])u[d[j]]=u[j],d[u[j]]=d[j],sz[y[j]]--;
}
void recover(int a){
for(int i=u[a];i!=a;i=u[i])for(int j=l[i];j!=i;j=l[j])u[d[j]]=d[u[j]]=j,sz[y[j]]++;
l[r[a]]=a,r[l[a]]=a;
}
dance
这里找到一的个数最少的一列,枚举这一列相关的行删去。这样做可以搜索到所有情况(因为每次都会删一个一),而且搜索树最小。
bool dance(){
int pos=r[0];
if(!pos)return 1;
for(int i=r[0];i;i=r[i])if(sz[i]<sz[pos])pos=i;
remove(pos),num++;
for(int i=d[pos];i!=pos;i=d[i]){
ans[num]=x[i];
for(int j=r[i];j!=i;j=r[j])remove(y[j]);
if(dance())return 1;
for(int j=l[i];j!=i;j=l[j])recover(y[j]);
}
return recover(pos),num--,0;
}
重复覆盖问题
DLX 还可以解决重复覆盖问题。
首先可以将每个正方形看作列,每条边看做行。问题变成选择最少的行使每列有至少一个一。
那么删除列时就不用删相关行,因为一列可以有多个一。
另外,由于要求最少,可以用 IDA*。估价函数可以这么设计:枚举每一列,选取这一列的相关行,但是只花费一行的代价。
证明:选取了所有相关行后,如果枚举到的列没有被覆盖,那么这个列会作为一个覆盖的第一个一,此时这一行的代价是无法避免的。
#include<bits/stdc++.h>
using namespace std;
int t,n,m,lim,head[65],sz[3605],l[3605],r[3605],u[3605],d[3605],cnt,x[3605],y[3605],num,ans[65];
bool vis[65];
vector<int>e[65];
void build(int b){
cnt=b;
for(int i=0;i<=b;i++)l[i]=i?i-1:b,r[i]=i==b?0:i+1,u[i]=d[i]=i;
}
int h(){
int cnt=0;
memset(vis,0,sizeof(vis));
for(int i=r[0];i;i=r[i]){
if(vis[i])continue;
cnt++,vis[i]=1;
for(int j=d[i];j!=i;j=d[j])for(int k=r[j];k!=j;k=r[k])vis[y[k]]=1;
}
return cnt;
}
void insert(int a,int b){
sz[b]++,cnt++,x[cnt]=a,y[cnt]=b,u[cnt]=b,d[cnt]=d[b],u[d[cnt]]=d[u[cnt]]=cnt;
if(!head[a])head[a]=l[cnt]=r[cnt]=cnt;
else l[cnt]=head[a],r[cnt]=r[head[a]],l[r[cnt]]=r[l[cnt]]=cnt;
}
void remove(int a){
for(int i=d[a];i!=a;i=d[i])l[r[i]]=l[i],r[l[i]]=r[i];
}
void recover(int a){
for(int i=u[a];i!=a;i=u[i])l[r[i]]=r[l[i]]=i;
}
bool dance(int dep){
if(dep+h()>lim)return 0;
int pos=r[0];
if(!pos)return 1;
for(int i=r[0];i;i=r[i])if(sz[i]<sz[pos])pos=i;
for(int i=d[pos];i!=pos;i=d[i]){
ans[dep]=x[i],remove(i);
for(int j=r[i];j!=i;j=r[j])remove(j);
if(dance(dep+1))return 1;
for(int j=l[i];j!=i;j=l[j])recover(j);
recover(i);
}
return 0;
}
int main(){
cin>>t;
while(t--){
cin>>n>>m;
for(int i=1,x;i<=m;i++)cin>>x,vis[x]=1;
m=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=n-i+1;j++){
for(int k=1;k<=n-i+1;k++){
e[++m].clear();
for(int l=0;l<i;l++){
e[m].push_back((j-1)*(2*n+1)+k+l);
e[m].push_back((j-1)*(2*n+1)+k+l+(2*n+1)*i);
e[m].push_back((j-1)*(2*n+1)+k+n+l*(2*n+1));
e[m].push_back((j-1)*(2*n+1)+k+n+l*(2*n+1)+i);
}
for(int l=0;l<e[m].size();l++){
if(vis[e[m][l]]){
m--;
break;
}
}
}
}
}
build(m);
for(int i=1;i<=m;i++)for(int j=0;j<e[i].size();j++)insert(e[i][j],i);
for(lim=0;!dance(0);lim++);
cout<<lim<<'\n',cnt=num=0,memset(head,0,sizeof(head)),memset(size,0,sizeof(size)),memset(l,0,sizeof(l)),memset(r,0,sizeof(r)),memset(u,0,sizeof(u)),memset(d,0,sizeof(d));
}
return 0;
}
例题
转化一下,相当于每行、每列、每宫中有且仅有一个 \(1\sim 9\),且每格中有且仅有一个数字,共 \(324\) 列;每格有九种数字,共 \(729\) 行,\(3241\) 个一。对于已经有的数,就只保留符合初始状态的一行。
build(324);
for(int i=0;i<9;i++){
for(int j=0,t;j<9;j++){
cin>>t;
for(int k=1;k<=9;k++){
if(t&&t!=k)continue;
insert(i*9*9+j*9+k,i*9+j+1);
insert(i*9*9+j*9+k,81+i*9+k);
insert(i*9*9+j*9+k,81*2+j*9+k);
insert(i*9*9+j*9+k,81*3+(i/3*3+j/3)*9+k);
}
}
}
dance();
for(int i=1;i<=num;i++)a[(ans[i]-1)/81][(ans[i]-1)/9%9]=ans[i]%9?ans[i]%9:9;
for(int i=0;i<9;i++)for(int j=0;j<9;j++)cout<<a[i][j]<<(j==8?'\n':' ');
用 DLX 枚举本身就起到了剪枝的作用,可以做 P1074。
每格有且仅有一个珠子,每个零件恰好用了一次,共 \(67\) 列;每个零件有不同的位置,再加上旋转翻转,共 \(2730\) 行,\(15084\) 个一。
枚举零件时,可以打表出零件的珠子与其中一个珠子坐标的差,然后枚举这个增量的正负号,枚举是否交换横纵坐标的增量。
const int len[12]={3,4,4,4,5,5,5,5,5,5,5,5},p[12][5][2]={{{0,0},{1,0},{0,1}},{{0,0},{0,1},{0,2},{0,3}},{{0,0},{1,0},{0,1},{0,2}},{{0,0},{1,0},{0,1},{1,1}},{{0,0},{1,0},{2,0},{2,1},{2,2}},{{0,0},{0,1},{1,1},{0,2},{0,3}},{{0,0},{1,0},{0,1},{0,2},{1,2}},{{0,0},{1,0},{0,1},{1,1},{0,2}},{{0,0},{0,1},{0,2},{1,2},{1,3}},{{0,0},{-1,1},{0,1},{1,1},{0,2}},{{0,0},{1,0},{1,1},{2,1},{2,2}},{{0,0},{1,0},{0,1},{0,2},{0,3}},};
int main(){
build(67);
for(int i=1;i<=10;i++)cin>>s[i]+1;
for(int i=1;i<=10;i++)for(int j=1;j<=i;j++)if(s[i][j]!='.')vis[s[i][j]-'A']=1;
for(int t=0;t<12;t++){
for(int f=0;f<=1;f++){
for(int dx=-1;dx<=1;dx+=2){
for(int dy=-1;dy<=1;dy+=2){
for(int x=1;x<=10;x++){
for(int y=1;y<=x;y++){
for(int i=0;i<len[t];i++){
int nx=x+dx*p[t][i][f],ny=y+dy*p[t][i][!f];
if(nx<1||nx>10||ny<1||ny>nx||(vis[t]&&s[nx][ny]!=t+'A'))goto tag;
}
a[++n]=(node){t,f,dx,dy,x,y};
for(int i=0;i<len[t];i++){
int nx=x+dx*p[t][i][f],ny=y+dy*p[t][i][!f];
insert(n,nx*(nx-1)/2+ny);
}
insert(n,56+t);
tag:;
}
}
}
}
}
}
if(dance()){
for(int i=1;i<=num;i++){
int t=a[ans[i]].t,f=a[ans[i]].f,dx=a[ans[i]].dx,dy=a[ans[i]].dy,x=a[ans[i]].x,y=a[ans[i]].y;
for(int i=0;i<len[t];i++){
int nx=x+dx*p[t][i][f],ny=y+dy*p[t][i][!f];
temp[nx][ny]=(char)(t+'A');
}
}
for(int i=1;i<=10;i++,putchar('\n'))for(int j=1;j<=i;j++)cout<<temp[i][j];
}
else puts("No solution");
}
[[搜索]]