詳解DLX及其應用

什麼是DLX?

讓咱們看看百度百科上的解釋:在 計算機科學 中, Dancing Links ,舞蹈鏈, 也叫 DLX, 是由 Donald Knuth 提出的數據結構,目的是快速實現他的 X算法.X算法是一種遞歸算法,時間複雜度不肯定, 深度優先, 經過回溯尋找精確覆蓋問題全部可能的解。有一些著名的精確覆蓋問題,包括鋪磚塊,八皇后問題,數獨問題。html

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) \]spa

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) \]code

6.那麼由4,同理咱們可得:
\[ \mathbf{A} = \left( \begin{array}{ccc} {1} & {1} \\ \end{array} \right) \]
那麼繼續上面的步驟,顯然整個矩陣最後就能夠被縮減完。htm

7.由此,咱們就獲得了那組解(1,4,5)blog

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應用(解數獨問題)(有空再寫)

爲何能夠用舞蹈鏈作

相關文章
相關標籤/搜索