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;

猜你喜欢

转载自blog.csdn.net/Spidy_harker/article/details/105095004
今日推荐