在整个求解过程中,指针在数据间跳跃着,就像精巧设计的舞蹈一样,故Donald E.Knuth把它称为Dancing Links(中文译名舞蹈链)。
目录
第一部分:DLX算法的提出
1.1一类被称为精确覆盖的问题
1.2经典的精确覆盖问题举例——矩阵覆盖
1.2.1问题描述
1.2.2从暴力算法到X算法
第二部分:精确覆盖问题的X算法
2.1X算法分析——矩阵覆盖问题的解决
第三部分:用于X算法的数据结构——双向十字循环链表
3.1主角登场——双向十字循环链表概览
3.2详细分析——双向十字循环链表的操作
Part 1:舞蹈链的节点结构体定义
Part 2:舞蹈链的初始化
Part 3:新节点的建立与插入
Part 4:指定列的删除和恢复
Final Part:起舞函数——让指针跳起来
补充附录:示例程序
在计算机科学中,精确覆盖问题指找出这样的一种覆盖,或证明其不存在。这是一个NP-完全问题,也是卡普的二十一个NP-完全问题之一。
满足以下条件的集合为一个精确覆盖:
- S*中任意两个集合没有交集,即X中的元素在S*中出现最多一次
- S*中集合的全集为X,即X中的元素在S*中出现最少一次
合二为一,即X中的元素在S*中出现恰好一次。
洛谷传送门!
给定一个N行M列的矩阵,矩阵中的每个元素要么是1,要么是0
现在需要在矩阵中挑选若干行,使得对于矩阵的每一列 j,在挑出的这些行中,有且仅有一行的第 j个元素为1,比如下面的选择方法:
图片来源:itdef的博客
简单的来说,就是从一个由0、1组成的矩阵中挑出若干整行,重新组成一个新的矩阵,使其满足下列条件:
1、设新矩阵的列数为M,则这个新矩阵中刚好含有M个1。
2、在这个新的矩阵中,任意两个1不位于同一列。
(不知道看到这里有没有想到八皇后问题,这也是DLX的应用之一)
最直接也是最容易想到的方法应该就是暴力枚举算法了。
从第一行开始,DFS枚举下面的所有行,利用判定函数判断是否成功。如果刚好覆盖,层层跳出递归(利用flag);如果到达底层都不成功则进行回溯,到上一层递归进入下一个状态,继续DFS搜索,直到找到对应的答案为止。(暴力的代码就不写了,反正复杂度绝对死翘翘)
上述的暴力复杂度是多少呢?考虑到每一行都有两种状态:被选中以及未选中。因此递归的复杂度达到了2的N次方,妥妥的T飞。
下面进行一次优化,经过认真观察可以得到在搜索时如果有两个1位于同一列中,那么该种情况必定不合法,再继续搜索下去没有必要,此时直接回溯,可以极大降低时间复杂度。
进行第二次优化,既然不会有两个1出现在同一列中,也就是说如果递归选中了一行,那么该行中所有1所在的列也可以一起删除掉,以减小矩阵规模。
到了这里,基本与Donald E.Knuth的X算法很接近了。
X算法的核心依然是采用回溯穷举搜索
Step 1:图中最上面一行(紫色标出)称为哨兵行,用于监视覆盖状态。首先选择一行(通常选择含1数量最少的一行),图中用红色标出。同时,在同一列中也有1且与红色行冲突的列用蓝色标出
Step 2:从所有蓝色的列看下去,把所有不属于红色行的1标注出来,并选中他们所在的行(图中用青色标注出来)
Step 3:根据前面的分析,图中用蓝色和青色标注出来的行和列都是不需要的,因此执行删除操作,大量减少搜索的范围。接着将剩下的矩阵单独提取出来。
Step 4:继续选择第一行,对剩下的矩阵进行同样的操作(递归性的体现),删除用红色、蓝色、青色标注出的行和列。
Step 5:经过步骤4的删除后,矩阵变成了空矩阵,但是哨兵栏并未清空,所以此次搜索失败,回溯到第三步的状态。
Step 6:回溯到之前的状态,这次选择第二行,再次进行查找、删除操作
Step 7:再次执行删除操作,矩阵中所有元素都被删除(包括哨兵栏)。至此,找到了一个精确覆盖的可行解。
上述X算法执行的过程中,出现了大量的删除、添加操作,最容易想到的数据结构是什么呢?当然是链表!然而,不同的链表不能处理二维的数据,同时考虑到在此过程中大量的删除、添加操作,因此需要一种二维强化版的链表,这就是——双向十字循环链表(简称舞蹈链)。
先来看看整体的舞蹈链长什么样子:
图片出处:Parabola 的博客
整体链表的最上端是哨兵节点(上文有提过它的作用)。
对于每一个节点而言:
- 首先,对于每一个节点而言,他的四个方向都被链接,所以称为十字链表。
- 其次,每一条链接都是双向的,可以由左节点访问右节点,也可以有右节点访问左节点,所以它又是双向链表。
- 最后,它的行和列都是循环的,意味着可以通过每一列的哨兵节点向上访问列的最末节点。每一行也是同样的,具有循环链表的性质。
#include
#include
#include
using namespace std;
const int MAXN=250501;
int ansk[MAXN]; //存储答案的数组
int node_size,row_head[MAXN]; //节点总数,第i行的头节点编号
struct Node
{
int left,right; //左右节点编号
int upper,down; //上下节点编号
int row,col; //节点所在行数、列数
int col_size; //如果该节点为列哨兵节点,存储该列的节点总数
}node[MAXN];
函数说明:舞蹈链的初始化主要针对第一行的哨兵节点,哨兵节点的上下形成自环,左右与其他节点相连。每列节点数初始化为0,每行头节点初始化为-1
重要提醒:最后千万别忘了给节点总数赋值!!!不然建立新结点的时候会有下标冲突。
void init(int col_size)
{
for(int i=0;i<=col_size;i++)
{
node[i].col=i,node[i].row=0; //初始化行号、列号
node[i].upper=node[i].down=i; //节点上下形成自环
node[i].left=i-1; node[i].right=i+1; //左右与其他节点相连
node[i].col_size=0; //列节点数初始化为0
}
/*链接最左节点与最右节点,形成横向的环*/
node[col_size].right=0;
node[0].left=col_size;
memset(row_head,-1,sizeof(row_head)); //每一行的头节点初始化为-1
node_size=col_size+1; //非常重要!!!!!!!!千万别忘了
}
函数说明:根据给出的行号、列号从内存池中建立相应的新节点,为新节点的结构体赋值。同时将新节点插入到原来已经构建好的关系网中。
插入的原则为:
- 在下方直接与所在列的哨兵节点相连,上方与原来哨兵节点连接的节点相连。特别注意连接顺序(先连接哨兵节点连接的节点,再连接哨兵节点,同时注意边的双向性)。
- 左侧与所在行的头节点的左节点(循环性,可以认为是末尾节点)相连,右侧与头节点相连。特别注意:如果将要插入的行现在没有节点,那么当前节点作为该行的头节点。
- 节点总数最后+1
void insert_node(int row,int col)
{
node[col].col_size++; //所在列的节点数+1
node[node_size].row=row,node[node_size].col=col;
node[node_size].down=col; //新节点与哨兵节点相连
node[node_size].upper=node[col].down; //新节点与哨兵节点下方的节点相连
node[node[col].down].down=node_size; //哨兵节点下方的节点与新节点相连
node[col].upper=node_size; //哨兵节点与新节点相连
if(row_head[row]==-1) //如果当前所在行没有头节点
{
row_head[row]=node_size; //当前节点作为头节点
node[node_size].left=node_size;
node[node_size].right=node_size;
}
else
{
/*与所在行的头节点以及头节点左侧的节点相连*/
node[node_size].left=node[row_head[row]].left;
node[node[row_head[row]].left].right=node_size;
node[node_size].right=row_head[row];
node[row_head[row]].left=node_size;
row_head[row]=node_size; //因为插入位置,代替了原来的头节点
}
node_size++; //节点总数自增操作
return;
}
函数说明:删除给定行时,先把哨兵节点移除,即把哨兵节点的左右节点互连,跳过哨兵节点。接下来自上而下遍历所有位于该列的节点,删除各个节点所在的行。回复给定行即为删除的逆操作。
void node_delete(int col)
{
/*先把哨兵节点的左右节点链接,跳过哨兵节点*/
node[node[col].left].right=node[col].right;
node[node[col].right].left=node[col].left;
/*从该行哨兵节点开始,自上而下遍历,并删除遍历到的节点所在行*/
for(int i=node[col].down;i!=col;i=node[i].down)
{
for(int j=node[i].right;j!=i;j=node[j].right)
{
node[node[j].upper].down=node[j].down; //将上方的节点连接至下方
node[node[j].down].upper=node[j].upper; //将下方的节点连接至上方
node[node[j].col].col_size--; //所在列的节点总数-1
}
}
}
void node_reverse(int col)
{
for(int i=node[col].upper;i!=col;i=node[i].upper)
{
for(int j=node[i].left;j!=i;j=node[j].left)
{
node[node[j].upper].down=j; //删除逆操作
node[node[j].down].upper=j;
node[node[j].col].col_size++;
}
}
/*把哨兵节点连接回去*/
node[node[col].left].right=col;
node[node[col].right].left=col;
}
函数说明:主要看之前的X算法的过程理解。
重要步骤:递归开始前寻找节点数最小的列进行递归。同时注意状态回溯的时候要把删除的列全部回溯到之前状态。
bool dance(int depth)
{
/*如果当前哨兵节点已经被删除完了,说明递归成功,输出答案并返回*/
if(node[0].right==0) {for(int i=0;i
#include
#include
#include
using namespace std;
const int MAXN=250501;
int ansk[MAXN];
int node_size,row_head[MAXN];
struct Node
{
int left,right;
int upper,down;
int row,col;
int col_size;
}node[MAXN];
void init(int col_size)
{
for(int i=0;i<=col_size;i++)
{
node[i].col=i,node[i].row=0;
node[i].upper=node[i].down=i;
node[i].left=i-1; node[i].right=i+1;
node[i].col_size=0;
}
node[col_size].right=0;
node[0].left=col_size;
memset(row_head,-1,sizeof(row_head));
node_size=col_size+1;
}
void insert_node(int row,int col)
{
node[col].col_size++;
node[node_size].row=row,node[node_size].col=col;
node[node_size].down=col;
node[node_size].upper=node[col].down;
node[node[col].down].down=node_size;
node[col].upper=node_size;
if(row_head[row]==-1)
{
row_head[row]=node_size;
node[node_size].left=node_size;
node[node_size].right=node_size;
}
else
{
node[node_size].left=node[row_head[row]].left;
node[node[row_head[row]].left].right=node_size;
node[node_size].right=row_head[row];
node[row_head[row]].left=node_size;
row_head[row]=node_size;
}
node_size++;
return;
}
void node_delete(int col)
{
node[node[col].left].right=node[col].right;
node[node[col].right].left=node[col].left;
for(int i=node[col].down;i!=col;i=node[i].down)
{
for(int j=node[i].right;j!=i;j=node[j].right)
{
node[node[j].upper].down=node[j].down;
node[node[j].down].upper=node[j].upper;
node[node[j].col].col_size--;
}
}
}
void node_reverse(int col)
{
for(int i=node[col].upper;i!=col;i=node[i].upper)
{
for(int j=node[i].left;j!=i;j=node[j].left)
{
node[node[j].upper].down=j;
node[node[j].down].upper=j;
node[node[j].col].col_size++;
}
}
node[node[col].left].right=col;
node[node[col].right].left=col;
}
bool dance(int depth)
{
if(node[0].right==0) {for(int i=0;i