欢迎访问——该文出处-博客园-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]控制外部变量访问权限的extern和static关键字

    一.extern 概述 编译器是由上至下编译源文件的,当遇到一些函数引用外部全局变量,而这个变量被定义在该函数声明主体的下方,又或者引用自其它的编译单元,这个情况就需要extern来向编译器表明此变量 ...

  2. bootstrap Autocomplete

    首先应用文件(已上传到文件,需要可自行下载) <link href="~/bower_components/select2/dist/css/select2.css" rel ...

  3. ProtocolError: <ProtocolError for 127.0.0.1/RPC2: 401 Unauthor.

    安装cloudera-manager-agent报错 查看/var/log/cloudera-scm-agent.log 报错 Traceback (most recent call last): F ...

  4. jmeter测试mysql遇到的问题

    1. 1.防火墙未关 错误: Communications link failure The last packet sent successfully to the server was 0 mil ...

  5. 兼容性很好的纯css圆角

    <!DOCTYPE HTML> <html lang="en-US"> <head> <meta charset="UTF-8& ...

  6. MySQL----数据库练习

    一.多对多的正反向查询 class Class(models.Model): name = models.CharField(max_length=32,verbose_name="班级名& ...

  7. 三维拓扑排序好题hdu3231

    /* 三维拓扑排序 将每个长方体分解成六个面,xyz三维进行操作 每一维上的的所有长方体的面都应该服从拓扑关系,即能够完成拓扑排序=如果两个长方体的关系时相交,那么其对应的三对面只要交叉即可 如 a1 ...

  8. springboot+mybatis+springMVC基础框架搭建

    项目结构概览 pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http: ...

  9. python列表1

    List (列表)List(列表) 是 Python 中使用最 频繁的数据类 型.列表 可以 完成大 多数集 合类 的数据 结构 实现. 列表中 元素 的类型 可以 不相同 ,它支 持数 字,字 符串 ...

  10. 关于前端滚动条,input框等样式的修改

    1.改变滚动条的样式 .orderList::-webkit-scrollbar {/*滚动条整体样式*/ width: 4px; /*高宽分别对应横竖滚动条的尺寸*/ height: 4px;}.o ...