Dancing Links X 算法详解
一、精确覆盖
1) Easy Finding
题意:给定一个M×N的01矩阵(其中M <= 16,N <=300),求一个行集合,使得每一列恰好有一个“1”。
题解:赤裸裸的精确覆盖问题,Dancing Links的模板题,直接按照矩阵构图后上模板。
2) Treasure Map
题意:给定一些矩形碎片(x1, y1, x2, y2),范围在[0,30]之间,要求用最少的矩形碎片拼成一个n×m(n,m <= 30)的矩形,如果没法完成,输出-1。
题解:精确覆盖。将n×m的矩形的每个单位格(1×1的格子)看成是Dancing Links矩阵的列,每个小矩形碎片看成是矩阵的行。首先对每个单位格按照行、列优先进行编号[0, n*m)。DLX矩阵A[i][j]=1当且仅当第j个单位格是第i个矩形片的组成部分。
图2-1
然后对DLX矩阵求一次精确覆盖即可。由于要求求最小解,每次搜索到一个可行解的时候更新最优解。如果某次搜索层数已经大于最优解,则直接返回(剪枝)。
3) Power Stations
题意:N个小镇由M条电缆两两相连,每个镇上有1个发电站,当某个发电站开始工作,它将会提供给由电缆直接相连的邻镇,但是由于一些特殊原因,一个镇不能接受多个镇提供的电力供给。并且每个发电站有一个发电的时间区间[L, R],对于每个发电站,只能选择至多一个[L, R]的子区间的时间段进行发电,问是否存在一种方案,使得接下来的D天内每个镇都有且仅有一个发电站来供给(其中N <= 60, M <= 150, D <= 5)。
对于图3-1这组数据,1号镇选择第一天发电,同时供给给2号镇;2号镇选择第二、三天发电,同时供给给1号镇;3号镇全程三天发电。
题解:精确覆盖问题。关键是建图的过程,如何把问题转化为Dancing Links的01矩阵是问题的关键。
我们需要理清01矩阵的行和列的含义,行代表问题的所有情况,列代表问题的约束条件。我们取行的子集的过程相当于取出一些互不冲突(即约束条件互相独立)的情况进行组合来满足问题的需求,那么行的子集就是我们需要的答案。
首先来考虑行,因为行对应了问题的所有情况,所以我们来枚举所有可能的情况,第i个发电站要么一直不发电,要么选择某一个时间段进行发电,一共两种情况:
1)第i个发电站 选择从第j天发电到k天,根据i,j,k的取值范围,可得最大行数为N*(D+1)*D/2 = 900;
2)第i个发电站 完全不工作,总共N个。
所以总的最大行数为N*(D+1)*D/2 + N = 960。那么可以用(i, j, k)代表第i个发电站的工作时间为[j, k],特殊的,(i, 0, 0)代表第i个全程不工作,靠邻镇供给。
然后考虑列,列对应了问题的约束条件:
1)每个镇只能被自己或者1个邻镇发电:列[0, N)代表第1天N个镇的发电与否,列[N, 2N)则代表第2天的,列[(i-1)N,iN)则代表第i天的N个镇的发电情况;
2)每个发电站只能取自己工作时间的一段子区间进行工作:用第D*N-1+i列代表第i个发电站的工作与否(靠邻镇供给的也算);
所以总的列数就是(D+1)*N = 360。然后枚举(i, j, k)对这个960×360的01矩阵建图,求一次精确覆盖即可。
4) NQUEEN - Yet Another N-Queen Problem
题意:给定一个N*N的棋盘,上面放置了一些互不冲突的皇后,求放置剩余的皇后完成N皇后问题(N皇后问题:N*N的棋盘放置N个皇后,使得任意两个皇后不能互相攻击,不同行、不同列、不同对角线)。
题解:经典精确覆盖问题变形。关键是建图的过程,如何把问题转化为Dancing Links的01矩阵是问题的关键。首先我们考虑没有摆放任何皇后时的解法:
行代表问题的所有情况,列代表问题的约束条件。
行的取值为棋盘的N*N个位置。
列代表了四类约束条件:
1)列[0, N) 代表了棋盘N行的占据情况
2)列[N, 2N) 代表了棋盘N列的占据情况
3)列[2N, 4N-1) 代表了棋盘2N-1条主对角线的占据情况
4)列[4N-1, 6N-2) 代表了棋盘2N-1条副对角线的占据情况
所以这是个行N*N,列6N-2的01矩阵。
图4-1
四个约束条件分情况讨论:对于(i, j)的位置,占据的行为i;占据的列为j;主对角线可以通过i和j的相对情况来判断,所有i-j相同的占据的主对角线为N+i-j;副对角线类似,所有i+j相同的位置占据的副对角线为i+j-1;
枚举所有情况,对每个(i, j)属于[1,N]建立对应的约束条件,就相当于建立了Dancing Links的01矩阵。
然而,N皇后问题不能直接求精确覆盖,因为我们发现,行和列必须完全覆盖到,但是主和副对角线没有要求一定要全部覆盖,所以我们问题的求解转变成对于这样一个01矩阵,求选出一些行,使得前2N列每列恰有一个“1” (这2N列分别对应行和列的约束条件)。那么也很简单, 只需要修改两个地方:
1) 在选择“1”元素最少的列的时候只选择[0, 2N);
2) 如果[0, 2N)列中都已经没有“1”可以选择了,那么算法终止;
以上就是求解N皇后问题的全部过程。
接下来再来看已经占据了一些格子之后,继续放置剩余棋子的N皇后问题。
之前已经讨论了每个格子对应了矩阵的一行,那么某个格子已经放置了棋子,相当于在全局可行解中必须选这些格子对应的行,那么问题就简单了,直接模拟将这些放置了棋子的格子对应的行利用Dancing Links X算法进行删除即可,并且这部分删除是不需要的回溯的,因为它们必选。然后X算法对剩余01矩阵进行求解即可。
5) Lamp
题意:给定N(N<= 500)个灯和M(M <= 500)个开关,灯是否亮起可以由任意某个开关的“开”或“关”来控制。给出M个开关的开关方案,使得所有灯都亮起。
例如,1号灯亮的条件是1号开关“开”或2号开关“开”,2号灯亮的条件是1号开关“关”。那么开关选择序列可以考虑1号“关”、2号“开”。
题解:经典精确覆盖问题变形。N皇后的简化版。还是那句话:行代表问题的所有情况,列代表问题的约束条件。那么问题的所有情况,即所有开关的“开”和“关”的状态数,情况总数为2*M。即01矩阵的每行代表每个开关的“开”或“关”。
问题的约束条件有两个,其中第二个是隐含条件:
1) 所有灯都亮起;
2) 每种开关只能调一种状态;
所有01矩阵的前N列代表在这个开关的状态下,对应的灯是否亮起(“1”代表亮,“0”代表不亮);后M列代表使用了对应的开关,也就是第i个开关代表的两行对应的列的值为“1”。
所以总的矩阵规模为 2*M行,(N+M)列的01矩阵,对前N列求一次精确覆盖即可。最后输出方案的时候,如果代表开关状态的两行都没有选中,说明任何一种状态都不会影响最后的结果,任意输出其中一种状态即可。
6) Dominoes
题意:给出以下12块骨牌,每个骨牌只能用一次,但是可以旋转或者翻转,要求铺满一个M×N的棋盘,保证M×N = 60。求方案数。
图6-1
例如,3×20的棋盘的其中一种方案如下:
题解:精确覆盖问题。行代表问题的所有情况,列代表问题的约束条件。
那么行代表了每个形状的骨牌经过翻转/旋转后的在原棋盘上每格的放置方案。这里需要注意翻转和旋转后骨牌最多有8(2×4)种情况,但是如果一旦翻转或旋转得到的结果一样的话只能算一种,所以这里可以采用二进制哈希标记每种旋转/翻转状态。
列分为两类:
1) 每个格子是否被占据;总共60个格子,即60列;
2) 当前骨牌放置方案用的是哪类骨牌,总共12列;
图6-3
因为要统计所有情况,而不是找到一个解就退出搜索,所以计算过程可能会比较慢,但是输入数据只有几种,可以算出所有情况后打表。
7) 数独系列(精确覆盖)
基础题
Sudoku Killer http://acm.hdu.edu.cn/showproblem.php?pid=1426
Su-Su-Sudoku http://acm.hdu.edu.cn/showproblem.php?pid=2780
Sudoku http://acm.hdu.edu.cn/showproblem.php?pid=3111
Sudoku http://poj.org/problem?id=2676
Sudoku http://poj.org/problem?id=3074
Sudoku http://poj.org/problem?id=3076
进阶题
Sudoku http://acm.hdu.edu.cn/showproblem.php?pid=5547 N=2的数独,深搜枚举也能过
Sudoku http://acm.hdu.edu.cn/showproblem.php?pid=3476 N=2的数独,枚举所有情况打表
Sudoku http://acm.hdu.edu.cn/showproblem.php?pid=3909 最全的经典数独题 N = 2,3,4
Squiggly Sudoku http://acm.hdu.edu.cn/showproblem.php?pid=4069 数独变形 + DFS找连通块
题意:对于一个N阶的数独,由N^2×N^2个格子组成,如图7-1为一个3阶的数独。要求满足四个限制条件:
1) 每个格子只能填1个数;
2) 每行的数字集合为[1, N^2],且不能重复;
3) 每列的数字集合为[1, N^2],且不能重复;
4) 每个“宫”的数字集合为[1, N^2],且不能重复;
其中“宫”的意思就是N×N的格子。对于N=3的情况,就是“九宫格”。现在问题是给定一个已经填了一些数字的数独,求当N=3时的一种解,满足以上四个限制条件。
题解:转变为精确覆盖问题。行代表问题的所有情况,列代表问题的约束条件。
每个格子能够填的数字为[1, 9],并且总共有3^2×3^2个格子,所以总的情况数为729种。也就是Dancing Links的行为729行。
列则分为四种:
1) [0,81)列 分别对应了81个格子是否被放置了数字。
2) [82,2*81)列 分别对应了9行,每行[1, 9]个数字的放置情况;
3) [2*81,3*81)列 分别对应了9列,每列[1, 9]个数字的放置情况;
4) [3*81,4*81)列 分别对应了9个“宫”,每“宫”[1, 9]个数字的放置情况;
所以总的列数为4*81=324列。
举个例子,对于在数独棋盘的i行j列的格子(i, j)上放置一个数字k,那么对应的Dancing Links的01矩阵行,一行上有四个“1”,分别对应四种约束条件:
1) 格子限制:行号*9+列号
2) 行不重复限制:81 + 行号*9 + (k-1)
3) 列不重复限制:2*81 + 列号*9 + (k-1)
4) “宫”不重复限制:3*81 + 宫号*9 + (k-1)
行号是i,列号是j,比较好理解;那么宫号我们定义如下图:
宫号的计算方式可以通过行号和列号得出。即 宫号 = (i/3)*3 + (j/3);
那么构建01矩阵的时候,我们从上到下,从左到右遍历数独,对于在(i, j)上有数字k的只需要插入一行,这行上有四列为“1”。对于没有填写数字的需要枚举[1, 9],把在(i, j)位置上填[1, 9]的情况都进行插入,一共9行。
矩阵构建完毕,求一次精确覆盖即可。
二、重复覆盖
8) whosyourdaddy
题意:给定一个N(N<= 50)个结点M条边的无向图,结点i选的话,所有和i相连的点都能被选中,求找出一个最少的点集合,使得所有点都能被选中。
题解:经典重复覆盖问题。利用IDA* + Dancing Links 求解。
重复覆盖的问题描述为:对于一个01矩阵,选出最少的行集合满足每列上至少一个“1”。
建立Dancing Links矩阵matrix[i][j] = 1当且仅当i和j相连或i=j。重复覆盖和精确覆盖稍微有点区别,即每次删除一列的时候,不能将这列上为“1”的行删除(这个是显然的,因为是重复覆盖,所以取出的行集合的一列上允许有多个“1”)。
具体步骤如下:
枚举递归最大深度maxDepth = [0, n)
利用DLX求解,如果有解 or 递归深度depth超过maxDepth则返回;
DLX求解时,每次找结点数最少的列mc,枚举mc的每一行被选中的情况。令本次选中的行为r,将r加入部分解,并将行r上有“1”的列全部删除。然后递归求解子矩阵。
这里需要注意的是,在删除列的时候其实只是将对应列的列首结点从链表中删除,所以在枚举行r上有“1”的列时,会枚举到已经删除的列,所以需要记录一个标记col_covered[i] 来记录第i列被删除了几次,这个是很重要的,因为在回溯的时候需要进行相应的恢复,也就是说删除列的算法需要这么写:
DoDeleteColumni
If(col_covered[i] is ZERO)
DeleteColumn i
++ col_covered[i]
那么在相应的回溯矩阵时,对删除的列进行恢复时,也要按照相反顺序恢复,即:
DoResumeColumni
-- col_covered[i]
If(col_covered[i] is ZERO)
Delete Column i
这样才能保证列删除的正确性。这里的概念等同于引用计数,即一列并不是“被删除”和“未被删除”两种状态,而是有“被删除0次”、“被删除1次”、“被删除2次”、“被删除N次”多种状态组成。
最后,由于重复覆盖的矩阵下降程度远远小于精确覆盖,会导致搜索空间树呈指数级增长的速度远远大于精确覆盖的情况,所以需要剪枝。这里介绍一种启发性函数(估价函数),也就是迭代加深(IDA*)里面的那个A*。
考虑到当前枚举深度为depth,最大枚举深度maxDepth,也就是当前搜索条件下只能最多再选择maxDepth – depth行。那么如果我们可以设计一个估价函数H()。函数返回的是剩余列删除需要选择最少的行个数。如果H() >maxDepth – depth,代表当前搜索条件下已经不可能搜到可行解了,可以直接返回,此所谓“剪枝”。
H()函数的计算原理:模拟每一列的删除,估算需要选择的行数。由于总的列数小于64,所以我们可以将每一行压缩成一个64位的整数,R[i]表示第i行的那个64位整数。用一个全局标记X记录剩下列的模拟删除情况(X的二进制第i位为“1”代表第i列已经被模拟删除)然后枚举剩下的列,如果这列没有被模拟删除,那么将它列上有“1”的行全部模拟选中,计数器cnt+1,行i模拟选中的意思就是将X = X or R[i];循环遍历每一列,最后cnt就是返回值。
通过这种方法计算出来的为估计代价,一定是小于等于实际代价的。
最后,IDA*的maxDepth就是问题的解。
9) Radar
题意:N个城市和M个雷达,每个雷达的范围为R,雷达和城市之间的距离为欧几里得距离,当距离小于等于R时能被覆盖到,求一个最小的距离R,使得所有的城市都能被覆盖到(N,M <= 50)。
题解:二分 + 重复覆盖。利用IDA* + Dancing Links 求解。
因为距离R满足单调性,即当R越大,城市被雷达覆盖的概率越大,所以可以采用二分。首先二分答案R,建立雷达和城市之间的覆盖关系图,然后利用IDA* + Dancing Links,如果能够求出一个重复覆盖,则减小R的值,反之,增大R。
注意枚举R的时候可以用整型,判断距离的时候采用平方,减小误差。
10) Bomberman - Just Search!
题意:给定一个N*M(N,M<=15)的图,其中'#'代表可以被炸弹炸裂的墙;‘.’代表可以放置炸弹的地方;‘*’代表不能被炸弹炸裂的墙;求放置最少的炸弹,将所有的‘#’炸裂。
题解:重复覆盖。
建图。给每个‘.’编号(r1, r2, … rn),给每个‘#’编号(c1, c2, … cm)。那么可以建立一个n×m的矩阵A,A[i][j] = 1代表 ri这个位置放置的炸弹可以炸到cj,枚举构建这个矩阵,然后求一次重复覆盖即可。
11) Repair Depots
题意:给定n(n<= 16)个机器人的位置,求建立c(1 <= c <= n)个修理厂,每个机器人选择最近的修理厂进行修理,机器人i和最近的那个修理厂的距离为d[i],现在要求max{d[i] | 1<=i<=n}最小,求这个最小值。
题解:Radar的加强版,难点在于修理厂的位置没有给出,我们需要模拟放置修理厂。修理厂可以放在三种位置上:
1、任意一个机器人的位置上
2、任意两个机器人的中心
3、任意三个机器人的外心(三角形外切圆圆心)
这样总的修理厂的可行个数是n + n*(n-1)/2 + n*(n-1)*(n-2)/6,也就是对应了Dancing Links矩阵的行,列就是n。
然后二分距离R,继续小于等于R的修理厂和机器人之间连边。
这里有一个很明显的优化,就是“子集剔除”。当某个修理厂的位置能够覆盖到1、3、6号机器人时,某些只能覆盖到这个机器人集合的子集的修理厂可以不需要。这个很好理解,因为是重复覆盖,一个01矩阵的任意两行,如果行A完全包含行B,那么行B一定不会比行A更优,行B可以直接剔除。
“子集剔除”可以利用dfs+ hash从大到小枚举在O(2^n)的时间内完成。
题意:给定n(n<= 50)个房子,需要在这些房子上建造m(1 <= m <= n)个消防站,第i个房子到最近的消防站的距离为d[i],现在要求max{d[i] | 1<=i<=n}最小,求这个最小值。
题解:Radar的数据加强版。二分距离 + 重复覆盖。
这里需要做一个“子集剔除”,否则会超时。由于矩阵的行数只有50,所以可以直接O(n^2)的枚举行i是否包含于行j,如果某一行被另一行包含,那么这行可以剔除。
由于列数也是50,所以每一行的状态可以压缩成一个INT64的整型。即第i的行状态可以用state[i]表示。那么第i行被第j行包含的判断条件为:state[i] != state[j] 并且 (state[i] & state[j]) == state[i]。
而且由于精度问题,二分距离的时候需要精确到1e-8。
13) Street Fighter
题意:N(N <= 25)个角色有M(M <= 2)种不同形态,给定每种形态下能够打败的其它角色相应形态的列表。求一个最小的角色列表,使得这个列表里面的角色能够打败列表以外的其它角色。
题解:精确覆盖 + 重复覆盖。构建DancingLinks矩阵A时,2N行代表N个角色的M种形态,2N列代表被打败的角色信息。
如果R1(0 <= R1
A[R1*2 + M1][R2*2 + M2] = 1;
题目要求选择一个角色时只能选取其中的一种形态,也就是说如果0号形态用了,那么1号形态就不能用了。这个对应了精确覆盖,所以在DLX矩阵的最后N列还要给出每个形态对应的角色。
这里还有一个隐含条件:就是任何角色的任何形态一定能够打败自己的任何形态。这样就可以开始求解了,前2N列求重复覆盖,后N列求精确覆盖。
14) 神龙的难题
题意:给定一个N×M的01矩阵,(N, M<= 15),再给定X×Y的框,要求用X×Y的框去覆盖这个01矩阵,使得所有的1都能被覆盖住,求满足条件的最少的框个数。
题解:重复覆盖。01矩阵中的每个1代表DLX矩阵中的一列,X×Y的框的所有放置情况代表DLX矩阵中的行,建立DLX关系矩阵求重复覆盖即可。
注意,这里的列数可能超过64,所以进行行状态hash的时候每行不能用一个INT64来表示,而是要用一个INT64的数组来表示状态。
15) Square Destroyer
题意:给定如图所示n*n(n <=5)的正方形,由2n(n+1)根火柴组成,求去掉最少的火柴使得所有的正方形都不存在。
题解:经典重复覆盖问题。模拟计算每根火柴能够销毁那些正方形。火柴对应Dancing Links 矩阵的行,正方形对应矩阵的列, A[i][j] = 1当且仅当第i根火柴是第j个正方形的组成部分(也可以理解成第i根火柴拿掉,第j个正方形就摧毁了)。建好图后求一次重复覆盖即可。
16) Airport
题意:Radar的变形。唯一的不同是把距离的计算方式从欧几里得距离变成了曼哈顿距离。二分距离,建图后计算重复覆盖即可。
17) A simple math problem.
题意:一个买彩票的问题。N个数字选M个,如果中了R个数就算中二等奖,问至少要买多少张彩票才能使得至少中一次二等奖(1<=R<=N<=M<=8)。
题解:C[N][M]为总共N个数选M个的情况数,C[N][R]为N个数选R个的情况数。那么建立DLX矩阵,矩阵的行代表选择数字的所有情况,矩阵的列代表所有中奖的情况,那么行数为C[N][M],列数为C[N][R]。
如图17-1,代表N=4,M=3,R=2的情况,每一行代表了彩票的购买情况(为了加以区分,四种情况分别采用四种颜色表示)。在每种情况下,能够中奖的情况用彩色格子进行标记。那么问题转化成了重复覆盖问题。
N=8,M=5,R=3的情况,搜索空间巨大,DancingLinks搜索半天出不来,最后只能采用流氓法打表了。