9.6 并查集

9.6 并查集!

并查集最擅长做的事情——将两个元素合并到同一集合、判断两个元素是否在同一集合中。
并查集用到了树的父结点表示法。在并查集中,每个元素都保存自己的父亲结点的编号,如果自己就是根
结点,那么父亲结点就是自己。这样就可以用树形结构把在同一集合的点连接到一起了。

(1) 并查集的实现

struct node
{
int parent; // 表示父亲结点。当编号i==parent时为根结点。
int count; // 当且仅当为根结点时有意义:表示自己及子树元素的个数
int value; // 结点的值
} set[N];
int Find(int x) // 查找算法的递归版本(建议不用这个)
{
return (set[x].parent==x) ? x : (set[x].parent = Find(set[x].parent));
}
int Find(int x) // 查找算法的非递归版本
{
int y=x;
while (set[y].parent != y) // 寻找父亲结点
y = set[y].parent;
while (x!=y) // 路径压缩,即把途中经过的结点的父亲全部改成y。
{
int temp = set[x].parent;
set[x].parent = y;
x = temp;
}
return y;
}
void Union(int x, int y) // 小写的union是关键字。
{
x=Find(x); y=Find(y); // 寻找各自的根结点
if (x==y) return; // 如果不在同一个集合,合并
if (set[x].count > set[y].count) // 启发式合并,使树的高度尽量小一些
{
set[y].parent = x;
set[x].count += set[y].count;
}
else
{
set[x].parent = y;
set[y].count += set[x].count;
}
}
void Init(int cnt) // 初始化并查集,cnt是元素个数
{
for (int i=1; i<=cnt; i++)
{
set[i].parent=i;
set[i].count=1;
set[i].value=0;
}
}
void compress(int cnt) // 合并结束,再进行一次路径压缩
{
for (int i=1; i<=cnt; i++) Find(i);
}

说明:
 使用之前调用 Init()!                                                                                                                     Union(x,y):把 x 和 y 进行启发式合并,即让节点数比较多的那棵树作为“树根”,以降低层次。
 Find(x):寻找 x 所在树的根结点。Find 的时候,顺便进行了路径压缩。
上面的 Find 有两个版本,一个是递归的,另一个是非递归的。
 判断 x 和 y 是否在同一集合:if (Find(x)==Find(y)) ……
 在所有的合并操作结束后,应该执行 compress()。
 并查集的效率很高,执行 m 次查找的时间约为 O(5m)。

(2) 团伙

【问题描述】
某地的黑社会组织猖獗,警方经过长期的调查,初步获得了一些资料:整个组织有 n 个人,任何两个认识的人不是朋友就是敌人,而且满足:① 朋友的朋友是朋友;② 敌人的敌人是朋友。
所有是朋友的人组成一个团伙。现在,警方拥有关于这 n 个人的 m 条信息(即某两个人是朋友,或某两个人是敌人),请你计算出该地最多可能有多少个团伙。
【分析】
对于朋友信息,很容易想到用并查集进行集合合并。
对于敌人信息,我们也会想到并查集:如果 3 和 5 是敌人,他们应该在同一集合;接下来 3 和 8 成为敌人,也处在同一集合。这样,5 和 8 就成为了朋友,同一集合内出现了两种关系——朋友、敌人。
为了解决这个问题,我们需要仔细分析一下。为了利用并查集,我们要统一关系:要么集合内都是朋友,要么都是敌人。通过上面的例子可以看出,集合内不能都是敌人。所以集合内应该都是朋友。
3 和 5 为敌,就是 3 的敌人和 5 为朋友。我们不妨将 3 的敌人记作 3',那么 8 和 3'也是朋友。同样,3和 5'、3 和 8'也是朋友。这样就可以用并查集表示敌人的关系了。

(3) 银河英雄传说①

【问题描述】
银河系的两大军事集团在巴米利恩星域爆发战争。泰山压顶集团派宇宙舰队司令莱因哈特率领十万余艘战舰出征,气吞山河集团点名将杨威利组织麾下三万艘战舰迎敌。
杨威利擅长排兵布阵,巧妙运用各种战术屡次以少胜多,难免恣生骄气。在这次决战中,他将巴米利恩星域战场划分成 30000 列,每列依次编号为 1,2,…,30000。之后,他把自己的战舰也依次编号为 1,2,…,30000,让第 i 号战舰处于第 i 列(i=1,2,…,30000),形成“一字长蛇阵”,诱敌深入。这是初始阵形。
当进犯之敌到达时,杨威利会多次发布合并指令,将大部分战舰集中在某几列上,实施密集攻击。合并指令为“M i j”,含义为让第 i 号战舰所在的整个战舰队列,作为一个整体(头在前尾在后)接至第 j 号战舰所在的战舰队列的尾部。显然战舰队列是由处于同一列的一个或多个战舰组成的。合并指令的执行结果会使队列增大。
然而,老谋深算的莱因哈特早已在战略上取得了主动。在交战中,他可以通过庞大的情报网络随时监听杨威利的舰队调动指令。

在杨威利发布指令调动舰队的同时,莱因哈特为了及时了解当前杨威利的战舰分布情况,也会发出一些询
问指令:“C i j”。该指令意思是,询问电脑,杨威利的第 i 号战舰与第 j 号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。
作为一个资深的高级程序设计员,你被要求编写程序分析杨威利的指令,以及回答莱因哈特的询问。
【分析】
由于我们要得到的信息除了某条战舰在哪个队列,还要知道它在该队列中的位置。因此需要对并查集进行扩充。
原有的并查集可以得到 parent、count,即战舰的父亲节点、战舰本身和它后面的战舰数量(仅在战舰的父节点是它本身时有意义)。可见,光有这两个数据是不能求出任意两艘战舰的战舰数量的。
设 depth 表示战舰到其父亲节点的战舰数量(包括父亲节点本身),即深度。
刚开始时,每个节点的 parent 是它自己,depth 等于 0,count 等于 1。
在回答“M i j”时,先对 i、j 进行查找和路径压缩,如果 i、j 的父亲 f1、f2 不相等,则进行合并:
令 parent[f2]=f1,depth[f2]=count[f1],count[f1]=count[f1]+count[f2]。
(此外,路径压缩时,如果结点 a 的最终的父亲是 f,则需要令 depth[a]+=depth[f])
在回答“C i j”时,先进行查找和路径压缩。如果位于同一集合,则输出 i、j 的 depth 的差值减去 1,否则输出-1。

(4) 可爱的猴子①

【问题描述】树上挂着 n 只可爱的猴子,编号为 1,…,n(2≤n≤200000)。猴子 1 的尾巴挂在树上,每只猴子有两只手,每只手可以最多抓住一只猴子的尾巴。所有的猴子都是悬空的,因此如果一旦脱离了树,猴子会立刻掉到地上。第 0,1,…,m(1≤m≤400000)秒钟每一秒都有某个猴子把它的某只手松开,因此常常有猴子掉到地上。现在请你根据这些信息,计算出每个猴子掉在地上的时间。
【分析】
如果把连在一起的猴子看成一个集合,每次松手就是断开了集合之间的某些联系或者直接将一个集合分离成两个。
我们要求的是每只猴子第一次脱离猴子 1 所在集合的时间。
难道要用“分查集”来实现吗?
我们不妨反过来想,如果时间从第 m 秒开始倒流,则出现的情形就是不断有某只猴子的手抓住另一只猴子。则我们要求的就转化成了:每只猴子最开始在什么时候合并到猴子 1 所在的集合。这样就可以应用并查集了。设在第 t 秒钟,猴子 i 抓住(实际上是放开)了猴子 j,那么此时就将 i 所在的集合与 j 所在的集合合并。
如果需要合并,并且原先猴子 i 与猴子 j 在同一个集合,那么就将猴子 j 所在集合的所有猴子掉落的时刻都设为 t。
为了枚举某一个集合里的所有元素,我们还需要用一个链表结构与并查集共同维护猴子的集合。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值