lsr1409111459
2022-02-04 11:12:13
本文所有内容(包括图片)均由 浩熙 制作,未经允许不得转发。
更好的阅读体验
不懂不知道,一懂吓一跳(虽然没有完全懂)。
这个算法/数据结构也太太太太巧妙了吧。
舞蹈链名副其实,确实好看又精巧。
舞蹈链主要解决的是 精确覆盖问题 。
给定一个由
我们可以观察出行
我们先选择第一行(用蓝色标记),然后将其中
根据要求,选择蓝色行后,所有绿色行均与其冲突,不能再选择。所以我们删掉上图中所有带颜色的部分,得到如下矩阵:
于是我们得到了规模更小的新的精确覆盖问题。选择次矩阵的第一行(原矩阵的第二行),再按照上述方法进行标记,结果如下:
再次将所有带颜色的部分删去,得到空矩阵。而此时蓝色行有
删掉上图中所有带颜色的部分,得到如下矩阵:
选择第一行(原矩阵的第五行),再进行标记,删除,得到空矩阵。而此时蓝色行没有
得到的解为原矩阵的第一行、第四行和第五行。
因此我们可以将该回溯算法总结成如下步骤:
从上述过程来看,我们需要很大的空间来存储缓存和回溯的矩阵。而如何缓存保证数据可以回溯,如何得到对应原矩阵的行的答案等,实现起来较为困难。
为了解决精确覆盖问题,高德纳( Donald Ervin Knuth )——经典巨著《计算机程序设计的艺术》的年轻作者,提出了 DLX(Dancing Links X) 算法。实际上,他提出的算法为 X算法 ,而舞蹈链则是实现这个算法的高效的数据结构。在介绍舞蹈链的过程中,我们可以发现其巧妙的结构带来的高效的效率,各元素就像在链上舞蹈一般灵巧。
在学习数据结构时,我们曾经接触过链表,其巧妙的插入删除操作让我们对其记忆犹新。
为了解决不同的问题,链表又分为单向链表,双向链表,循环链表等等。
其中双向链表,即每个元素含有两个指针,分别指向前面和后面的元素。
比如我们有三个连续的元素 A.Right=B B.Right=C B.Left=A C.left=B
将其相连。
当我们要删除元素 A.Right=C C.Left=A
。
值得注意的是,此时 B.Right=C B.Left=A
。
当我们还想将 A.Right=B C.Left=B
即可。
再观察上述删除和插入操作,类比上文中的缓存和回溯操作,我们发现其恰好对应,可以灵活运用。而更巧妙的是,它不再需要额外的空间去存储缓存的数据,而是可以直接进行删除和操作,这使得空间复杂度大大降低了。而时间上,插入一个结点和回溯一个矩阵相比也更快了。
另外,我们将双向链表的首尾相连,则得到了 双向循环链表 ,这在实际应用中是很广泛的。
看看这高大上的名字吧。
我们分析一下这个名字, 双向循环链表我们已经介绍过了,那这个十字交叉是什么东西?
我们是将若干条双向循环链表十字交叉得到一个类似于矩阵的数据结构。
我们先来分析两条双向循环链表十字交叉是个什么概念。
这就是一个十字交叉双向循环链表,其中元素
对于交点元素,它的指针便有四个。我们记成 B.Left=A B.Right=C B.up=D B.down=E
。
把舞蹈链单独写个小标题只是因为它是重点,并不代表它跟 十字交叉双向循环链表 不同。恰恰相反,舞蹈链本质就是一个 十字交叉双向循环链表 。
对于上面的样例(怕大家记不住我们把它拿下来):
我们可以构造出如下图的舞蹈链:
不用惊慌,我们一步一步分析这个数据结构。
对于上图(下文称作 DLX )中的每个结点,都含有六个元素。可以视为每个结点是 DLX 中的一个元素,而每个结点本身又是一个包含六个更细小元素的结构体。这六个元素分别为
而对于 DLX ,我们将其中的元素分为三种。
第一种为 DLX 中的数字,如上图的
第二种为 DLX 中的列元素,如上图的
第三种,或者说第三个为 DLX 中的
下面我们来具体谈一谈这样一个 DLX 究竟是如何工作的。
我们首先给出 DLX 的步骤:
head.right==head
。如果等于,说明求出解,输出答案并返回 true
;否则,继续。c=head.right
。false
;否则跳转步骤 false
。false
,则输出 No Solution!
。下面根据上述步骤进行对样例的模拟:
head.right!=head
,取 c=head.right=1
,遍历列元素所在列链(得到元素 head.right!=head
。4.取 c=head.right=2
,遍历列元素所在列链(得到元素
false
。(说明没有满足要求的行能覆盖该列)false
。head.right!=head
。c=head.right=2
,遍历列元素所在列链(得到元素 false
。(说明没有满足要求的行能覆盖该列)head.right!=head
。c=head.right=3
,遍历列元素所在列链(得到元素 head.right==head
,得出解。struct DLXnode
{
int Row,Col;
int Left,Right,Up,Down;
};
int cnt=0;
DLXnode node[6000];//DLX中的所有元素,包括head元素和列元素
int row[510];//每行第一个元素
int ans[510];//记录答案
int lcnt[510];//每列元素个数
结构体部分与上文对应。
其中 row
是为了建 DLX 时更方便,一个辅助数组。
lcnt
是用来提速的(),具体用法在 dance()
中会体现。只需记住它记录的是每列的元素个数即可。
void init()
{
for(int i=0;i<=m;i++)
{
node[i].Left=i-1;
node[i].Right=i+1;
node[i].Up=i;
node[i].Down=i;
lcnt[i]=0;
}
node[0].Left=m;
node[m].Right=0;
cnt=m;
}
初始化的是
void addnode(int r,int c)
{
//修改该结点所在列链
node[++cnt].Row=r;
node[cnt].Col=c;
node[cnt].Up=node[c].Up;
node[cnt].Down=c;
node[node[cnt].Up].Down=cnt;
node[c].Up=cnt;
//修改该结点所在行链
if(!row[r])//该结点是其所在行的第一个元素
{
node[cnt].Left=cnt;
node[cnt].Right=cnt;
row[r]=cnt;
}
else//该结点不是其所在行的第一个元素
{
node[cnt].Left=node[row[r]].Left;
node[cnt].Right=row[r];
node[node[cnt].Left].Right=cnt;
node[node[cnt].Right].Left=cnt;
}
lcnt[c]++;//对应列元素个数++
}
void remove(int c)
{
for(int i=node[c].Down;i!=c;i=node[i].Down)//枚举列链中元素
for(int j=node[i].Right;j!=i;j=node[j].Right)//枚举列链中元素所对应行链中元素并删除
{
node[node[j].Down].Up=node[j].Up;
node[node[j].Up].Down=node[j].Down;
lcnt[node[j].Col]--;
}
//删除列链(仅需删除列元素即可删除整个列链)
node[node[c].Left].Right=node[c].Right;
node[node[c].Right].Left=node[c].Left;
}
void resume(int c)
{
//恢复列链(仅需恢复列元素即可恢复整个列链)
node[node[c].Left].Right=c;
node[node[c].Right].Left=c;
for(int i=node[c].Down;i!=c;i=node[i].Down)//枚举列链中元素
for(int j=node[i].Right;j!=i;j=node[j].Right)//枚举列链中元素所对应行链中元素并恢复
{
node[node[j].Down].Up=j;
node[node[j].Up].Down=j;
lcnt[node[j].Col]++;
}
}
bool dance(int dep)//dep表示答案的个数(搜索的层数)
{
if(node[0].Right==0)//如果head.right==head,说明有解,输出答案
{
for(int i=1;i<dep-1;i++)
printf("%d ",ans[i]);
printf("%d\n",ans[dep-1]);
return true;
}
int C=node[0].Right;//取列元素 c=head.right 。
for(int i=node[0].Right;i;i=node[i].Right)//提速(见文字)
{
if(lcnt[i]<lcnt[C])
C=i;
}
remove(C);//删除列链
for(int i=node[C].Down;i!=C;i=node[i].Down)//枚举选择行
{
ans[dep]=node[i].Row;
for(int j=node[i].Right;j!=i;j=node[j].Right)//删除选择行链元素所在列链
remove(node[j].Col);
if(dance(dep+1))return true;
for(int j=node[i].Right;j!=i;j=node[j].Right)//恢复选择行链元素所在列链
resume(node[j].Col);
}
resume(C);//恢复列链
return false;
}
上述提速的枚举,在正常的舞蹈链中可以不加。它的作用仅仅是提速。
但是在洛谷的舞蹈链中,如果不加这个枚举,会
因此我们加了这个提速的枚举。原理是减少搜索树的分叉数。
首先我们知道,取所有列中任意一列进行操作对于结果没有影响,因此我们选择列中元素最少的列,这可以使得搜索树的分叉数大大减少(尤其是当搜索层数较高时)。
另外,代码中 remove(C)
和 resume(C)
也可以写在循环中,也就是 remove(node[i].Col)
和 resume(node[i].Col)
。但其实删除的都是同一行,所以写在外面可以减少一点点复杂度。不理解的话就背板子(当然也可以问),但最好还是理解理解。
int main()
{
scanf("%d%d",&n,&m);
init();
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
scanf("%d",&x);
if(x==1)addnode(i,j);
}
if(!dance(1))printf("No Solution!\n");
return 0;
}
没什么好讲的,如果上面所有内容都理解了,这个主函数肯定能看懂。
跳跃的舞者,舞蹈链(Dancing Links)算法——求解精确覆盖问题