欢迎访问——该文出处-博客园-zhouzhendong

去博客园看该文章--传送门

舞蹈链是一个非常玄学的东西……

问题模型

精确覆盖问题:在一个01矩阵中,是否可以选出一些行的集合,使得在这些行的集合中,每列有且仅有1个1。

例子

1 1 0 1 0 1 0 0 0

1 0 1 0 1 0 1 1 0

0 1 1 0 1 1 1 1 1

1 0 0 0 1 0 0 0 0

0 1 0 1 0 0 0 0 0

0 0 0 0 0 1 0 0 1

那么答案就是

重复覆盖问题:精确覆盖问题的变形,允许列中的1多于1个。

具体算法见后面。

X算法

解决这个精确覆盖问题,我们首先有一个X算法。

X算法基于dfs,具体是这样的:每次,对于当前剩余矩阵(一开始就是原矩阵),然后选择一列,这一列为当前需要匹配的列(随便选,应每一列都要匹配),然后对于当前列,选择一个剩余矩阵中存留的行,使得该行的这一列为1,使得该列得以覆盖,然后删除会有冲突的一些情况,具体为:

1、删除该行的其他地方的1所覆盖的列

2、删除该列暂未被选中的其他行

举个例子:

比如选中第6列第6行

那么删除相斥的行(蓝色行)和相关的列(红色的列)剩余矩阵为:

1 0 1 0 1 1 1

1 0 0 0 1 0 0

0 1 0 1 0 0 0

然后,比如说再删除剩余矩阵的第5列的第1行,那么:

剩余的矩阵为:

1 1

最后一步,删除所有的即可。这就找到了一组解。

当然会有寻解失败的情况,所以是回溯的算法。

舞蹈链 - DLX 算法 概述

上面的X算法好是好,但是空间问题太严重,建立剩余矩阵也太耗时,所以我们采用舞蹈链优化的X算法—— DLX算法 来解决问题!

DLX什么鬼??

DLX 算法基于 X 算法和十字链表,其原理就是把转换矩阵的过程通过链来实现。

先贴一个生动的图:

来自<万仓一黍-博客园>

注意这个图还是稍微有点难以描述的地方:DLX算法的链是循环的,图片难以做到这个效果,用问题描述就是在上图的基础上,每行的尾部再连向首部,每列也是如此。

那么,有了这个结构,在删除的时候,就只需要修改某些链即可。

比如上图,如果要删除第2列,先删掉,然后枚举选择哪一行,如果是第3行,那么只要把该行的除该列位置以外的元素以及这些元素的列都从链表中删除即可,对于删除列,改变的是横向的第一行的链表,对于删除单个元素,仅仅改变纵向的链表;也就是说横向的指针除了第一行,其他的行都会被改变。

这个时候,十字链表循环的原因就慢慢出来了。

当选择一列的时候,这一列不一定就是第一列,要删除该某行元素,起点不一定是第一行,循环链表解决的这个问题。

在实际实现中,为了优化DLX的速度,往往会维护每列的元素个数,每次选择列的时候选择元素个数最小的那一列,据说这样会快一点。

说到这里可能读者还是一头雾水,没关系,结合代码来看,慢慢体会即可。

至于DLX解决重复覆盖问题,我们只要在删除列的时候,不要删光涉及它的行就可以了。

实现

DLX算法大致懂了之后,看起来挺简单的,实际上如果不看标算,一开始写的也是一头雾水。

首先定义一下:

x[i]表示节点i的行号

y[i]表示节点i的列号

L[i]表示节点i的左指针指向的节点编号

R[i]表示节点i的右指针指向的节点编号

U[i]表示节点i的上指针指向的节点编号

D[i]表示节点i的下指针指向的节点编号

C[i]表示第i列的元素个数

ans[i]表示答案集合中的第i行的行号

ansd表示答案集合行数

n,m表示总行数和总列数

cnt表示元素总数

DLX模板,首先是初始化。

初始化的时候,我们要知道有多少列(比如说m),然后构建第一个虚点和m个列首元素。同时初始化DLX里面的已有元素个数,即m+1。注意是构建循环的链表。

    void init(int c){
memset(x,,sizeof x),memset(y,,sizeof y);
memset(L,,sizeof L),memset(R,,sizeof R);
memset(U,,sizeof U),memset(D,,sizeof D);
memset(C,,sizeof C),memset(ans,,sizeof ans);
anscnt=,m=c;
for (int i=;i<=m;i++)
L[i]=i-,R[i]=i+,U[i]=D[i]=i;
L[]=m,R[m]=,cnt=m;
}

然后是建立十字链表

然而我们会发现这个过程远远比我们想象中的复杂。不用想了,看下面的解说。

在建立十字链表的时候,我们一行一行来,对于同一行,一列一列来。对于同一行的L和R,我们发现每行的相邻两个元素(这里说的元素是只选1的)的L等于下一个元素,R等于上一个元素,当然存在特殊情况:每行的第一个元素的L和最后一个元素的R,这样的话,只要在一行开始前记录一下行首标记,然后处理完一行之后再对首尾元素特殊赋值即可。当然如果一行没有元素,不可进行这个特殊赋值的操作,会出错,L和R以及行的处理,这里就写到这里,具体代码可以到练习题里面任意的题目里面联系代码理解。

现在还剩下U和D的问题。D好对付,就是当前列的首元素(循环的嘛),那么U呢??难不成每列搞一个数组之类的东西……,然后……???你硬要这样我也不拦你……对于一个简洁的代码来说,这是冗余!!!

我们发现,在修改U[该列]之前,U[该元素]=U[该列]!!这是个好东西,一发修改了D[U[该列]]以及U[该元素]之后,在修改D[该元素]以及U[该列]也不迟。

那不就好了,具体见代码。别忘了给该列的计数器C[该列]+1。

    void link(int i,int j){
cnt++;
x[cnt]=i;
y[cnt]=j;
L[cnt]=cnt-;
R[cnt]=cnt+;
D[cnt]=j;
D[U[j]]=cnt;
U[cnt]=U[j];
U[j]=cnt;
C[j]++;
}

然后就是最最重要也是最最玄学的暴力部分了!!!

很简单,每次找到C最小的一列,然后删除这一列以及相应的行,然后枚举要删除这一列得先删除哪些行。然后枚举是取哪一行的使得该列被精确覆盖,对于这一行覆盖的其他列,也分别进行删除操作。然后继续在剩余矩阵中求解。直到没有列剩余为止。别忘了恢复链表。注意这个过程有个很奇怪的地方:请在删除操作的地方按照R走,在恢复的时候按照L走,不知道为什么这样比较快……我有同学因为这个写成同向的,导致NOIP2009靶形数独卡了很久……

    void Delete(int k){
L[R[k]]=L[k];
R[L[k]]=R[k];
for (int i=D[k];i!=k;i=D[i])
for (int j=R[i];j!=i;j=R[j]){
U[D[j]]=U[j];
D[U[j]]=D[j];
C[y[j]]--;
}
}
void Reset(int k){
L[R[k]]=k;
R[L[k]]=k;
for (int i=U[k];i!=k;i=U[i])
for (int j=L[i];j!=i;j=L[j]){
U[D[j]]=j;
D[U[j]]=j;
C[y[j]]++;
}
}
bool solve(){
if (R[]==)
return true;
anscnt++;
int k=R[];
for (int i=R[k];i!=;i=R[i])
if (C[i]<C[k])
k=i;
Delete(k);
for (int i=D[k];i!=k;i=D[i]){
ans[anscnt]=x[i];
for (int j=R[i];j!=i;j=R[j])
Delete(y[j]);
if (solve())
return true;
for (int j=L[i];j!=i;j=L[j])
Reset(y[j]);
}
Reset(k);
anscnt--;
return false;
}

解释完了,贴膜板

const int N=100+5,M=100+5,S=N*M;
struct DLX{
int n,m,cnt;
int x[S],y[S],L[S],R[S],U[S],D[S];
int C[M],anscnt,ans[N];
void init(int c){
memset(x,,sizeof x),memset(y,,sizeof y);
memset(L,,sizeof L),memset(R,,sizeof R);
memset(U,,sizeof U),memset(D,,sizeof D);
memset(C,,sizeof C),memset(ans,,sizeof ans);
anscnt=,m=c;
for (int i=;i<=m;i++)
L[i]=i-,R[i]=i+,U[i]=D[i]=i;
L[]=m,R[m]=,cnt=m;
}
void link(int i,int j){
cnt++;
x[cnt]=i;
y[cnt]=j;
L[cnt]=cnt-;
R[cnt]=cnt+;
D[cnt]=j;
D[U[j]]=cnt;
U[cnt]=U[j];
U[j]=cnt;
C[j]++;
}
void Delete(int k){
L[R[k]]=L[k];
R[L[k]]=R[k];
for (int i=D[k];i!=k;i=D[i])
for (int j=R[i];j!=i;j=R[j]){
U[D[j]]=U[j];
D[U[j]]=D[j];
C[y[j]]--;
}
}
void Reset(int k){
L[R[k]]=k;
R[L[k]]=k;
for (int i=U[k];i!=k;i=U[i])
for (int j=L[i];j!=i;j=L[j]){
U[D[j]]=j;
D[U[j]]=j;
C[y[j]]++;
}
}
bool solve(){
if (R[]==)
return true;
anscnt++;
int k=R[];
for (int i=R[k];i!=;i=R[i])
if (C[i]<C[k])
k=i;
Delete(k);
for (int i=D[k];i!=k;i=D[i]){
ans[anscnt]=x[i];
for (int j=R[i];j!=i;j=R[j])
Delete(y[j]);
if (solve())
return true;
for (int j=L[i];j!=i;j=L[j])
Reset(y[j]);
}
Reset(k);
anscnt--;
return false;
}
}dlx; 

应用举例

数独求解--传送门

练习题

模板题

POJ3740 传送门  题解

正常题

POJ2676 传送门  题解

POJ3074 传送门  题解

POJ3076 传送门  题解

NOIP2009T4 传送门  题解

骨灰题

BZOJ1501 [NOI2005]智慧珠游戏 传送门 题解

舞蹈链 DLX的更多相关文章

  1. [学习笔记] 舞蹈链(DLX)入门

    "在一个全集\(X\)中若干子集的集合为\(S\),精确覆盖(\(\boldsymbol{Exact~Cover}\))是指,\(S\)的子集\(S*\),满足\(X\)中的每一个元素在\( ...

  2. luogu P4929 【模板】舞蹈链 DLX

    LINK:舞蹈链 具体复杂度我也不知道 但是 搜索速度极快. 原因大概是因为 每次检索的时间少 有一定的剪枝. 花了2h大概了解了这个东西 吐槽一下题解根本看不懂 只能理解大概的想法 核心的链表不太懂 ...

  3. P4929-[模板]舞蹈链(DLX)

    正题 题目链接:https://www.luogu.com.cn/problem/P4929 题目大意 \(n*m\)的矩形有\(0/1\),要求选出若干行使得每一列有且仅有一个\(1\). 解题思路 ...

  4. Vijos1755 靶形数独 Sudoku NOIP2009 提高组 T4 舞蹈链 DLX

    欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目(传送门) 题意概括 给出一个残缺的数独,求这个数独中所有的解法中的最大价值. 一个数独解法的价值之和为每个位置所填的数值 ...

  5. POJ3076 Sudoku 舞蹈链 DLX

    欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目(传送门) 题意概括 给出一个残缺的16*16数独,求解. 题解 DLX + 矩阵构建  (两个传送门) 学完这个之后,再 ...

  6. POJ3074 Sudoku 舞蹈链 DLX

    欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目(传送门) 题意概括 给出一个残缺的数独,求解. 题解 DLX + 矩阵构建  (两个传送门) 代码 #include & ...

  7. POJ2676 Sudoku 舞蹈链 DLX

    欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目(传送门) 题意概括 给出一个残缺的数独,求解.SPJ 题解 DLX + 矩阵构建  (两个传送门) 代码 #includ ...

  8. 关于用舞蹈链DLX算法求解数独的解析

    欢迎访问——该文出处-博客园-zhouzhendong 去博客园看该文章--传送门 描述 在做DLX算法题中,经常会做到数独类型的题目,那么,如何求解数独类型的题目?其实,学了数独的构建方法,那么DL ...

  9. POJ3740 Easy Finding 舞蹈链 DLX

    欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目 精确覆盖问题模板题 算法 DLX算法 学习DLX算法--传送门 代码 #include <cstring> ...

随机推荐

  1. 二、消息队列之如何在C#中使用RabbitMQ

    1.什么是RabbitMQ.详见 http://www.rabbitmq.com/. 作用就是提高系统的并发性,将一些不需要及时响应客户端且占用较多资源的操作,放入队列,再由另外一个线程,去异步处理这 ...

  2. VBS计时器2

    打开计时器,如果点击暂停,会显示你刚才事物所用的时间(以分钟为单位) dim c //控制循环 c= vbyes while c<>vbno dim a a= 60*hour(now)+m ...

  3. js——class基础

    js的类?其实还是原型! class Point{ constructor(x, y){ this.x = x; this.y = y; } toString(){ return '(' + this ...

  4. oracle数据库内存调整之增加内存

    注:本文来源:小颜Kevin   <oracle数据库内存调整之增加内存> 模拟操作系统内存从2G增加为8G后,调整数据库内存参数,示例中参数不作为实际生产环境参考,因为因需所取,调整参数 ...

  5. Confluence 6 使用 Apache 和 mod_proxy 添加 SSL和其他

    添加 SSL 如果你计划在你的应用中启用 SSL ,请参考 Securing your Atlassian applications with Apache using SSL页面中的内容,并确定你在 ...

  6. Confluence 6 从关闭的连接中恢复

    当数据库服务器进行重启或者因为网络问题导致连接中断.所有在数据库连接池中的连接都会被中断.希望处理这个问题,通常需要 Confluence 进行重启. 但是,数据库连接池中的连接可以通过运行 SQL ...

  7. Swift 新增fileprivate 详解

    以前项目中只要用了private  那么在同一个文件同一个类中还是能访问的(比如一个类中写了一个extension) swift3.0现在不行了 新增了一个fileprivate 的访问控制 以前的p ...

  8. 《剑指offer》 二进制中1的个数

    本题来自<剑指offer> 二进制中1的个数 题目: 输入一个整数,输出该数二进制表示中1的个数.其中负数用补码表示. 思路: 两种思路: 第一种:对n进行左移,检测最后一位是否为1,但考 ...

  9. 部署描述符 web.xml

    google的部署描述符详解: https://cloud.google.com/appengine/docs/flexible/java/configuring-the-web-xml-deploy ...

  10. cf861D 字典树+时间戳

    好久没碰字典树之类的题了,搞起来有点生疏 /* 把所有母串的后缀加入字典树中 然后再扫一次所有母串的后缀,把后缀放到字典树中查询,找到第一个访问次数为1的结点返回即可 num在计数时,同一个母串的子串 ...