首页 > 其他分享 >舞蹈链学习笔记

舞蹈链学习笔记

时间:2023-09-27 18:11:30浏览次数:39  
标签:blue 学习 cnt color 矩阵 笔记 舞蹈 int red

Dancing Links X

1. 问题引入

给定一个 \(N\) 行 \(M\) 列的 \(01\) 矩阵 \(A\)(\(N,M\leq 500\)),选出若干行 \((T_1,T_2,\cdots,T_k)\),使得 \(\forall j\in [1,m],\sum\limits_{i=1}^k A_{T_i,j}=1\)。

即对于矩阵的每一列 \(j\),在你挑选的这些行中,有且仅有一行的第 \(j\) 个元素为 \(1\)。

暴力的话就是枚举每行是否选择,然后检查是否合法,总时间复杂度 \(O(nm\cdot2^n)\),如果将每一行看做一个 \(m\) 位二进制数,也只可以优化到 \(O(n\cdot2^n)\)。所以需要一种巧妙的算法来解决这个问题。

2. X 算法

2.1 X 算法流程

X 算法的算法流程为:

1.枚举选择当前矩阵的每一行。

2.删除当前选择的行,删除当前选择的行中为 \(1\) 的列,删除被删除的列中含 \(1\) 的行,得到一个新矩阵。

3.当矩阵不为空,继续递归处理;否则若最后删除的行为全 \(1\) 行,则找到一种解;否则回溯。

假设这个矩阵为 \(A\)。

\(A=\begin{pmatrix} 0&0&1&0&1&1&0\\ 1&0&0&1&0&0&1\\ 0&1&1&0&0&1&0\\ 1&0&0&1&0&0&0\\ 0&1&0&0&0&0&1\\ 0&0&0&1&1&0&1\\ \end{pmatrix}\)

先选择第一行(红色的为被选择的行,蓝色为其他将被删除的元素)。

\(\begin{pmatrix} \color{red}0&\color{red}0&\color{red}1&\color{red}0&\color{red}1&\color{red}1&\color{red}0\\ 1&0&\color{blue}0&1&\color{blue}0&\color{blue}0&1\\ \color{blue}0&\color{blue}1&\color{blue}1&\color{blue}0&\color{blue}0&\color{blue}1&\color{blue}0\\ 1&0&\color{blue}0&1&\color{blue}0&\color{blue}0&0\\ 0&1&\color{blue}0&0&\color{blue}0&\color{blue}0&1\\ \color{blue}0&\color{blue}0&\color{blue}0&\color{blue}1&\color{blue}1&\color{blue}0&\color{blue}1\\ \end{pmatrix}\)

得到新矩阵,此时新矩阵的每一行分别对应原矩阵的第 \(2,4,5\) 行,每一列分别对应原矩阵的第 \(1,2,4,7\) 列。

\(\begin{pmatrix} 1&0&1&1\\ 1&0&1&0\\ 0&1&0&1\\ \end{pmatrix}\)

同样再选择第一行。

\(\begin{pmatrix} \color{red}1&\color{red}0&\color{red}1&\color{red}1\\ \color{blue}1&\color{blue}0&\color{blue}1&\color{blue}0\\ \color{blue}0&\color{blue}1&\color{blue}0&\color{blue}1\\ \end{pmatrix}\)

得到空矩阵 \(\begin{pmatrix} \end{pmatrix}\),上一次删除的行不为全 \(1\) 行,进行回溯,尝试选择第二行。

\(\begin{pmatrix} \color{blue}1&\color{blue}0&\color{blue}1&\color{blue}1\\ \color{red}1&\color{red}0&\color{red}1&\color{red}0\\ \color{blue}0&1&\color{blue}0&1\\ \end{pmatrix}\)

得到矩阵 \(\begin{pmatrix}1&1\end{pmatrix}\),显然将这一行选择后,将得到一组解,即选择原矩阵中的 \(1,4,5\) 行。

2.2 X 算法原理

每次删除时将当前选择的行删除无需解释,将当前选择的行中为 \(1\) 的列删除保证了每一列只会有一个 \(1\),同时再将被删除的列中含 \(1\) 的行删除是因为这些行无法再被选择。

如果得到了一个空矩阵有两种情况,一种是行被删完,一种是列被删完,只有后者才表示出现了一种解,所以只有最后选择的是全 \(1\) 行,才证明列被删完。

但是发现并不容易实现,所以我们使用 Dancing Links ,即双向十字链表来维护这些操作。

3.1 双向十字链表

双向十字链表顾名思义,即对于每一个元素都记录它的上下左右四个元素(\(l,r,u,d\)),同时记录它所在的行和列的编号(\(row,col\))。

此外我们还需要有一个行首提示和列提示,行首提示即 \(fir_i\) ,记录第 \(i\) 行的第一个元素;列提示是我们虚拟出的 \(m+1\) 个节点,\(1\sim m\) 分别对应第 \(1\sim m\) 列,\(0\) 号节点没有右节点则表示这个 Dancing Links 为空。

双向十字链表

3.2 DLX

DLX 的时间复杂度,仅与矩阵中 \(1\) 的个数有关,其理论复杂度大概在 \(O(c^n)\) 左右(\(n\) 表示 \(1\) 的个数),\(c\) 为某个很接近 \(1\) 的常数,所以时间复杂度十分玄学。因此在实现过程中,可能仅仅是循环的方向相反就会相差几倍的时间。所以在这里仅介绍主流写法。

3.2.1 remove 操作

\(remove (p)\) 指的是删除第 \(p\) 列及相关的行。

首先把 \(p\) 删掉,即把 \(l_p\) 的右节点设为 \(r_p\),把 \(r_p\) 的左节点设成 \(l_p\)。

随后遍历第 \(p\) 列的每个元素,把每个元素这一行中的每个元素都删去,即把 \(u_j\) 的下节点设为 \(d_j\), \(d_j\) 的上节点设为 \(u_j\),同时修改每一列的元素个数。

inline void remove(int p)
{
	l[r[p]]=l[p],r[l[p]]=r[p];//删除 p
	for(register int i=d[p];i!=p;i=d[i])//遍历这一列
		for(register int j=r[i];j!=i;j=r[j])//遍历当前行
			u[d[j]]=u[j],d[u[j]]=d[j],--siz[col[j]];//删除这一个元素
	return ;
}

3.2.2 recover 操作

\(recover (p)\) 指的是还原第 \(p\) 列及相关的行。

首先把 \(p\) 还原,即把 \(l_p\) 的右节点、\(r_p\) 的左节点设成 \(p\)。

随后遍历第 \(p\) 列的每个元素,把每个元素这一行中的每个元素都还原,即把 \(u_j\) 的下节点、\(d_j\) 的上节点设为 \(j\),同时修改每一列的元素个数。即所有操作顺序与 \(remove (p)\) 的操作顺序相反。

inline void recover(int p)
{
	for(register int i=u[p];i!=p;i=u[i])//遍历这一列
		for(register int j=l[i];j!=i;j=l[j])//遍历当前行
			u[d[j]]=d[u[j]]=j,++siz[col[j]];//还原这一个元素
	l[r[p]]=r[l[p]]=p;//还原 p
	return ;
}

3.2.3 build 操作

即初始化一个 Dancing Links,没什么特殊的。

inline void build(int n,int m)//n 行 m 列
{
	for(register int i=0;i<=m;++i)
		l[i]=i-1,r[i]=i+1,u[i]=d[i]=i;
	l[0]=m,r[m]=0,cnt=m;return ;
}

3.2.4 insert 操作

\(insert (i,j)\) 表示插入一个位于 \(i\) 行 \(j\) 列的元素。

将这个元素编号为 \(cnt\),将它插入到 \(fir_i\) 与 \(r_{fir_i}\),\(j\) 与 \(d_j\) 之间。

inline void insert(int i,int j)
{
	++cnt;
	row[cnt]=i,col[cnt]=j,siz[j]++;//记录行号,列号,修改这一列的元素个数
	u[cnt]=j,d[cnt]=d[j],u[d[j]]=cnt,d[j]=cnt;//插在 j 与 d[j] 之间
	if(!fir[i]) fir[i]=l[cnt]=r[cnt]=cnt;//如果此行仍为空,直接将 fir[i] 赋值为 cnt
	else l[cnt]=fir[i],r[cnt]=r[fir[i]],l[r[fir[i]]]=cnt,r[fir[i]]=cnt;//插在 fir[i] 与 r[fir[i]] 之间
	return ;
}

3.2.5 dance 操作

这便是我们实现 X 算法的步骤。

1.首先判断 \(r_0\) 是否为空,为空则表示找到一种可行解,直接结束。

2.选择元素最少的一列(优化,具有一定的启发性),将这一列删除,枚举这一列中每一行,这次选择为哪一行。

3.将选择的这一行中每一列删掉。

4.递归至下一层。回溯。

void dance(int dep)
{
	if(!r[0])//找到一种解
	{
		for(register int i=1;i<dep;++i) cout<<ans[i]<<' ';//输出答案
		exit(0);
	}
	int pos=r[0];
	for(register int i=r[0];i;i=r[i]) if(siz[i]<siz[pos]) pos=i;//找到元素最少的一列
	remove(pos);//将这一列删除
	for(register int i=d[pos];i!=pos;i=d[i])//枚举这一列中每一行
	{
		//选择第 i 行
		ans[dep]=row[i];
		for(register int j=r[i];j!=i;j=r[j]) remove(col[j]);//将这一行每一列删除
		dance(dep+1);//递归
		for(register int j=l[i];j!=i;j=l[j]) recover(col[j]);//还原
	}
	recover(pos);return ;//还原这一列
}

这样我们就解决了板题。

4. 舞蹈链的应用

4.1 精确覆盖问题

给定 \(n\) 个集合 \(S_i\) 以及一个集合 \(X\),求满足以下条件的无序多元组 \((T_1,T_2,\cdots,T_m)\):

1.\(\forall i,j\in [1,m],T_i\bigcap T_j=\varnothing (i \not= j)\)

2.\(\bigcup\limits_{i=1}^{m} T_i=X\)

3.\(\forall i\in [1,m],T_i\in\left\{S_1,S_2,\cdots,S_n\right\}\)

只需要将 \(S_i\) 中的元素离散化,然后构造一个矩阵使 \((i,{S_i}_j)\) 为 \(1\),就转化成了板题的形式。

这就是 DLX 最基础的建模。即行表示决策,对应一个集合,表示选/不选;列表示状态,第 \(i\) 列对应着某个条件 \(P_i\)。

4.2 数独问题

要求你完成一个 \(16\times 16\) 的数独。

我们考虑如果我们往 \((r,c)\) 中填入 \(x\),会对我们什么造成影响,第 \(r\) 行,第 \(c\) 列,第 \(b\) 宫不能再填入 \(x\),同时 \((r,c)\) 格不能再填入数字。

所以对于表示 \((r,c)\) 中填入 \(x\) 这一行,有 \(4\) 列上为 \(1\)。我们发现宫并不是决策的参数,因为它可以被确定的 \(r,c\) 表示出来;

所以数独问题其实为最典型的精确覆盖问题。

4.3 重复覆盖问题

给定一个 \(N\) 行 \(M\) 列的 \(01\) 矩阵 \(A\)(\(N,M\leq 500\)),选出若干行 \((T_1,T_2,\cdots,T_k)\),使得 \(\forall j\in [1,m],\sum\limits_{i=1}^k A_{T_i,j}\geq 1\)。

重复覆盖问题也就是说对于每一列我们可以选择若干个 \(1\),那么我们在处理的时候,只需要删除与当前选择行相关的所有列。

但是这样矩阵的下降速度会变慢,所以要加上一个强剪枝,即启发式搜索中的估价函数。一般可以判断当前状态最优情况是不是已经比全局最优解劣,也就是最优性剪枝。或者也可以使用 IDA*进行迭代加深。

inline void remove(int p)
{
	//这里删除的时候,不要把 p 也给删掉,因为接下来还要遍历 p 所在的行
	for(register int i=d[p];i!=p;i=d[i])
        l[r[i]]=l[i],r[l[i]]=r[i],--siz[col[i]];
	return ;
}
inline void recover(int p)
{
	for(register int i=u[p];i!=p;i=u[i])
        l[r[i]]=r[l[i]]=i,++siz[col[i]];
	return ;
}
inline int h()
{
    int ans=0;
    for(register int i=1;i<=tot;++i) st[i]=0;//初始化
    for(register int i=r[0];i;i=r[i])//遍历每一列
    {
        if(st[i]) continue;
        ++ans,st[i]=1;//如果还未被选择
        for(register int j=d[i];j!=i;j=d[j])//最优情况下,所有与之相关的列的所有相关的行都可以被选择过
            for(register int k=r[j];k!=j;k=r[k])
                st[col[k]]=1;
    }
    return ans;
}
void dance(int dep)
{
	if(!r[0]){ANS=min(ANS,dep-1);return ;}//取最优解
	int pos=r[0];if(dep+h()>ANS) return ;//最优性剪枝
	for(register int i=r[0];i;i=r[i]) if(siz[i]<siz[pos]) pos=i;//选出元素最少的列。
	for(register int i=d[pos];i!=pos;i=d[i])//枚举选择哪一行
	{
        remove(i);//删除当前列
		for(register int j=r[i];j!=i;j=r[j]) remove(j);//删除其他相关列
		dance(dep+1);//递归下一层
		for(register int j=l[i];j!=i;j=l[j]) recover(j);//还原其他相关列
        recover(i);//还原当前列
	}
	return ;
}

5.例题

P4205 [NOI2005] 智慧珠游戏

骨牌覆盖问题,类似于数独。我们先手动表示出每种智慧珠的相对坐标,对于不同的旋转和翻转的情况,我们只需要在枚举位置的同时,枚举坐标优先加到横坐标还是纵坐标,再枚举每次的正负号即可。

那么每一行表示将某种智慧珠放到某一种位置的决策;列则共有 \(55+12\) 列,前 \(55\) 列表示每一个位置都要有且仅有一个珠子,后 \(12\) 列表示每种智慧珠必须使用一次。

NQUEEN - Yet Another N-Queen Problem

精确覆盖问题,每一行的决策表示在一个位置放置一个皇后,状态为每一行,每一列,每一条对角线。共有 \(n+n+(2n-1)+(2n-1)\) 列。

但我们发现我们只需要将前 \(2n\) 列完全覆盖,并不需要每条对角线都将其覆盖,表示对角线的列只是起到约束作用。所以只需特殊处理一下,只要前 \(2n\) 列被覆盖完就表示找到了可行解。

P1074 [NOIP2009 提高组] 靶形数独

对数独答案的优劣性进行了定义,要求求出最优解。我们只需要在找到可行解后不结束程序,继续找到所有的解,取最优解即可。

破坏正方形 Square Destroyer

完全覆盖问题,矩阵中第 \(i\) 行表示拆去编号为 \(i\) 的边,每一列表示一个正方形。

Lamp

\(n\) 盏灯,\(m\) 个开关,一盏灯可以由多个开关控制,一个开关最多可控制两个灯。对于每个灯,给出一个控制它的开关列表。例如,对于灯1,列表为“1 ON 3 OFF 9 ON”,这意味着如果开关1处于“ON”状态或开关3处于“OFF”状态或开关9处于“ON”状态,则灯1将被点亮。

完全覆盖问题,因为每个开关有两种情况,将一个开关拆成两个,分别表示这个开关开或关。这样就是一个 \(2m\) 行 \(n\) 列的矩阵,但是每个开关只能有一个状况,所以我们额外记录一下,防止同时即开又关即可。

Airport

平面坐标系中 \(n\) 个点中选择 \(k\) 个,使每个点到这 \(k\) 个点中离它距离最近的点的距离最大值最小。

完全覆盖问题,二分这个最大距离,每次重新建图,矩阵中第 \(i,j\) 个元素,如果 \(dis(i,j)\) 小于等于这个距离就为 \(1\)。处理的时候进行可行性剪枝,如果当前步数加上估价函数大于 \(k\) 就返回。

还有一个优化是将每个点对的距离求出来,排序后通过这个二分。但如果这样需要算上自己与自己的距离或最后加上一个 \(0\),因为如果 \(k=n\) 答案为 \(0\)。

参考博客:

Dancing Links-OI Wiki

dancing links(舞蹈链)——求解精准覆盖及重复覆盖问题

Dancing Links X专题学习

「舞蹈链 DLX 」学习笔记

标签:blue,学习,cnt,color,矩阵,笔记,舞蹈,int,red
From: https://www.cnblogs.com/int-R/p/DLX.html

相关文章

  • 力扣刷题笔记-06 N字形变换
    06N字形变换不要混日子,小心日子把你混了对于题目的理解比如说,我给一个字符串,LEETCODE,行数为3,然后按照N字形排列,就是下面这个排列方式。排列完之后正常读取,结果就是LCETOEED。这叫做N字形变换。这个例子里给的行数就是3,往下排三行,然后往右往上走。chatGPT思路边界情况/特......
  • 改造提交学习记录接口
            ......
  • c的基本语法(课上笔记)
    "#"的意义预处理,在编译时进行内容代替scanf对于scanf("");引号中内容为必须输入的内容。当输入多个数据时,默认输入的数据间以空格或者回车分开。对于int,直接相除为向下取整14.0f格式即表示(float)14.0定义常量的方法#defineCSPo表示将CSP定义为o(CSP为宏,o为内容)const......
  • Spring-Boot-Starter 学习笔记(1)
    Spring-Boot-Starter1.准备配置类和Bean对象SpringBoot提供了两个注解:@Configuration:Spring提供的配置类注解,作用在类上,代表整个类是个Spring配置类,对照传统的SpringXML配置文件。@Bean:作用于方法上,代表此方法的返回值(对象)将会被Spring容器所管理,从而完成Bean......
  • Python学习笔记2
    defdouble(a):"""两倍处理三个引号可以多行注释,3个单引号也可以用来多行注释"""returna*2a=double(5)print(a)ifisinstance(a,int):#检测是否是某个类型print("a是整数")print(True+1)#True为1print(False+1)......
  • 软考笔记(1)--操作系统
     前言操作系统模块属于基本知识范畴,通常会在单选题中出现,约占2~5分左右。主要知识结构如下图示:  一、基本知识点操作系统是计算机系统中的核心系统软件,负责管理和控制计算机系统中硬件和软件资源,合理地组织计算机工作流程和有效利用资源,在计算机和用户之间起接口的作用。......
  • unet原理学习与记录
    UNET:     左边编码下采样,右边编码上采样。   改进版本认为原始版本融合特征跨度太远,改为就近融合下面有4个损失函数,如果前面三个效果就很好,第四个可以丢掉(剪枝) 数据增强包:albumentations 链接:https://github.com/albumentations-team/albumentations#i-......
  • 深度学习-梯度下降MiniBatch、RMSprop、Adam等
    目录 0、综述:SGD1、mini-batch2、指数平均加权3、理解指数加权平均4、指数加权平局的修正5、动量梯度下降法6、RMSprop7、Adam优化算法8、衰减率9、局部最优  0、综述:在VSLAM后端中有各种梯度下降优化算法,例如:最速下降法、牛顿法、高斯-牛顿法、LM法、Dog......
  • 迁移学习与ResNet
    一、迁移学习深度学习中,迁移学习可以让小样本学习得更好,省时,方便。eg:我们采用YOLOV5训练识别动物(假定是简单得二分类),那么我们可以使用作者基于coco数据集训练得所得权重文件weight1;在此基础上,训练我们的数据,即:使用我们的数据对weight1接着调整,直到weight1适应于我们的数据。......
  • Linux2.1.13网络源代码学习(https://qiankunli.github.io/2022/07/04/linux_2_1_13_ne
    简介简介源码目录网络分层数据结构套接字套接字与vfssk_buff结构网络协议栈实现——数据struct和协议structlinux1.2.13接收数据收到数据包的几种情况Socket读取发送数据面向过程/对象/ioc以下来自linux1.2.13源码,算是参见Linux1.0的学习笔记。源码目......