1. Exact Cover Problem

DLX是用来解决精确覆盖问题行之有效的算法。

在讲解DLX之前,我们先了解一下什么是精确覆盖问题(Exact Cover Problem)?

1.1 Polyomino

多联骨牌(Polyomino)是一种类似于七巧板的棋盘游戏:

如下图所示,除去中间\(4\)个方格不允许放置任何东西,这个棋盘总共有\(8*8-4=60\)个方格

将这\(12\)个由\(5\)个方格组成的图形全部放入到棋盘中,满足每个格子都被使用,而且只被使用一次。

每个格子都被覆盖,而且只能被覆盖一次,对,这就是精确覆盖问题!

(PS:因为\(12*5=60\),而整个棋盘除去中间\(4\)格也刚好是\(60\)格,所以你应该很容易就明白"每个格子都被覆盖,而且只能被覆盖一次"的含义)

1.2 Sudoku

数独(Sudoku)这个游戏,大家应该都非常熟悉了。

我们以经典的\(9*9\)数独为例

  • 每一个方格必须要放置一个数字,而且只能放置一次
  • 每一行只能放置1-9,而且每个数字只能出现一次
  • 每一列只能放置1-9,而且每个数字只能出现一次
  • 每一宫只能放置1-9,而且每个数字只能出现一次

是的,这很明显也是一个精确覆盖问题。

1.3 Exact Cover Problem

我们下面将精确覆盖问题抽象一下。

给定一个仅由 \(0\) 和 \(1\) 组成的矩阵,

是否能找到一个行的集合,使得集合中每一列都恰好包含一个 \(1\)

下图的矩阵中,我们可以找到一个集合\((row1,row4,row5)\),使得每一列有且只有一个\(1\)

2 Dancing Links & X Algorithm

  • Algorithm X = “traditional” backtracking ( DFS )
  • Algorithm DLX = Dancing Links + Algorithm X

2.1 X algorithm

理解了精确覆盖问题,我们再来了解一下 X 算法。

X算法是由 Donald Knuth 提出的一个用来解决 精确覆盖问题的算法。

它实际上就是一种传统意义上的回溯(Backtracking)。

假定我们需要求解的矩阵为A,我们来看一下它的主要流程:

  • 如果矩阵 \(A\) 为空,找到解;成功返回。
  • 否则,选择一个列 \(c\)。
    • 选择一个满足 \(A[r][c]=1\)行 \(r\),把 \(r\) 包含进部分解

      • 对于所有满足 \(A[r][j]=1\) 的 \(j\),从矩阵 \(A\) 中删除第 \(j\) 列;
    • 对于所有满足 \(A[i][j]=1\) 的 \(i\),从矩阵 \(A\) 中删除第 \(i\) 行。
  • 再不断减少的矩阵 \(A\) 上递归地重复上述算法。

好,这是个递归的过程,但是看起来有些费解,让我们用图来解释吧。

如下图所示,假设当前我们选择的是第\(3\)列,那么第三列中含有\(1\)的行分别是\(row1\)和\(row3\)。

假设我们选择第一行(图片中被标红),那么这行中,第3,5,6列都含有1,所以我们将列3,5和6标记,表示已经覆盖过。

由于3,5,6列已经被覆盖,所以其他行如果在列3,5或者6出现1,则一定不能选择,所以我们将第3行和第6行删去,因为第一行已经被我们选择了,所以第一行也删去,那么我们就会得到右边的新矩阵,它只包含\(row2,row4,row5\)三行。

好的,接下来我们再选择\(row2\)(在图中是第一行,但实际上它的标号是\(row2\)),选择之后,覆盖第1,4,7列,同样做删除操作之后,将会得到右边的空矩阵。

但是我们会发现,第2列并没有被覆盖,但是矩阵已经为空,所以我们并没有找到答案。

这时候,我们就需要回溯。

刚才我们选择了\(row2\),并确定\(row2\)是错误的,那么现在我们选择\(row4\),它将会覆盖第1和第4列,删除操作后,得到右边的\((1,1)\)矩阵,此时还剩下第2和第7列没有被覆盖,然而我们只剩下\(row5\)这一行,所以再次选择\(row5\),矩阵为空,所有列全部被覆盖,OK,我们得到了一组正解,它就是\((row1,row4,row5)\)

对,这就是X算法的核心思想了。

2.2 Dancing Links

没错,dancing links并不是一个算法,它实际上是一个数据结构,双向循环十字链表

如下图所示,把十字链表变成双向十字链表,再加上头尾循环,就变成了\(dancing \;links\)

不过,实际上的dancing links,还有一个链表头(List header)

前面我们用到的矩阵 \(A\),所对应的dancing links就如下图所示。

对于每一个元素,我们有5个fields,分别是\(L[x],R[x],U[x],D[x],C[x]\)

\(L,R,U,D\)分别代表x的左右和上下,\(C\)代表当前元素所在的列,实际上有时候我们还会再加上一个域,来表示当前元素所在的行。

对于每一列,我们还有一个链表头,除了拥有\(L[y],R[y],U[y],D[y],C[y]\)这5个基本的域之外,它还有一个额外的\(S[y]\),用来表示当前列总过有多少个1,别入图中\(x\)所在的列,总共有两个1,所以\(S[C[X]]=2\)

2.2.1 Subsequent Operations

假设 \(x\) 指向双向链的一个节点;\(L[x]\)和 \(R[x]\)分别表示 \(x\) 的前驱节点和后继节点。

每个程序员都知道将 \(x\) 从链表删除的操作:

\(L[R[x]] ← L[x], R[L[x]] ← R[x]\)

但是只有少数程序员意识到如下操作:

$L[R[x]] ← x, R[L[x]] ← x $

而这就是dancing links的精髓所在,在回溯的过程中,我们仅仅只是将某个元素移除,而不是将它彻底删除,所以用这种方式,我们不需要额外开辟空间去存储递归过程中的矩阵和位置信息,而是通过跳舞来解决这个问题!

2.3 DLX Algorithm

好的,还是刚才的矩阵 \(A\), 我们把dancing links运用到X算法上,来看看DLX是如何进行的。

首先我们查看\(R[head]\),发现它等于\(A\),所以我们覆盖第一列,并进行\(remove\)操作。

因为第一列需要被覆盖,所以第一列存在1的行,都将被删去,我们将这些元素标记为红色。

我们选择\(row2\), 那么\(row2\)除了覆盖第\(2\)列,还覆盖了第\(4(D)\)和第\(7(G)\)列。

于此同时,凡是也也覆盖第\(4(D)\)或者第\(7(G)\)列行,都将被删去,我们把这些元素用标记为黑色。

删去这些元素,我们继续遍历表头,这时候我们需要覆盖的是第\(2(B)\)列,同样进行\(remove\)操作。

这时候我们只能选择\(row3\),继续做相应的\(remove\)操作。

最后我们发现还剩下第\(5(E)\)列没有被覆盖,但是矩阵 \(A\) 中已经没有元素了。

这时候我们需要进行回溯,也就是这里的\(resume\)操作。

回溯回来发现,这里只有\(row3\)能选,那我们继续执行\(resume\)操作。

刚才我们选择了\(row2\),这次我们选择\(row4\), 如之前所述,再次执行\(remove\)操作。

这里又出现两个选择,\(row3\)和\(row5\),我们会先选择\(row3\),继而删光矩阵中的所有元素,发现无解,再次resume回来。

那我们继续选择\(row5\),再次执行\(remove\)操作。

最后我们只能选择\(row1\),执行\(remove\)操作。

这次我们发现,\(R[head] = head\),矩阵中也没有任何元素,所有列均被覆盖。

因此我们得到了答案\((row4,row5,row1)\)。

2.3.1 Heuristic

前面有提到过,我们还有一个叫做S的域,这个域是有作用的,我们不应该每次都选取\(head\)的右结点\(R[head]\),我们应该去选择1的数量最少的列。

如下图所示的矩阵(假设为\(B\)),第4列只有\(S[y]=1\),说明我们必须要选择\(row3\),而且\(row3\)一定是正确的,那连带图中紫色标出的另外4个1,也是正确的,于是矩阵\(B\)瞬间被\(remove\)操作删减为\((1,1)\),我们可以迅速通过2层的递归得到一个解\((row3,row5)\)

另外,如果把链表的指针形式改写为静态数组形式,效率会更高。

3 Application & Comparison

3.1 Polyomino and Exact Cover

在最开始我们介绍的多联骨牌(Polyomino),我们来考虑如何将它转化为精确覆盖问题。

首先,我们将\(60\)个方格编号,为\(1-60\)。

那么,如果某个格子被覆盖到了,那么这一列就为1,

总共有\(12\)个图形,所以我们还需要标记是哪一个图形,这里我们用\(61-72\)来表示这\(12\)个图形。

如下图,我们用十字这个图形来覆盖,如果是左边这种情况,我们会覆盖\(2,9,10,11,18\)这\(5\)列,加上十字这个图形是编号\(70\),所以我们还要覆盖列\(70\)。

如果是右边这种情况,我们会覆盖\(3,10,11,12,19,70\)这\(6\)列。

从这里可以看出,我们的矩阵会有\(72\)列,以及若干行,具体多少行,和\(12\)个图形的形状有关。

将它们完全转化为矩阵之后。就变成精确覆盖问题了,套用DLX模板,即可求解。

3.2 Sudoku and Exact Cover

数独问题怎么转化为精确覆盖问题呢?

我们需要构造的矩阵,行和列分别表示什么呢?

  • 对于列\((4*n^2)\), 一共有4个限制:

    • 位置限制:每一格有且仅有一个数.
    • 列限制:每一列中每个数仅出现一次.
    • 行限制:每一行中每个数仅出现一次.
    • 区域限制:每个区域每个数仅出现一次.
  • 对于行\((n^3)\):

    • 表示每个数放入每格中.

对于位置限制,每一个位置都需要出现一个数,且只能出现一个数,拿\(4*4\)的数独,那就是16个格子每个格子只能出现一个数,我们将它们在矩阵中编号为\(1-16\)。

如下图所示,2出现在第一行,第一列,所以在举证的列1,填上1,数字4出现在第一行第二列,所以在列2填上1。

对于列的限制,每一列中每个数仅出现一次。我们将它们在矩阵中编号为\(17-32\)。

如下图所示,2出现在数独的第一列,所以在矩阵的第18列(表示第1列出现2)填上1,数字4出现在数独第二列,所以在矩阵的第24列(表示第2列出现4)填上1。

对于行的限制,每一行中每个数仅出现一次。我们将它们在矩阵中编号为\(33-48\)。

如下图所示,2出现在数独的第一行,所以在矩阵的第34列(表示第1行出现2)填上1,数字4出现在数独第二列,所以在矩阵的第36列(表示第1行出现4)填上1。

对于宫的限制,每一宫中每个数仅出现一次。我们将它们在矩阵中编号为\(48-64\)。

如下图所示,2出现在数独的第一行,所以在矩阵的第50列(表示第1宫出现2)填上1,数字4出现在数独第二列,所以在矩阵的第52列(表示第1宫出现4)填上1。

那么最终,我们的矩阵共有\(4*n^2=64\)列

而每个格子最多有n总放置方法\((1-n)\),我们共有\(n*n\)个格子,所以最多会有\(n^3=64\)行。

对于\(9*9\)的数独,

我们将其转化为一个 \(729*324\) 的矩阵,然后DLX模板套之即可!

struct DLX{
int n, m, cnt;
int L[maxnode], R[maxnode], U[maxnode], D[maxnode], row[maxnode], col[maxnode];
int S[MAXC], H[MAXR], o[MAXR];
void init( int _n, int _m ){
n = _n; m = _m;
for( int i = 0; i <= m; ++i ){
S[i] = 0;
U[i] = D[i] = i;
L[i] = i - 1; R[i] = i + 1;
}
R[m] = 0; L[0] = m;
cnt = m;
for( int i = 1; i <= n; ++i ) H[i] = -1;
}
void link( int r, int c ){
S[c]++;
col[++cnt] = c; row[cnt] = r;
D[cnt] = D[c]; U[D[c]] = cnt;
U[cnt] = c; D[c] = cnt;
if( H[r] < 0 ) H[r] = L[cnt] = R[cnt] = cnt;
else{
R[cnt] = R[H[r]];
L[R[H[r]]] = cnt;
L[cnt] = H[r];
R[H[r]] = cnt;
}
}
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 = R[i]; j != i; j = R[j] ){
U[D[j]] = U[j];
D[U[j]] = D[j];
--S[col[j]];
}
}
void resume( int c ){
for( int i = U[c]; i != c; i = U[i] )
for( int j = L[i]; j != i; j = L[j] ){
U[D[j]] = D[U[j]] = j;
++S[col[j]];
}
L[R[c]] = R[L[c]] = c;
}
bool dancing( int d ){
if( R[0] == 0 )
return true;
int c = R[0];
for( int i = R[0]; i != 0; i = R[i] )
if( S[i] < S[c] )
c = i;
remove(c);
for( int i = D[c]; i != c; i = D[i] ){
o[d] = row[i];
for( int j = R[i] ; j != i; j = R[j] ) remove( col[j] );
if( dancing( d + 1 ) ) return true;
for( int j = L[i] ; j != i; j = L[j] ) resume( col[j] );
}
resume(c);
return false;
}
}dlx;

3.2.1 test

我先使用 qqwing 生成的难度级分别别为简单,中等和困难的\(9*9\)数独各200个。

然后对DLX和DFS分别进行测试,得到如下图所示的结果,DLX要比DFS快了60-140倍!

3.3 N-queens and Exact Cover

N皇后问题,也可以转化为精确覆盖,然后DLX模板套之。。

通过前面的讲解你应该能够自己建模了吧?试一试SPOJ NQUEEN这道题目怎么样?

4 Conclusion

  • DLX is a simple and beautiful algorithm.
  • It can solve Exact Cover Problem(精确覆盖) efficiently.
  • It can also solve Overlapping Cover(重复覆盖) Problem.(虽然本文没有提及,但这也是DLX的重要运用,需要对remove和resume操作以及dancing部分进行略微的修改)
  • Thanks for Donald E. Knuth.

5 Reference

Let’s dance ! Thank you!

Dancing Links and Exact Cover的更多相关文章

  1. [HDU1017]Exact cover[DLX][Dancing Links详解][注释例程学习法]

    Dancing Links解决Exact Cover问题. 用到了循环双向十字链表. dfs. 论文一知半解地看了一遍,搜出一篇AC的源码,用注释的方法帮助理解. HIT ACM 感谢源码po主.链接 ...

  2. HUST 1017 - Exact cover (Dancing Links 模板题)

    1017 - Exact cover 时间限制:15秒 内存限制:128兆 自定评测 5584 次提交 2975 次通过 题目描述 There is an N*M matrix with only 0 ...

  3. [ACM] HUST 1017 Exact cover (Dancing Links,DLX模板题)

    DESCRIPTION There is an N*M matrix with only 0s and 1s, (1 <= N,M <= 1000). An exact cover is ...

  4. HUST 1017 Exact cover (Dancing links)

    1017 - Exact cover 时间限制:15秒 内存限制:128兆 自定评测 6110 次提交 3226 次通过 题目描述 There is an N*M matrix with only 0 ...

  5. HUST1017 Exact cover —— Dancing Links 精确覆盖 模板题

    题目链接:https://vjudge.net/problem/HUST-1017 1017 - Exact cover 时间限制:15秒 内存限制:128兆 自定评测 7673 次提交 3898 次 ...

  6. hustoj 1017 - Exact cover dancing link

    1017 - Exact cover Time Limit: 15s Memory Limit: 128MB Special Judge Submissions: 5851 Solved: 3092 ...

  7. Dancing Link --- 模板题 HUST 1017 - Exact cover

    1017 - Exact cover Problem's Link:   http://acm.hust.edu.cn/problem/show/1017 Mean: 给定一个由0-1组成的矩阵,是否 ...

  8. 【转】Dancing Links题集

    转自:http://blog.csdn.net/shahdza/article/details/7986037 POJ3740 Easy Finding [精确覆盖基础题]HUST1017 Exact ...

  9. Dancing Links 学习笔记

    Dancing Links 本周的AI引论作业布置了一道数独 加了奇怪剪枝仍然TLE的Candy?不得不去学了dlx dlxnb! Exact cover 设全集X,X的若干子集的集合为S.精确覆盖是 ...

随机推荐

  1. NodeJs在Linux下使用的各种问题

    环境:ubuntu16.04 ubuntu中安装NodeJs 通过apt-get命令安装后发现只能使用nodejs,而没有node命令 如果想避免这种情况请看下面连接的这种安装方式: 拓展见:Linu ...

  2. Java 线程

    线程:线程是进程的组成部分,一个进程可以拥有多个线程,而一个线程必须拥有一个父进程.线程可以拥有自己的堆栈,自己的程序计数器和自己的局部变量,但不能拥有系统资源.它与父进程的其他线程共享该进程的所有资 ...

  3. 基于本地存储的kvm虚拟机在线迁移

    基于本地存储的kvm虚拟机在线迁移 kvm虚拟机迁移分为4种(1)热迁移基于共享存储(2)热迁移基于本地存储(3)冷迁移基于共享存储(4)冷迁移基于本地存储 这里介绍的是基于本地存储的热迁移 动态块迁 ...

  4. bootstrap + requireJS+ director+ knockout + web API = 一个时髦的单页程序

    也许单页程序(Single Page Application)并不是什么时髦的玩意,像Gmail在很早之前就已经在使用这种模式.通常的说法是它通过避免页面刷新大大提高了网站的响应性,像操作桌面应用程序 ...

  5. [APUE]文件和目录(上)

    一.文件权限 1. 各种ID 我在读这一章时遇到了各种ID,根据名字完全不清楚什么意思,幸好看到了这篇文章,http://blog.csdn.net/ccjjnn19890720/article/de ...

  6. .NET Core的日志[1]:采用统一的模式记录日志

    记录各种级别的日志是所有应用不可或缺的功能.关于日志记录的实现,我们有太多第三方框架可供选择,比如Log4Net.NLog.Loggr和Serilog 等,当然我们还可以选择微软原生的诊断框架(相关A ...

  7. 谈谈一些有趣的CSS题目(八)-- 纯CSS的导航栏Tab切换方案

    开本系列,谈谈一些有趣的 CSS 题目,题目类型天马行空,想到什么说什么,不仅为了拓宽一下解决问题的思路,更涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题 ...

  8. 算法与数据结构(十六) 快速排序(Swift 3.0版)

    上篇博客我们主要聊了比较高效的归并排序算法,本篇博客我们就来介绍另一种高效的排序算法:快速排序.快速排序的思想与归并排序类似,都是采用分而治之的方式进行排序的.快速排序的思想主要是取出无序序列中第一个 ...

  9. css3圆形百分比进度条的实现原理

    原文地址:css3圆形百分比进度条的实现原理 今天早上起来在查看jquery插件机制的时候,一不小心点进了css3圆形百分比进度条的相关文章,于是一发不可收拾,开始折腾了... 关于圆形圈的实现,想必 ...

  10. iOS微信第三方登录实现

    iOS微信第三方登录实现   一.接入微信第三方登录准备工作.移动应用微信登录是基于OAuth2.0协议标准构建的微信OAuth2.0授权登录系统.在进行微信OAuth2.0授权登录接入之前,在微信开 ...