舞蹈链(DLX)

舞蹈链(DLX)

部分转自:
dancing links x详解_多行不译必自闭的博客-CSDN博客

问题引入:

P4929 【模板】舞蹈链(DLX)

给定一 N N N M M M列的矩阵,每个元素要么是 0 0 0,要么是 1 1 1,要求选出其中若干行,使得这若干行组成的子矩阵中,每一列有且仅有一个 1 1 1,无解则输出 − 1 -1 1.

在这里插入图片描述

首先朴素地考虑,采用枚举法,对于每一行,有选或者不选两种选择,对 N N N行做出选择后 O ( N M ) \Omicron(NM) O(NM)扫描,最终复杂度 O ( 2 N N M ) \Omicron(2^NNM) O(2NNM)

然后考虑剪枝,每确定一行之后,考虑与该行冲突的其它行,即在该行上出现 1 1 1的列,若其它行在同一列上也出现了 1 1 1,那么与该行冲突,删去即可.同时由于该列以及满足答案,故我们将该列也删去.假定先选择第一行删去之后矩阵规模将缩小,如下图所示,矩阵中标标为红色即选择的行,标为蓝色的即为已解决的列,标为紫色的即为与当前选择行冲突的行,将三者全部删去,留下规模更小的子矩阵:
在这里插入图片描述
在这里插入图片描述

然后我们对子矩阵重复进行上述操作,直至删完为止,即可求出答案.但是删除可能有不成功的情况,需要回溯,因此除了删除亦需要复原操作.但如何进行这两个操作成为了一个棘手的问题.

于是算法大师 D o n a l d E . K n u t h Donald E.Knuth DonaldE.Knuth(《计算机程序设计艺术》的作者)出面解决了这个方面的难题。他提出了 D L X ( D a n c i n g L i n k s X ) DLX(Dancing Links X) DLXDancingLinksX算法。实际上,他把上面求解的过程称为X算法,而他提出的舞蹈链 ( D a n c i n g L i n k s ) (Dancing Links) DancingLinks实际上并不是一种算法,而是一种数据结构。一种非常巧妙的数据结构,他的数据结构在缓存和回溯的过程中效率惊人,不需要额外的空间,以及近乎线性的时间。而在整个求解过程中,指针在数据之间跳跃着,就像精巧设计的舞蹈一样,故 D o n a l d E . K n u t h Donald E.Knuth DonaldE.Knuth把它称为 D a n c i n g L i n k s Dancing Links DancingLinks(中文译名舞蹈链)。

D a n c i n g L i n k s Dancing Links DancingLinks的核心操作是删除和复原,为便于理解,我们先采用简单的双向链表为例进行模拟:

对于链表每个节点 x x x,我们有前驱 p r e [ x ] pre[x] pre[x]以及后缀 n x t [ x ] nxt[x] nxt[x],那在删除 x x x时,只需如下代码:

    pre[nxt[x]]=pre[x]; nxt[pre[x]]=nxt[x];

复原时只需把前驱后缀指定为 x x x即可.

    pre[nxt[x]]=nxt[pre[x]]=x;

这两行简单的代码蕴含了此算法的精髓.

D a n c i n g   L i n k s Dancing\ Links Dancing Links运用交叉十字循环双向链,支持矩阵动态地插入,删除,复原.

对于每个节点,我们维护几个值, l [ x ] , r [ x ] , u [ x ] , d [ x ] l[x],r[x],u[x],d[x] l[x],r[x],u[x],d[x]分别表示节点 x x x链接的左右上下节点, r o w [ x ] , c o l [ x ] row[x],col[x] row[x],col[x]分别代表j节点 x x x在原来矩阵中对应的行和列.

此外,对于原矩阵,我们维护两个个变量 h [ i ] h[i] h[i]表示第 i i i行所对应链表的头节点, t o t [ j ] tot[j] tot[j]表示第 j j j列中 1 1 1的个数.

首先我们建立一个空循环双向链表,链表头为 0 0 0,然后新建 M M M个节点分别对应 M M M列,作为列节点,编号为 1 − M 1-M 1M,作为第 0 0 0行,即其它链表的表头.\

inline void init() {
    for (R ll i=0; i<=m; i++) {
        l[i]=i-1; r[i]=i+1;
        u[i]=d[i]=i;
    }
    l[0]=m; r[m]=0;
    memset(h, -1, sizeof h);
    memset(tot, 0, sizeof tot);
    cnt=m;
}

然后对于原矩阵的每一个 1 1 1,我们将其位置插入舞蹈链,操作代码如下:

inline void add(ll x, ll y) {
    ++cnt;//新建节点
    row[cnt]=x; col[cnt]=y;//标记节点的行和列
    if (h[x]==-1)  { //如果该行链表为空,那新节点作为表头
        l[cnt]=r[cnt]=h[x]=cnt;
    }else {
        l[cnt]=h[x]; r[cnt]=r[h[x]];//第x行对应的双向链表插入操作
        l[r[cnt]]=cnt; r[h[x]]=cnt;
    }
    ++tot[y];//统计第y列1的个数
    u[cnt]=y; d[cnt]=d[y];  //将新节点插入第y列对应的双向链表
    u[d[y]]=cnt; d[y]=cnt;
}

在经过所有 1 1 1的插入后,我们可以实现从 0 0 0出发向左右遍历每一列的表头,然后从每一列出发通过 u u u d d d遍历每一列中的每一个节点.这种结构极大方便了我们的删除和复原操作.

若我们在第 x x x列填上了 1 1 1(通常 x x x选择含 1 1 1最少的列),那么将第 x x x列从横向链表 0 − M 0-M 0M中删除,然后删除第 x x x列含 1 1 1的所有行,从第 x x x列向下跳,对于遍历到的每一个节点 i i i,继续遍历其对应的行链表,将其行内所有节点 j j j(除了 i i i本身)在 j j j对应的列链表中删除,这样就解除了 i i i所对的行与其它行的关系,同时保留了 i i i与其列链表的联系,便于恢复.复原操作与删除操作相反即可.

inline void remove(ll x) {
    l[r[x]]=l[x]; r[l[x]]=r[x];
    for (R ll i=d[x]; i!=x; i=d[i]) {
        for (R ll j=l[i]; j!=i; j=l[j]) {
            d[u[j]]=d[j]; u[d[j]]=u[j];
            --tot[col[j]];
        }
    }
}

inline void resume(ll x) {
    for (R ll i=d[x]; i!=x; i=d[i]) {
        for (R ll j=l[i]; j!=i; j=l[j]) {
            d[u[j]]=u[d[j]]=j;
            ++tot[col[j]];
        }
    }
    l[r[x]]=r[l[x]]=x;
}
}
    }
}

通过上面的操作,我们选定了第 x x x列上填上 1 1 1并删除了 x x x列及所有在 x x x列上含 1 1 1的行,事实上我们并未选定具体坐标,因此下一步即枚举第 x x x列含 1 1 1的行,然后将该行计入答案,与该行冲突的行全部删去,然后迭代搜索子矩阵,若不成功,回溯时复原即可.最终十字链为空时即寻找到答案.

算法复杂度不会证,一般是 O ( 能 过 ) \Omicron(能过) O()

inline bool dance(ll now) {
    if (!r[0]) {
        res=now;
        return true;
    }
    ll c=r[0];
    for (R ll i=r[c]; i; i=r[i]) {
        if (tot[i]<tot[c]) c=i;
    }
    if (tot[c]==0) return false;
    remove(c);
    for (R ll i=d[c]; i!=c; i=d[i]) {
        resl[now]=row[i];
        for (R ll j=l[i]; j!=i; j=l[j]) {
            remove(col[j]);
        }
        if (dance(now+1)) return true;
        for (R ll j=l[i]; j!=i; j=l[j]) {
            resume(col[j]);
        }
    }
    resume(c);
    return false;
}

完整代码:DLX

应用

数独问题:

数独的特点是每一行每一列每一宫每一格都只能填一个数,因此该问题可以转化为 729 729 729行和 324 324 324列的精确覆盖问题.对于每一行我们遍历数字 i i i填在第 x x x行第 y y y列的 729 729 729种情况,然后该行在对应的行列和宫里标记为 1 1 1,最终选出的 81 81 81行即为数独的答案.

/*1~81 81 cells82~162 num[i]in row[j]163~243 num[i] in column[j]244~324 num[i] in sub[j]*/
const ll bel_sub[10][10]={{0, 0, 0, 0, 0, 0, 0, 0, 0, 0},//预处理宫
                          {0, 1, 1, 1, 2, 2, 2, 3, 3, 3},
                          {0, 1, 1, 1, 2, 2, 2, 3, 3, 3},
                          {0, 1, 1, 1, 2, 2, 2, 3, 3, 3},
                          {0, 4, 4, 4, 5, 5, 5, 6, 6, 6},
                          {0, 4, 4, 4, 5, 5, 5, 6, 6, 6},
                          {0, 4, 4, 4, 5, 5, 5, 6, 6, 6},
                          {0, 7, 7, 7, 8, 8, 8, 9, 9, 9},
                          {0, 7, 7, 7, 8, 8, 8, 9, 9, 9},
                          {0, 7, 7, 7, 8, 8, 8, 9, 9, 9}};

const ll N=730, M=325, MAXN=N*M;

ll cnt, row[MAXN], col[MAXN], l[MAXN], r[MAXN], u[MAXN], d[MAXN], h[MAXN], tot[MAXN];
inline void init() {
    cnt=324;
    for (R ll i=0; i<=cnt; i++) {
        l[i]=i-1; r[i]=i+1;
        u[i]=d[i]=i;
    }
    l[0]=cnt; r[cnt]=0;
    memset(h, -1, sizeof h);
    memset(tot, 0, sizeof tot);
}

inline void add(ll x, ll y) {
    ++cnt;
    row[cnt]=x; col[cnt]=y;
    if (h[x]<0) {
        h[x]=l[cnt]=r[cnt]=cnt;
    }else {
        l[cnt]=h[x]; r[cnt]=r[h[x]];
        r[h[x]]=l[r[cnt]]=cnt;
    }
    u[cnt]=y; d[cnt]=d[y];
    d[y]=u[d[cnt]]=cnt;
    ++tot[y];
}

inline void remove(ll x) {
    l[r[x]]=l[x]; r[l[x]]=r[x];
    for (R ll i=d[x]; i!=x; i=d[i]) {
        for (R ll j=l[i]; j!=i; j=l[j]) {
            u[d[j]]=u[j]; d[u[j]]=d[j];
            --tot[col[j]];
        }
    }
}

inline void resume(ll x) {
    for (R ll i=d[x]; i!=x; i=d[i]) {
        for (R ll j=l[i]; j!=i; j=l[j]) {
            d[u[j]]=u[d[j]]=j;
            ++tot[col[j]];
        }
    }
    l[r[x]]=r[l[x]]=x;
}

ll res[82];
inline bool dance(ll now) {
    if (!r[0]) return true;
    ll c=r[0];
    for (R ll i=r[c]; i; i=r[i]) {
        if (tot[i]<tot[c]) c=i;
    }
    if (!tot[c]) return false;
    remove(c);
    for (R ll i=d[c]; i!=c; i=d[i]) {
        for (R ll j=l[i]; j!=i; j=l[j]) {
            remove(col[j]);
        }
        res[now]=row[i];
        if (dance(now+1)) return true;
        for (R ll j=l[i]; j!=i; j=l[j]) {
            resume(col[j]);
        }
    }
    resume(c);
    return false;
}

inline void Insert(ll x, ll y, ll Num) {
    ll rem_row=(Num-1)*81+(x-1)*9+y;
    ll rem1=(x-1)*9+y, rem2=81+(Num-1)*9+x, rem3=162+(Num-1)*9+y, rem4=243+(Num-1)*9+bel_sub[x][y];
    add(rem_row, rem1);
    add(rem_row, rem2);
    add(rem_row, rem3);
    add(rem_row, rem4);
}

bool used_row[10][10], used_col[10][10], used_sub[10][10];
ll num[10][10];
int main() {
    init();
    for (R ll i=1; i<=9; i++) {
        for (R ll j=1; j<=9; j++) {
            read(num[i][j]);
            used_row[i][num[i][j]]=true;
            used_col[j][num[i][j]]=true;
            used_sub[bel_sub[i][j]][num[i][j]]=true;
            if (num[i][j]) Insert(i, j, num[i][j]);
        }
    }
    for (R ll i=1; i<=9; i++) {
        for (R ll j=1; j<=9; j++) {
            if (num[i][j]) continue;
            for (R ll k=1; k<=9; k++) {
                if (used_row[i][k] || used_col[j][k] || used_sub[bel_sub[i][j]][k]) continue;
                Insert(i, j, k);
            }
        }
    }
    dance(1);
    for (R ll i=1, Num, x, y; i<=81; i++) {
        Num=(res[i]-1)/81+1;
        res[i]-=(Num-1)*81;
        x=(res[i]-1)/9+1;
        y=(res[i]-1)%9+1;
        num[x][y]=Num;
    }
    for (R ll i=1; i<=9; i++) {
        for (R ll j=1; j<=9; j++) {
            writesp(num[i][j]);
        }
        putchar('\n');
    }
}

P4205 [NOI2005] 智慧珠游戏

思路比较简单,十二种零件支持翻转旋转,对应 60 60 60种情况,一共有 55 55 55个格子对应 55 55 55列,对每个格子枚举 60 60 60种零件,若能放入则在放入几个格子对应的列中标记为 1 1 1,即可转化为精确覆盖问题. 但是操作起来较为繁琐,稍微简单点的做法是先打表.

代码较长,挂个链接:https://www.luogu.com.cn/paste/0v4c6xz1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值