初学DLX

前言

(DLX),全称(Dancing Links X),即舞蹈链算法。

这是一个十分高效且实用的算法,它主要用于求出精确覆盖问题的一组解。(貌似重复覆盖问题也可以,但我不会(2333)

前置基础:十字链表

(DLX)这个算法,建立于十字链表的基础之上。

十字链表,就相当于对于一个链表中的节点,我们要维护它上、下、左、右四个方向的相邻节点。

似乎挺好理解的?

而它的作用,就在于可以很好地维护一个矩阵行列的删除,是(DLX)这个算法的基础。

什么是精确覆盖问题?

给你一个集合,以及它的若干子集,先要你选择其中一部分子集,使得集合中的每个元素出现且恰好出现一次。

这就是精确覆盖问题

(DLX)的大致思想

我们以每个子集作为矩阵中的行,以每个元素作为矩阵中的列。如果子集中含有某个元素,就将对应行、列上的元素变为(1),否则为(0)

例如集合为({1,2,3,4,5,6,7,8}),子集为({1,2,3,5,7},{1,3,4,8},{2,6},{4,6,8},{5,7}),就可以建出这样一个矩阵:

(1) (2) (3) (4) (5) (6) (7) (8)
(1) (1) (1) (1) (0) (1) (0) (1) (0)
(2) (1) (0) (1) (1) (0) (0) (0) (1)
(3) (0) (1) (0) (0) (0) (1) (0) (0)
(4) (0) (0) (0) (1) (0) (1) (0) (1)
(5) (0) (0) (0) (0) (1) (0) (1) (0)

然后我们要考虑,每一列只能被选择一次,因此,就能得到(DLX)的核心思想:

选择了某一行,就需要删掉所有这一行为(1)的列,以及这些列上为(1)的行。

以上面的表格为例,假设我们删去了第一行,则需要删除(1,2,3,5,7)(5)列,而(1,2,3,5)(4)行在这(5)列上有(1),因此要删去这(4)行。

所以最后剩下的表格是这样的:

(4) (6) (8)
(4) (1) (1) (1)

可以发现,在我们删完第(4)行后,整个矩阵就为空了,因此,(1,4)是一组合法解。

而如果删完所有行后依然存在某些列没被删,就说明这种选择方式不合法。

具体实现:初始化

一开始,我们要先按照元素总数建好每一列,每个点用一个十字链表存储,上下都指向自己,左右指向相邻节点。(特殊地,最左端指向最右端,最右端指向最左端)

这一过程代码如下:

struct node//十字链表上的一个节点
{
	int x,y,u,d,l,r;//坐标(x,y),剩余四个变量分别存储上、下、左、右的相邻节点
	I node(CI X=0,CI Y=0,CI U=0,CI D=0,CI L=0,CI R=0):x(X),y(Y),u(U),d(D),l(L),r(R){}//构造函数
}O[N*N+5];
I void Init(CI x)//初始化有x个元素
{
	RI i;for(tot=x,i=0;i<=x;++i) O[i]=node(0,i,i,i,i-1,i+1);//对于0~x列,每个元素上下指向自己,左右指向相邻节点
	O[O[0].l=x].r=0,memset(lnk,-1,sizeof(lnk));//最左端指向最右端,最右端指向最左端,并初始化lnk为-1
}

具体实现:插入元素

接下来,我们需要把矩阵中为(1)的元素一个个插入矩阵。

考虑插入要做些什么。

首先,我们要更新这一列的(Size)值加(1)(用于搜索剪枝,不然会跑的很慢)。

然后我们为其新建一个节点,更新十字链表的信息。

唯一需要注意的是如果这个节点是该行的第一个节点,则需要将其作为行头节点。

这一过程代码如下:

I void Insert(CI x,CI y)//插入节点 
{
	++sz[y],O[++tot]=node(x,y,y,O[y].d),O[y].d=O[O[y].d].u=tot,//更新Size,建新节点,更新上下信息 
	if(~lnk[x]) O[tot].l=lnk[x],O[tot].r=O[lnk[x]].r,O[lnk[x]].r=O[O[lnk[x]].r].l=tot;//如果已有行头节点,更新左右节点 
	else lnk[x]=O[tot].l=O[tot].r=tot;//否则将其作为行头节点 
}

具体实现:删除与还原

由前面我们知道,(DLX)的核心思想在于删除行列与还原。

考虑删除一列(还原类似),我们需要删除这一列上值为(1)的所有行,因此我们会先枚举列上的每一个元素,然后枚举当前元素所在行的每一元素,将其删去。

代码如下:

I void Delete(CI x)//删除
{
	O[O[O[x].l].r=O[x].r].l=O[x].l;//从列中删除
	for(RI i=O[x].d;i^x;i=O[i].d) for(RI j=O[i].r;j^i;j=O[j].r)//枚举列中元素和该元素所在的行
		O[O[O[j].u].d=O[j].d].u=O[j].u,--sz[O[j].y];//删除
}
I void Regain(CI x)//还原
{
	for(RI i=O[x].d;i^x;i=O[i].d) for(RI j=O[i].r;j^i;j=O[j].r)//枚举列中元素和该元素所在的行
		O[O[j].u].d=O[O[j].d].u=j,++sz[O[j].y];//还原
	O[O[x].l].r=O[O[x].r].l=x;//还原到列中
}

具体实现:(Dance)搜索

以上这些其实都只是辅助,真正核心的其实是(Dance)这个函数。

首先,我们要判断当前是否已为空矩阵,如果是,则说明已经找到一组合法解,输出即可。

否则,我们找到元素个数最少,即(Size)最小的一列,来进行操作(这应该是搜索一个常用剪枝吧)。

操作的过程就是枚举删除这一列的哪一行来删去,记得搜完一行要还原。

代码如下:

I void Dance(CI x)//类似于搜索的一个过程 
{
	if(!O[0].r) {for(RI i=1;i^x;++i) printf("%d ",res[i]);exit(0);}//如果为空,说明已经找出答案,输出 
	RI i,j,t=O[0].r;for(i=O[t].r;i;i=O[i].r) sz[t]>sz[i]&&(t=i);//找出Size最小的一列,有助于提高效率 
	Delete(t);for(i=O[t].d;i^t;i=O[i].d)//枚举选择哪一行
	{
		for(res[x]=O[i].x,j=O[i].r;j^i;j=O[j].r) Delete(O[j].y);//删掉这一行上的每一列 
		for(Dance(x+1),j=O[i].l;j^i;j=O[j].l) Regain(O[j].y);//搜索完后还原 
	}Regain(t);
}

代码(板子

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 500
using namespace std;
int n,m;
class FastIO
{
	private:
		#define FS 100000
		#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
		#define tn (x<<3)+(x<<1)
		#define D isdigit(c=tc())
		char c,*A,*B,FI[FS];
	public:
		I FastIO() {A=B=FI;}
		Tp I void read(Ty& x) {x=0;W(!D);W(x=tn+(c&15),D);}
		Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
		#undef D
}F;
class DancingLinksX//DLX算法 
{
	private:
		int tot,sz[N+5],lnk[N+5],res[N+5];
		struct node//十字链表上的一个节点
		{
			int x,y,u,d,l,r;//坐标(x,y),剩余四个变量分别存储上、下、左、右的相邻节点
			I node(CI X=0,CI Y=0,CI U=0,CI D=0,CI L=0,CI R=0):x(X),y(Y),u(U),d(D),l(L),r(R){}//构造函数
		}O[N*N+5];
		I void Dance(CI x)//类似于搜索的一个过程 
		{
			#define Delete(x)
			{
				O[O[O[x].l].r=O[x].r].l=O[x].l;
				for(RI i=O[x].d;i^x;i=O[i].d) for(RI j=O[i].r;j^i;j=O[j].r)
					O[O[O[j].u].d=O[j].d].u=O[j].u,--sz[O[j].y];
			}//删除 
			#define Regain(x)
			{
				for(RI i=O[x].d;i^x;i=O[i].d) for(RI j=O[i].r;j^i;j=O[j].r)
					O[O[j].u].d=O[O[j].d].u=j,++sz[O[j].y];
				O[O[x].l].r=O[O[x].r].l=x;
			}//还原 
			if(!O[0].r) {for(RI i=1;i^x;++i) printf("%d ",res[i]);exit(0);}//如果为空,说明已经找出答案,输出 
			RI i,j,t=O[0].r;for(i=O[t].r;i;i=O[i].r) sz[t]>sz[i]&&(t=i);//找出Size最小的一列,有助于提高效率 
			Delete(t);for(i=O[t].d;i^t;i=O[i].d)//枚举选择哪一行
			{
				for(res[x]=O[i].x,j=O[i].r;j^i;j=O[j].r) Delete(O[j].y);//删掉这一行上的每一列 
				for(Dance(x+1),j=O[i].l;j^i;j=O[j].l) Regain(O[j].y);//搜索完后还原 
			}Regain(t);
		}
	public:
		I void Init(CI x)//初始化有x个元素
		{
			RI i;for(tot=x,i=0;i<=x;++i) O[i]=node(0,i,i,i,i-1,i+1);//对于0~x列,每个元素上下指向自己,左右指向相邻节点
			O[O[0].l=x].r=0,memset(lnk,-1,sizeof(lnk));//最左端指向最右端,最右端指向最左端,并初始化lnk为-1
		}
		I void Insert(CI x,CI y)//插入节点 
		{
			++sz[y],O[++tot]=node(x,y,y,O[y].d),O[y].d=O[O[y].d].u=tot,//更新Size,建新节点,更新上下信息 
			~lnk[x]?(O[tot].l=lnk[x],O[tot].r=O[lnk[x]].r,O[lnk[x]].r=O[O[lnk[x]].r].l=tot)//如果已有行头节点,更新左右节点 
			:(lnk[x]=O[tot].l=O[tot].r=tot);//否则将其作为行头节点 
		}
		I void Solve() {Dance(1),puts("No Solution!");}//搜索,若无解输出"No Solution!"
}DLX;
int main()
{
	RI i,j,x;for(F.read(n,m),DLX.Init(m),i=1;i<=n;++i) 
		for(j=1;j<=m;++j) F.read(x),x&&(DLX.Insert(i,j),0);
	return DLX.Solve(),0;
}
原文地址:https://www.cnblogs.com/chenxiaoran666/p/DLX.html