DLX算法模板(註釋詳解)

解決這類問題的大體思路:
1)判斷所有的情況數,作爲列
2)判斷所有的條件數,作爲行
3)判斷每一行滿足哪些條件,可以插入1
4)判斷是精確覆蓋還是重複覆蓋
5)注意輸入輸出

精確覆蓋

struct DLX
{
    int n,m;///行列的規模
    int u[maxnode],d[maxnode],l[maxnode],r[maxnode];///結點四個方向的指針
    int col[maxnode],row[maxnode];///結點的行列指針
    int h[maxn],s[maxn];
    ///h[]行首結點(額外的行結點),s[]每一列的個數
    bool visit[maxn];///v[]是H()函數的標記數組
    int ansed,ans[maxn],siz;///答案的個數,答案,總結點數

    void ini(int _n,int _m)///初始化
    {
        n=_n,m=_m;///規模
        for(int i=1;i<=m;i++)///第1->m個結點作爲列首節點
        {
            u[i]=i;///上下指針都指向自己
            d[i]=i;
            l[i]=i-1;///左右指針相連
            r[i]=i+1;
            col[i]=i;///列首節點的列指針指向本列
            row[i]=0;///列首節點的行指針爲第0行
        }
        ///第0個結點(表頭head)左指針指向node1,右指針指向nodem
        ///第m個結點右指針指向head,使得行首結點首尾相接
        l[0]=m,r[0]=1;
        r[m]=0;
        siz=m;
        clclow(h);///列首結點初始化爲-1,表明該行全爲0,沒有指向哪個爲1的結點
        clc(ans);
        ansed=0;///答案的數量爲0
    }

    void Link(int R,int c)
    {
        siz++;///結點數+1
        ///接下來的操作是在Node_c和Node_c->down之間插入一個結點
        ///該列從上到下的行號不一定是從小到大的,但是插入的複雜度爲O(1)
        u[siz]=c;///插入結點的up指針指向c
        d[siz]=d[c];///插入結點的down指針指向c->down
        u[d[c]]=siz;///結點c->down的up指針指向插入結點
        d[c]=siz;///列首結點c的down指針指向插入結點
        row[siz]=R,col[siz]=c;///設置行標和列標
        if(h[R]<0)///第r行還沒有元素
        {
            h[R]=siz;///第r行的行指針指向插入結點
            r[siz]=l[siz]=siz;///插入的結點的左右指針都指向自己
        }
        else
        {///在行首結點H[r]和H[r]->right插入結點
            l[siz]=h[R];
            r[siz]=r[h[R]];
            l[r[h[R]]]=siz;
            r[h[R]]=siz;
        }
        return ;
    }

    void del(int c)///刪除列及對應的行
    {
        r[l[c]]=r[c];///刪除列首結點
        l[r[c]]=l[c];
        for(int i=d[c];i!=c;i=d[i])///遍歷該列每個結點node_i
            for(int j=r[i];j!=i;j=r[j])///將與node_i同列的結點刪除
            {
                d[u[j]]=d[j];
                u[d[j]]=u[j];
            }
        return ;
    }

    void recover(int c)///恢復列及對應的行
    {
        for(int i=u[c];i!=c;i=u[i])
            for(int j=l[i];j!=i;j=l[j])///恢復行
            {
                d[u[j]]=j;
                u[d[j]]=j;
            }
        r[l[c]]=c;///恢復列首結點
        l[r[c]]=c;
        return ;
    }

    bool Dancing(int dep) ///開始跳舞
    {
     if(r[0]==0) ///表頭的右指針指向自己
        {
            ansed=dep;
            return true;
        }
        int c=r[0];///表頭右指針指向的列首結點c
        del(c);///刪除第c列,及該列的1所在的行
        for(int i=d[c];i!=c;i=d[i])
        {
            ans[dep]=row[i];
            for(int j=r[i];j!=i;j=r[j])
                del(col[j]);
            if(Dancing(dep+1))
                return true;
            for(int j=l[i];j!=i;j=l[j])
                recover(col[j]);
        }
        return false;
    }
}dlx;

模板題:
Exact cover(HUST 1017)


重複覆蓋

其實是準確覆蓋的轉化模型。
首先選擇當前要覆蓋的列,將該列刪除,枚舉覆蓋到該列的所有行:對於某一行r,假設認爲它是解集中的一個,那麼該行所能覆蓋到的列都不必再搜,所以刪除該行覆蓋到的所有列。注意此時不用刪去覆蓋到這些列的其它行,因爲一列中允許有多個1。

h()函數剪枝利用的思想是A*搜索中的估價函數。即,對於當前的遞歸深度K下的矩陣,估計其最好情況下(即最少還需要多少步)才能出解。也就是,如果將能夠覆蓋當前列的所有行全部選中,去掉這些行能夠覆蓋到的列,將這個操作作爲一層深度。重複此操作直到所有列全部出解的深度是多少。如果當前深度加上這個估價函數返回值,其和已然不能更優(也就是已經超過當前最優解),則直接返回,不必再搜。

代碼與精確覆蓋的區別:
1)刪除/恢複函數不同:精確覆蓋是刪除/恢復行及對應的列,重複覆蓋是刪除/恢復列
2)精確覆蓋由於是NP問題,所以使用A*優化
3)Dancing函數有區別,見代碼

#include <iostream>
#include <cstdio>
#include <cstring>
#define clc(x) memset(x,0,sizeof(x))
#define clclow(x) memset(x,-1,sizeof(x))
using namespace std;

const int maxn=15*15+5,maxnode=maxn*maxn,INF=1000000000;

struct DLX
{
    int n,m;///行列的規模
    int u[maxnode],d[maxnode],l[maxnode],r[maxnode];///結點四個方向的指針
    int col[maxnode],row[maxnode];///結點的行列指針
    int h[maxn],s[maxn];
    ///h[]行首結點(額外的行結點),s[]每一列的個數
    bool visit[maxn];///v[]是H()函數的標記數組
    int ansed,ans[maxn],siz;///答案的個數,答案,總結點數

    void ini(int _n,int _m)///初始化
    {
        n=_n,m=_m;///規模
        for(int i=1;i<=m;i++)///第1->m個結點作爲列首節點
        {
            u[i]=i;///上下指針都指向自己
            d[i]=i;
            l[i]=i-1;///左右指針相連
            r[i]=i+1;
            col[i]=i;///列首節點的列指針指向本列
            row[i]=0;///列首節點的行指針爲第0行
            s[i]=0;///每一列1的個數爲0
        }
        ///第0個結點(表頭head)左指針指向node1,右指針指向nodem
        ///第m個結點右指針指向head,使得行首結點首尾相接
        l[0]=m,r[0]=1;
        r[m]=0;
        siz=m;
        clclow(h);///列首結點初始化爲-1,表明該行全爲0,沒有指向哪個爲1的結點
        clc(ans);
        ansed=INF;///次數初始化爲INF
    }

    void Link(int R,int c)
    {
        ++s[col[++siz]=c];
        u[siz]=c;///插入結點的up指針指向c
        d[siz]=d[c];///插入結點的down指針指向c->down
        u[d[c]]=siz;///結點c->down的up指針指向插入結點
        d[c]=siz;///列首結點c的down指針指向插入結點
        row[siz]=R,col[siz]=c;///設置行標和列標
        s[c]++;///第c列的1的個數++
        if(h[R]<0)///第r行還沒有元素
        {
            h[R]=siz;///第r行的行指針指向插入結點
            r[siz]=l[siz]=siz;///插入的結點的左右指針都指向自己
        }
        else
        {///在行首結點H[r]和H[r]->right插入結點
            l[siz]=h[R];
            r[siz]=r[h[R]];
            l[r[h[R]]]=siz;
            r[h[R]]=siz;
        }
        return ;
    }

    void remove(int c)///刪除第i列
    {
        for(int i=d[c];i!=c;i=d[i])
        {
            r[l[i]]=r[i];
            l[r[i]]=l[i];
        }
        return ;
    }

    void resume(int c)///恢復列
    {
        for(int i=u[c];i!=c;i=u[i])
        {
            r[l[i]]=i;
            l[r[i]]=i;
        }
        return ;
    }

    int H()///IDA*的H函數,獲得代價
    {
        int ans=0;
        clc(visit);
        for(int i=r[0];i!=0;i=r[i])
        if(!visit[i])
        {
            ans++;
            for(int j=d[i];j!=i;j=d[j])
                for(int k=r[j];k!=j;k=r[k])
                    visit[col[k]]=1;
        }
        return ans;
    }

    void Dancing(int dep) ///開始跳舞
    {
    /*  重複覆蓋
        1、如果矩陣爲空,得到結果,返回
        2、從矩陣中選擇一列,以選取最少元素的列爲優化方式
        3、刪除該列及其覆蓋的行
        4、對該列的每一行元素:刪除一行及其覆蓋的列,
        5、進行下一層搜索,如果成功則返回
        6、恢復現場,跳至4
        7、恢復所選擇行
    */
        if(dep+H()>=ansed) return ;
        if(r[0]==0) ///全部覆蓋了
        {
            if(ansed>dep) ansed=dep;
            return ;
        }
        int c=r[0];///表頭右指針指向的列首結點c
        for(int i=r[0];i!=0;i=r[i])
            if(s[i]<s[c]) c=i;
        //del(c);///精確覆蓋
        for(int i=d[c];i!=c;i=d[i])
        {
            remove(i);//新增(重複覆蓋)
            for(int j=r[i];j!=i;j=r[j])  
            	remove(j);//del(col[j])(精確覆蓋)
            Dancing(dep+1);
            for(int j=l[i];j!=i;j=l[j])  
            	resume(j);//recover(col[j])(精確覆蓋)
            resume(i);//新增(重複覆蓋)
        }
        //recover(c);///精確覆蓋
        return;
    }
}dlx;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章