舞蹈链(DLX)
部分转自:
dancing links x详解_多行不译必自闭的博客-CSDN博客
问题引入:
给定一 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) DLX(DancingLinksX)算法。实际上,他把上面求解的过程称为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 1−M,作为第 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 0−M中删除,然后删除第 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');
}
}
思路比较简单,十二种零件支持翻转旋转,对应 60 60 60种情况,一共有 55 55 55个格子对应 55 55 55列,对每个格子枚举 60 60 60种零件,若能放入则在放入几个格子对应的列中标记为 1 1 1,即可转化为精确覆盖问题. 但是操作起来较为繁琐,稍微简单点的做法是先打表.
代码较长,挂个链接:https://www.luogu.com.cn/paste/0v4c6xz1