舞蹈链 DLX
欢迎访问——该文出处-博客园-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的更多相关文章
- [学习笔记] 舞蹈链(DLX)入门
"在一个全集\(X\)中若干子集的集合为\(S\),精确覆盖(\(\boldsymbol{Exact~Cover}\))是指,\(S\)的子集\(S*\),满足\(X\)中的每一个元素在\( ...
- luogu P4929 【模板】舞蹈链 DLX
LINK:舞蹈链 具体复杂度我也不知道 但是 搜索速度极快. 原因大概是因为 每次检索的时间少 有一定的剪枝. 花了2h大概了解了这个东西 吐槽一下题解根本看不懂 只能理解大概的想法 核心的链表不太懂 ...
- P4929-[模板]舞蹈链(DLX)
正题 题目链接:https://www.luogu.com.cn/problem/P4929 题目大意 \(n*m\)的矩形有\(0/1\),要求选出若干行使得每一列有且仅有一个\(1\). 解题思路 ...
- Vijos1755 靶形数独 Sudoku NOIP2009 提高组 T4 舞蹈链 DLX
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目(传送门) 题意概括 给出一个残缺的数独,求这个数独中所有的解法中的最大价值. 一个数独解法的价值之和为每个位置所填的数值 ...
- POJ3076 Sudoku 舞蹈链 DLX
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目(传送门) 题意概括 给出一个残缺的16*16数独,求解. 题解 DLX + 矩阵构建 (两个传送门) 学完这个之后,再 ...
- POJ3074 Sudoku 舞蹈链 DLX
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目(传送门) 题意概括 给出一个残缺的数独,求解. 题解 DLX + 矩阵构建 (两个传送门) 代码 #include & ...
- POJ2676 Sudoku 舞蹈链 DLX
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目(传送门) 题意概括 给出一个残缺的数独,求解.SPJ 题解 DLX + 矩阵构建 (两个传送门) 代码 #includ ...
- 关于用舞蹈链DLX算法求解数独的解析
欢迎访问——该文出处-博客园-zhouzhendong 去博客园看该文章--传送门 描述 在做DLX算法题中,经常会做到数独类型的题目,那么,如何求解数独类型的题目?其实,学了数独的构建方法,那么DL ...
- POJ3740 Easy Finding 舞蹈链 DLX
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目 精确覆盖问题模板题 算法 DLX算法 学习DLX算法--传送门 代码 #include <cstring> ...
随机推荐
- mysql连表分组报错---- sql_mode=only_full_group_by问题解决
#### sql语句报错问题 #1055 - Expression #3 of SELECT list is not in GROUP BY clause and contains nonaggreg ...
- Eclipse中避免修改后台代码后手动install和重启
之前每次修改maven多模块项目后都得重新执行mvn install ,install完还得重启jetty/tomcat服务器,非常浪费 时间,其实修改代码后可以不用执行install,也不用 ...
- Java数据结构与算法解析(十二)——散列表
散列表概述 散列表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值. 散列表的思路很简单,如果所有的键都是整数,那么就可以使用一个简单 ...
- http请求在ie中F12查看显示已挂起
页面有解析和运算工作之后 http新请求在ie中F12查看显示已挂起,http post ,请求返回少量数据
- 1)django-建立步骤和目录说明
一:前言 django是python最流行的WEB框架. 二:django安装 pip install django 三:django项目建立步骤 1.创建django工程 django-admin ...
- liunx tomcat 运行模式apr
tomcat运行模式一共三种:bio.nio.apr 网上对这三种运行模式的解释外: bio运行模式:阻塞式I/O操作,表示Tomcat使用的是传统的Java I/O操作(即java.io包及其子包) ...
- 在启用属性的情况下启动 Confluence 6
在一些情况下,你可以希望 Confluence 在系统启动的时候就对属性文件进行打印.如果你的 Confluence 经常进行重启,并且你可能忘记来启动针对系统诊断的属性文件日志开关. 编辑 CONF ...
- 网络编程—tcp
一.TCP简介 TCP介绍 TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)是一种面向连接的.可靠的.基于字节流的传输层通信协议,由IETF ...
- java概念基础笔记整理
1.构造方法没有类型,有类型的不是不叫构造方法. 2.一个类的的成员变量可以是java允许的任何数据类型,一个类可以把某个对象作为自己的一个成员变量,如果用这样的类创建对象,那么该对象中就会其他对象, ...
- css中border画三角形
<!doctype html><html lang="en"> <head> <meta charset="UTF-8&quo ...