什么是DLX?

让我们看看百度百科上的解释:在 计算机科学 中, Dancing Links ,舞蹈链, 也叫 DLX, 是由 Donald Knuth 提出的数据结构,目的是快速实现他的 X算法.X算法是一种递归算法,时间复杂度不确定, 深度优先, 通过回溯寻找精确覆盖问题所有可能的解。有一些著名的精确覆盖问题,包括铺砖块,八皇后问题,数独问题。

X算法

概念

X算法用由0和1组成的矩阵A来表示精确覆盖问题,目标是选出矩阵的若干行,使得其中的1在所有列中出现且仅出现一次。(出自度娘)

实现步骤


1.如果矩阵A为空(没有任何列),则当前局部解即为问题的一个解,返回成功;否则继续。
2.根据一定方法选择第c列。如果某一列中没有1,则返回失败,并去除当前局部解中最新加入的行。
3.选择第r行,使得A[r,c]=1(该步是不确定的)。
4.将第r行加入当前局部解中。
5.对于满足A[r,j]=1的每一列j,从矩阵A中删除所有满足A[i,j]=1的行,最后再删除第j列。
6.对所得比A小的新矩阵递归地执行此算法。

图解

例如有一个这样的矩阵A:
\[ \mathbf{A} = \left( \begin{array}{ccc} {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{array} \right) \]
ps:例子引用自grenet奆佬
这个例子就包含了一个(1,4,5)的精确覆盖解。

1.然后让我们人工模拟一遍X算法,好好体会体会:
最开始首先假定选择第一列:
\[ \left( \begin{array}{ccc} {0} & {0} & {1} & {0} & {1} & {1} & {0} \\ { } & { } & { } & { } & { } & { } & { } \\ { } & { } & { } & { } & { } & { } & { } \\ { } & { } & { } & { } & { } & { } & { } \\ { } & { } & { } & { } & { } & { } & { } \\ { } & { } & { } & { } & { } & { } & { } \\ \end{array} \right) \]
那么对于第一行有1的列,即(3,5,6),可向下不断延伸,遇到有1的位置,就把该行标记:
\[ \mathbf{B} = \left( \begin{array}{ccc} {0} & {0} & {1} & {0} & {1} & {1} & {0} \\ { } & { } & {0} & { } & {0} & {0} & { } \\ {0} & {1} & {1} & {0} & {0} & {1} & {0} \\ { } & { } & {0} & { } & {0} & {0} & { } \\ { } & { } & {0} & { } & {0} & {0} & { } \\ {0} & {0} & {0} & {1} & {1} & {0} & {1} \\ \end{array} \right) \]

2.这样就可以用A矩阵-B矩阵(即删除所标记的行和列),得到一个新的、小一点的矩阵A(即得到一个规模较小的精确覆盖问题):
\[ \mathbf{A} = \left( \begin{array}{ccc} {1} & {0} & {1} & {1} \\ {1} & {0} & {1} & {0} \\ {0} & {1} & {0} & {1} \\ \end{array} \right) \]

3.那么根据1,又可进行一下操作:
先选第一列:
\[ \mathbf{A} = \left( \begin{array}{ccc} {1} & {0} & {1} & {1} \\ { } & { } & { } & { } \\ { } & { } & { } & { } \\ \end{array} \right) \]
那么对于第一行有1的列,即(1,3,4),可向下不断延伸,遇到有1的位置,就把该行标记:
\[ \mathbf{B} = \left( \begin{array}{ccc} {1} & {0} & {1} & {1} \\ {1} & { } & {1} & {0} \\ {0} & {1} & {0} & {1} \\ \end{array} \right) \]

4.这样就可以用A矩阵-B矩阵(即删除所标记的行和列),又得到一个新的、小一点的矩阵A(即又得到一个规模较小的精确覆盖问题):
\[ \mathbf{A} = \left( \begin{array}{ccc} {0}\\ \end{array} \right) \]
这个时候我们发现当前A矩阵不为空,也没有一列有1(既无法继续操作)
则这个时候说明之前走出了错误的一步,就需要我们回溯——

5.那么根据步骤就回溯到3:
\[ \mathbf{A} = \left( \begin{array}{ccc} {1} & {0} & {1} & {1} \\ {1} & {0} & {1} & {0} \\ {0} & {1} & {0} & {1} \\ \end{array} \right) \]
这个时候就不能尝试第一行,那我们就标记第二行,并按照之前的方法扩展:
\[ \mathbf{B} = \left( \begin{array}{ccc} {1} & {0} & {1} & {1} \\ {1} & {0} & {1} & {0} \\ {0} & { } & {0} & { } \\ \end{array} \right) \]

6.那么由4,同理我们可得:
\[ \mathbf{A} = \left( \begin{array}{ccc} {1} & {1} \\ \end{array} \right) \]
那么继续上面的步骤,显然整个矩阵最后就可以被缩减完。

7.由此,我们就得到了那组解(1,4,5)

DLX算法

那为什么还要用DLX算法呢?直接用X算法不好吗?

根据我们刚才的运行过程,

如果过程中有大量的回溯和标记过程,那我们如果用数组存储之前的信息,显然是不可能的,~妥妥的MLE没商量

这个时候我们就要隆重请出舞蹈链的X算法(即DLX)

舞蹈链怎样实现


舞蹈链,即为一个双向十字循环链表, 即每个点都与上下左右四个点连有一条双向指针
(第一排的up指向最后一排,最后一排的down指向第一排;第一列的left指向最后一列,最后一列的right指向第一列)
重点:因为是链表,所以我们需要一个初始节点来建表

1.那么最开始初始化的时候,我们将第一行的指针指好:

template<typename TP>inline void init(TP n,TP m)
{
    F1(i,0,m)
    {
        L[i]=i-1,R[i]=i+1;
        U[i]=D[i]=i;
    }
    L[0]=m,R[m]=0,cnt=m;
        //cnt即已有节点,H[]即链表表头
    memset(H,-1,sizeof H);
    return;
}

2.对输入矩阵扫描,对于有1的点进行插入操作:
这样我们就只在1与1之间建链,对于X算法中挨个挨个去扩展来找1就要快得多:

template<typename TP>inline void push(TP r,TP c)
{
    U[++cnt]=c,D[cnt]=D[c];
    U[D[c]]=cnt,D[c]=cnt;
    row[cnt]=r,col[cnt]=c;
    if(H[r]!=-1)
    {
        R[cnt]=R[H[r]],L[R[H[r]]]=cnt;
        L[cnt]=H[r],R[H[r]]=cnt;
    }
    else H[r]=L[cnt]=R[cnt]=cnt;
    return;
}

3.对于最关键的删除/回溯操作
因为只将有1的点加入链表,所以直接扫就行
值得一提的是,我们在删除的时候(就是从当前所选列扩展的时候),我们只是把"对应列"有1的行与整个链表“分开”,而这一行元素之间的关系并没有破坏,这样回溯的时候就相当容易,只需反着操作一遍即可。
代码如下:

//删除操作:
template<typename TP>inline void del(TP c)
{
    L[R[c]]=L[c],R[L[c]]=R[c];
    for(TP i=D[c];i!=c;i=D[i])
        for(TP j=R[i];j!=i;j=R[j])
            U[D[j]]=U[j],D[U[j]]=D[j];
    return;
}
//回溯操作:
template<typename TP>inline void reback(TP c)
{
    for(TP i=U[c];i!=c;i=U[i])
        for(TP j=L[i];j!=i;j=L[j])
            U[D[j]]=D[U[j]]=j;
    L[R[c]]=R[L[c]]=c;
    return;
}

4.接下来就是整个舞蹈链过程中最美的地方:

template<typename TP>inline bool dancing(TP dep)
{
    if(R[0]==0)
    {
        tot=dep;
        return true;
    }
    TP c=R[0];del(c);
    for(TP i=D[c];i!=c;i=D[i])
    {
        ans[dep]=row[i];//记录答案
        for(TP j=R[i];j!=i;j=R[j]) del(col[j]);
        if(dancing(dep+1)) return true;
        for(TP j=L[i];j!=i;j=L[j]) ret(col[j]);
    }
    ret(c); //这个地方一定要记得回溯!!
    return false;
}

例题:

LuoguP4929 【模板】舞蹈链(DLX)
完整代码(建议先自己写一遍再看):

#include<cstdio>
#include<cstring>
#define rg register int
#define I inline int
#define V inline void
#define ll long long
#define db double
#define B inline bool
#define F1(i,a,b) for(rg i=a;i<=b;++i)
#define F2(i,a,b) for(rg i=a;i>=b;--i)
#define ed putchar('\n')
#define bl putchar(' ')
using namespace std;
#define getchar()(p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
char buf[1<<21],*p1=buf,*p2=buf;
template<typename TP>V read(TP &x)
{
    TP f=1;x=0;register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') f=-1;
    for(;c>='0'&&c<='9';c=getchar()) x=(x<<3)+(x<<1)+(c^'0');
    x*=f;
}
template<typename TP>V print(TP x)
{
    if(x<0) x=-x,putchar('-');
    if(x>9) print(x/10);
    putchar(x%10+'0');
}
const int N=250005;
int n,m,a,cnt;
struct Dancing_Links_X{
    int U[N],D[N],L[N],R[N],col[N],row[N],ans[N],H[N];
    V init()
    {
        F1(i,0,m)
        {
            L[i]=i-1,R[i]=i+1;
            U[i]=D[i]=i;
        }
        L[0]=m,R[m]=0,cnt=m;
        memset(H,-1,sizeof H);
        return;
    }
    template<typename TP>V push(TP r,TP c)
    {
        U[++cnt]=c,D[cnt]=D[c];
        U[D[c]]=cnt,D[c]=cnt;
        row[cnt]=r,col[cnt]=c;
        if(H[r]!=-1)
        {
            L[cnt]=H[r],R[cnt]=R[H[r]];
            L[R[H[r]]]=cnt,R[H[r]]=cnt;
        }
        else H[r]=L[cnt]=R[cnt]=cnt;
        return;
    }
    template<typename TP>V del(TP c)
    {
        L[R[c]]=L[c],R[L[c]]=R[c];
        for(TP i=D[c];i!=c;i=D[i])
            for(TP j=R[i];j!=i;j=R[j])
                U[D[j]]=U[j],D[U[j]]=D[j];
        return;
    }
    template<typename TP>V reback(TP c)
    {
        for(TP i=U[c];i!=c;i=U[i])
            for(TP j=L[i];j!=i;j=L[j])
                U[D[j]]=D[U[j]]=j;
        L[R[c]]=R[L[c]]=c;
        return;
    }
    template<typename TP>B dancing(TP tot)
    {
        if(R[0]==0)
        {
            F1(i,0,tot-1) print(ans[i]),bl;
            return true;
        }
        TP c=R[0];del(c);
        for(TP i=D[c];i!=c;i=D[i])
        {
            ans[tot]=row[i];
            for(TP j=R[i];j!=i;j=R[j]) del(col[j]);
            if(dancing(tot+1)) return true;
            for(TP j=L[i];j!=i;j=L[j]) reback(col[j]);
        }
        reback(c); 
        return false;
    }
}DLX;
int main()
{
    read(n),read(m),DLX.init();
    F1(i,1,n)
        F1(j,1,m)
        {
            read(a);
            if(a) DLX.push(i,j);
        }
    if(!DLX.work(0)) puts("No Solution!");
    return 0;
}

优化

当然,如果你完全按照上面这么打一定会~TLE(hhh

这个地方还有一个优化,就是我们在"dancing"过程中,无论用什么方法选择列最终都可以得到解,但有的方法效率明显较高。

为减少迭代次数,我们可以每次都选取1最少的列。

进行这个操作我们只需再定义一个数组s[]

在"push“中加上这样一句:

++s[c];

将"del"部分改成:

U[D[j]]=U[j],D[U[j]]=D[j],--s[col[j]];

将"reback"部分改成:

U[D[j]]=D[U[j]]=j,++s[col[j]];

将"dancing"部分改成:

TP c=R[0];
for(TP i=c;i!=0;i=R[i]) if(s[i]<s[c]) c=i;
del(c);

DLX应用(解数独问题)(有空再写)

为什么可以用舞蹈链做