欢迎访问——该文出处-博客园-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. python old six day

    今天主要内容: . is 和== 的区别 . 编程的问题 一.       is和==的区别! is  比较的是内存地址 ==  比较的是值 记住结果就好 ⑴id 通过id() 我们查看到一个变量表示 ...

  2. 【原创】大数据基础之Kerberos(2)hive impala hdfs访问

    1 hive # kadmin.local -q 'ktadd -k /tmp/hive3.keytab -norandkey hive/server03@TEST.COM'# kinit -kt / ...

  3. python创建udp服务端和客户端

    1.udp服务端server from socket import * from time import ctime HOST = '' PORT = 8888 BUFSIZ = 1024 ADDR ...

  4. scp命令:远程复制粘贴文件

    文章链接:https://www.cnblogs.com/webnote/p/5877920.html scp是secure copy的简写,用于在Linux下进行远程拷贝文件的命令,和它类似的命令有 ...

  5. Java的家庭记账本程序(D)

    日期:2019.2.8 博客期:031 星期一 今天是把程序的查询功能以列表的形式完成了! 截图如下:

  6. js之DOM对象一

    一.什么是HTML  DOM HTML  Document Object Model(文档对象模型) HTML DOM 定义了访问和操作HTML文档的标准方法 HTML DOM 把 HTML 文档呈现 ...

  7. Allegro PCB Design GXL (legacy) 使用slide推挤走线,走线的宽度就发生改变的原因

    Allegro PCB Design GXL (legacy) version 16.6-2015 使用slide推挤走线,走线的宽度就会发生改变. 后来发现是因为约束管理器(Constraint M ...

  8. JAVA中的Token

    JAVA中的Token 基于Token的身份验证 来源:转载 最近在做项目开始,涉及到服务器与安卓之间的接口开发,在此开发过程中发现了安卓与一般浏览器不同,安卓在每次发送请求的时候并不会带上上一次请求 ...

  9. 如何使用VisualSVN Server建立版本库

    首先打开VisualSVN Server Manager,如图: 可以在窗口的右边看到版本库的一些信息,比如状态,日志,用户认证,版本库等.要建立版本库,需要右键单击左边窗口的Repositores, ...

  10. 论文阅读笔记十一:Rethinking Atrous Convolution for Semantic Image Segmentation(DeepLabv3)(CVPR2017)

    论文链接:https://blog.csdn.net/qq_34889607/article/details/8053642 摘要 该文重新窥探空洞卷积的神秘,在语义分割领域,空洞卷积是调整卷积核感受 ...