【搜索】Dancing Links——01矩阵

来源:互联网 发布:未来最值钱的是数据 编辑:程序博客网 时间:2024/06/05 17:56

本文转载自 

ccy1991911的博客


本来ccy是决定放弃Dancing Links的,结果第二天吴老师又哈来一堆关于Dancing Links的资料,于是,ccy又被埋了!~

    额,到现在为止,ccy就会01矩阵,这里,写一下我学习01矩阵这个问题时操作上的一些东西。

    ccy对于01矩阵的操作想的比较清楚,但是,对于效率的一些分析还是很茫然,吴老师说,我们长期处于知其然而不知其所以然状态中。

    这是Dancing Links论文的链接,反正ccy看起很累就是了:http://sqybi.com/works/dlxcn/

**************************************************************************************************

    问题描述:给定一个由0和1组成的矩阵,是否能找到一个行的集合,使得集合中每一列都恰好包含一个1?

    如:6 7
        0010110
        1001001
        0110010
        1001000
        0100001
        0001101

    其中,行集合{1,4,5}是一解。

 

    拿着这题,在不考虑啥时间复杂度效率问题的时候,很明显,用暴搜肯定是搜得出来的。现在,我们就来看下暴搜要怎样搜。(虽然,不管怎么搜,貌似都搜都出来。)

    如果01矩阵为空,问题解决;

    否则

        选取一个列c(保证列c上一定有1);

        选取一行r,满足01矩阵中A[r][c]==1;

            删除列j满足A[r][j]==1;

            删除行i满足A[i][j]==1;

    在不断减小01矩阵下,递归进行上述操作。

    这是个很明显的暴搜方案。

 

    Dancing Links同样在暴搜,只是,舞得很漂亮。

   

    DLX有种压缩的感觉。我们保存的不是整个矩阵,而是矩阵里1的关系。对于每个1,我们生成一个新的结点,结点包括这几个域:

    1、链表连接:L[x],R[x],U[x],D[x],分别表示它的上下左右是哪个结点。

    2、坐标标号:nRow[x],nCol[x],表示结点x的行号列号。

    在这个表里,我们还要做两个表头,列表头和行表头,可以直接把它想成平时链表里的哨兵结点,但是又有那么一点点不同。对于每行每列而言,我们让链尾连向那个哨兵结点,形成一个环,即,每列每行又是一个单独的双向链环。

    还需要一个大表头head,它连接着列表头和行表头。

    因为,我们是给每个1新建结点,对于表头同样是新建,所以,需要建数组表示第几行的行表头的结点标号是多少,第几列的列表头的结点标号是多少。有数组:Rd[r],Cd[c]。

    下面,我们还需要一个S[c]数组,表示,第c列上有S[c]个1。

 

    现在,看我们那个朴素的搜索,把它用上面建的这些变量来实现。

 

    先说明一下,这个诡异的链表建出来究竟是什么样子。

    如上面的样例:

    
    (ccy辛苦画滴图呀!!!!)

    绿色的是行表头和列表头,右下角那个绿色的是大表头。

    蓝色的是1结点。

    红色是链表的链接情况。

    注意两条粉红色的连线,这里只是个行和列的示意,说明的是每个行每个列其实是单独的一个环。

 

    在做dfs操作前,我们需要把这个表建出来。

    对于每个方框,我们分配一个结点空间给它,并有自己的标号。

    首先建行,再建列,再建读入数据中的那些1结点。

    有图:

     

    关于选确定的列c,我们总是选S[c]最小的一个。

    论文上阐述了好多,ccy就茫然了好多。

    我们每选取一行后,就会删除一些其他的行,使得S[]里的一些元素一定变小。而在列c的前提下,我们选取的行是要枚举的,所以,枚举数量当然越小越好。

 

    下面,我们把这个搜索的流程dfs()再写一下。

 

    如果R[head]==head,则找到一个解;

    否则

        选取一个列c;

        删除列c;

        对于每个r=D[c],D[D[c],……,当r!=c,

            选取当行nRow[r],加进答案集合;

            对于每个j=R[r],R[R[r],……,当j!=r,

                删除列Cd[nCol[j]];

            dfs();

            把nRow[r]从集合里取出;

            对于每个j=L[r],L[L[r]],……,当j!=r,

                取消删除列Cd[nCol[j]];

        取消删除列c;

 

    选取列c:

    int minnum=INT_MAX;
    int c=R[head];
    for (int i=R[head];i!=head;i=R[i])
    {
        if (!nCol[D[i]]) continue;
        if (S[nCol[D[i]]]<minnum)
        {
            minnum=S[nCol[D[i]]];
            c=i;
        }
    }

 

    删除列c:把c从表头删除,通过c的列链表去删除c链表的所有行。

    L[R[c]]=L[c];
    R[L[c]]=R[c];

    for (int i=U[c];i!=c;i=U[i])
        for (int j=L[i];j!=i;j=L[j])
        {
            if (!nCol[j]) continue;
            U[D[j]]=U[j];
            D[U[j]]=D[j];
            S[nCol[j]]--;
        }

 

    取消删除列c:

    for (int i=D[c];i!=c;i=D[i])
        for (int j=R[i];j!=i;j=R[j])
        {
            if (!nCol[j]) continue;
            S[nCol[j]]++;
            U[D[j]]=D[U[j]]=j;
        }
    L[R[c]]=R[L[c]]=c;

    这里,我们删除的方向是相反的,这里也是Dancing Links最精彩的地方。

    其实行恢复并不一定要相反,因为j的移动方向和for循环里的操作方向是垂直的,但是,列恢复一定要相反。

    如图:

     
    我们是从上到下进行删除的,这是删除后的图,打了叉的连线代表已经不存在。

    如果,我们恢复的时候依然从上到下,恢复出来的图就是这个样子:

    


    题目要求,保证选出来的行集合,每列都有1,这个,要怎么实现呢?

    开始,我纠结过这个问题,其实,有很多解决方法,就是硬开变量来记录都OK。只是,下面这个代码在处理这里的时候很巧妙,并不需要特别判断与记录。

    我们每次选取一行放进集合时,就要把那一行有1的列删去。抓住这点,如果没有1,就没有做列删除的操作,由于一开始就是建了所有列的列表头的,那么,即使做完都没还有列表头存在,那就证明该列根本就没有1,也就是在判断链表是否为空R[head]==R时,就可以做出每列是否都有1的判断。

 

ccy代码:

#include<iostream>
using namespace std;

const int maxr=1001;
const int maxc=1001;


int L[maxr*maxc],R[maxr*maxc],U[maxr*maxc],D[maxr*maxc];

int S[maxc];

int Rd[maxr],Cd[maxc];

int nRow[maxr*maxc],nCol[maxr*maxc];

int head;


int n,m,cnt;


int best[maxr],ans[maxr];

void ins_row(int r)
{
    cnt++;

    U[D[head]]=cnt;
    D[cnt]=D[head];
    U[cnt]=head;
    D[head]=cnt;

    L[cnt]=R[cnt]=cnt;

    Rd[r]=cnt;
}

void ins_column(int c)
{
    cnt++;

    L[R[head]]=cnt;
    R[cnt]=R[head];
    L[cnt]=head;
    R[head]=cnt;

    U[cnt]=D[cnt]=cnt;

    Cd[c]=cnt;
}

void ins_node(int r,int c)
{
    cnt++;

 
    int rhead=Rd[r];
 
    int chead=Cd[c];

    L[R[rhead]]=cnt;
    R[cnt]=R[rhead];
    L[cnt]=rhead;
    R[rhead]=cnt;

    U[D[chead]]=cnt;
    D[cnt]=D[chead];
    U[cnt]=chead;
    D[chead]=cnt;

    S[c]++;
    nRow[cnt]=r;
    nCol[cnt]=c;
}

void init()
{
 
    head=0;
    L[head]=R[head]=U[head]=D[head]=head;
 
    cnt=0;

    scanf("%d%d",&n,&m);
 
    for (int i=1;i<=n;i++)
        ins_row(i);
 
    for (int i=1;i<=m;i++)
        ins_column(i);
    char ch;
    for (int i=1;i<=n;i++)
    {
        scanf("%c",&ch);
        for (int j=1;j<=m;j++)
        {
            scanf("%c",&ch);
            if (ch=='1') ins_node(i,j);
        }
    }
}

void Remove(int c)
{
 
    L[R[c]]=L[c];
    R[L[c]]=R[c];

 
    for (int i=D[c];i!=c;i=D[i])
        for (int j=L[i];j!=i;j=L[j])
        {
            if (!nCol[j]) continue;
            U[D[j]]=U[j];
            D[U[j]]=D[j];
            S[nCol[j]]--;
        }
}

void Resume(int c)
{
 
    for (int i=U[c];i!=c;i=U[i])
        for (int j=R[i];j!=i;j=R[j])
        {
            if (!nCol[j]) continue;
            S[nCol[j]]++;
            U[D[j]]=D[U[j]]=j;
        }
 
    L[R[c]]=R[L[c]]=c;
}

void dfs()
{
 
    if (R[head]==head)
    {
        if (ans[0]>best[0])
            for (int i=0;i<=ans[0];i++)
                best[i]=ans[i];
        return;
    }

 
    int minnum=INT_MAX;
    int c;
    for (int i=R[head];i!=head;i=R[i])
    {
        if (!nCol[D[i]]) {c=i; return;}
        if (S[nCol[D[i]]]<minnum)
        {
            minnum=S[nCol[D[i]]];
            c=i;
        }
    }

 
    Remove(c);
 
    for (int i=U[c];i!=c;i=U[i])
    {
        ans[0]++;
        ans[ans[0]]=nRow[i];
  
        for (int j=L[i];j!=i;j=L[j])
        {
            if (!nRow[j]) continue;
            Remove(Cd[nCol[j]]);
        }
        dfs();
        ans[0]--;
  
        for (int j=R[i];j!=i;j=R[j])
        {
            if (!nRow[j]) continue;
            Resume(Cd[nCol[j]]);
        }
    }
 
    Resume(c);
}

void out_ans()
{
    for (int i=1;i<best[0];i++)
        for (int j=i+1;j<=best[0];j++)
            if (best[j]<best[i]) swap(best[i],best[j]);
    printf("%d\n",best[0]);
    for (int i=1;i<=best[0];i++)
        printf("%d ",best[i]);
}

int main()
{
    freopen("data.in","r",stdin);
    freopen("data.out","w",stdout);

    init();
    dfs();
    out_ans();

    fclose(stdin);
    fclose(stdout);
    return 0;
}

    这个代码是ccy在网上东拼西凑,最后综合了一下,形成自己的这种。

    本来找到一个代码,在建表的时候,没有先建行表头和列表头,而是遇到新的行列再建,原本两者没有什么大区别,但是,针对这题而言,我觉得我这样建还好些,而且,这样建的表和读入的数据对应得起来。

    在理解这个算法的时候,ccy有几个疑问,基本上上面都已经解释了,这里,我在列一下:

    1、删除与恢复的方向。

    2、c为什么要去S[]中的最小。

    3、怎样保证每列都有1。

    4、代码中标红色的一句。

       本来我找到的代码,这里的return写的是continue。试想,如果,有一次c的值真的是continue那里来的,那么一定没有解,而如果真的进了continue句话,这样的状态又不会再变,所以,一旦真有这种情况出现,说明,这样走下去肯定无解,因为,即使走下去,总有一次c的值会是从continue那里的i赋值而来,依然无解。

       这里,我直接用了return。

       原因是,如果列表头中存在这个表头,但是它的D[]有指着的是自己,那说明该列根本就不可能再出现1了,所以,再深入dfs下去也没有意义了,直接可以return掉。

 

    额,ccy继续加油……O(∩_∩)O~……

**************************************************************************************************

    有个地方,开始没看透,在Kruth的论文里其实就只有列表头,而这里,我还有行表头。现在正在做n皇后问题,这样,我就需要列、行、两个方向对角线的表头。这时,才发现,需呀与head关联的实际上只是列表头,这样来看,其实,我们也只需要列表头。不过,现在ccy还做不来只有列表头的呢。还是开个哨兵结点当表头比较好。O(∩_∩)O~


原创粉丝点击