并查集 是一种树型的数据结构,用于处理一些不相加集合的合并和查询问题。在使用中常常以森林来表示。 并查集也是用来维护集合的,和前面学习的set不同之处在于,并查集能很方便地同时维护很多集合。如果用set来维护会非常的麻烦。并查集的核心思想是记录每个结点的父亲结点是哪个结点。
我们来引入一下:
话说江湖上散落着各式各样的大侠,有上千个之多。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架。但大侠们有一个优点就是讲义气,绝对不打自己的朋友。而且他们信奉“朋友的朋友就是我的朋友”,只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是自己人。这样一来,江湖上就形成了一个一个的群落,通过两两之间的朋友关系串联起来。而不在同一个群落的人,无论如何都无法通过朋友关系连起来,于是就可以放心往死了打。但是两个原本互不相识的人,如何判断是否属于一个朋友圈呢?
我们可以在每个朋友圈内推举出一个比较有名望的人,作为该圈子的代表人物,这样,每个圈子就可以这样命名“齐达内朋友之队”“罗纳尔多朋友之队”……两人只要互相对一下自己的队长是不是同一个人,就可以确定敌友关系了。
但是还有问题啊,大侠们只知道自己直接的朋友是谁,很多人压根就不认识队长,要判断自己的队长是谁,只能漫无目的的通过朋友的朋友关系问下去:“你是不是队长?你是不是队长?”这样一来,队长面子上挂不住了,而且效率太低,还有可能陷入无限循环中。于是队长下令,重新组队。队内所有人实行分等级制度,形成树状结构,我队长就是根节点,下面分别是二级队员、三级队员。每个人只要记住自己的上级是谁就行了。遇到判断敌友的时候,只要一层层向上问,直到最高层,就可以在短时间内确定队长是谁了。由于我们关心的只是两个人之间是否连通,至于他们是如何连通的,以及每个圈子内部的结构是怎样的,甚至队长是谁,并不重要。所以我们可以放任队长随意重新组队,只要不搞错敌友关系就好了。于是,门派产生了。
假设现在武林中的形势如图所示。虚竹小和尚与周芷若MM是我非常喜欢的两个人物,他们的终极boss分别是玄慈方丈和灭绝师太,那明显就是两个阵营了。我不希望他们互相打架,就对他俩说:“你们两位拉拉勾,做好朋友吧。”他们看在我的面子上,同意了。这一同意可非同小可,整个少林和峨眉派的人就不能打架了。这么重大的变化,可如何实现呀,要改动多少地方?其实非常简单,我对玄慈方丈说:“大师,麻烦你把你的上级改为灭绝师太吧。这样一来,两派原先的所有人员的终极boss都是师太,那还打个球啊!反正我们关心的只是连通性,门派内部的结构不要紧的。”玄慈一听肯定火大了:“我靠,凭什么是我变成她手下呀,怎么不反过来?我抗议!”抗议无效,上天安排的,最大。反正谁加入谁效果是一样的,我就随手指定了一个。
对于并查集还有两种优化:路径压缩和按秩合并
我们继续看上文的故事,对于武林啊,使用路径压缩大致就是相当于这样
建立门派的过程是两个人两个人地连接起来的,谁当谁的手下完全随机。最后的树状结构会变成什么胎唇样,我也完全无法预计,一字长蛇阵也有可能。这样查找的效率就会比较低下。最理想的情况就是所有人的直接上级都是掌门,一共就两级结构,只要找一次就找到掌门了。哪怕不能完全做到,也最好尽量接近。这样就产生了路径压缩算法。 设想这样一个场景:两个互不相识的大侠碰面了,想知道能不能揍。 于是赶紧打电话问自己的上级:“你是不是掌门?” 上级说:“我不是呀,我的上级是谁谁谁,你问问他看看。” 一路问下去,原来两人的最终boss都是东厂曹公公。 “哎呀呀,原来是记己人,西礼西礼,在下三营六组白面葫芦娃!” “幸会幸会,在下九营十八组仙子狗尾巴花!” 两人高高兴兴地手拉手喝酒去了。 “等等等等,两位同学请留步,还有事情没完成呢!”我叫住他俩。 “哦,对了,还要做路径压缩。”两人醒悟。 白面葫芦娃打电话给他的上级六组长:“组长啊,我查过了,其习偶们的掌门是曹公公。不如偶们一起及接拜在曹公公手下吧,省得级别太低,以后查找掌门麻环。” “唔,有道理。” 白面葫芦娃接着打电话给刚才拜访过的三营长……仙子狗尾巴花也做了同样的事情。 这样,查询中所有涉及到的人物都聚集在曹公公的直接领导下。每次查询都做了优化处理,所以整个门派树的层数都会维持在比较低的水平上。
now lets look at the structure;
1) 初始化:初始的时候每个结点各自为一个集合,father[i]表示结点 i 的父亲结点,如果 father[i]=i,我们认为这个结点是当前集合根结点。
void init() {
for (int i = 1; i <= n; ++i) {
father[i] = i;
}
}
2) 查找:查找结点所在集合的根结点,结点 x 的根结点必然也是其父亲结点的根结点。
int get(int x) {
if (father[x] == x) { // x 结点就是根结点
return x;
}
return get(father[x]); // 返回父结点的根结点
}
3) 合并:将两个元素所在的集合合并在一起,通常来说,合并之前先判断两个元素是否属于同一集合。
void merge(int x, int y) {
x = get(x);
y = get(y);
if (x != y) { // 不在同一个集合
father[y] = x;
}
}
上面三个操作是并查集常用的操作
前面的并查集的复杂度实际上在有些极端情况会很慢。比如树的结构正好是一条链,那么最坏情况下,每次查询的复杂度达到了O(n) 。这并不是我们期望的结果。路径压缩的思想是,我们只关心每个结点的父结点,而并不太关心树的真正的结构。
这样我们在一次查询的时候,可以把查询路径上的所有结点的father[i]都赋值成为根结点。只需要在我们之前的查询函数上面进行很小的改动
int get(int x) {
if (father[x] == x) { // x 结点就是根结点
return x;
}
return father[x] = get(father[x]); // 返回父结点的根结点,并另当前结点父结点直接为根结点
}
路径压缩在实际应用中效率很高,其一次查询复杂度平摊下来可以认为是一个常数。并且在实际应用中,我们基本都用带路径压缩的并查集。
所谓带权并查集,是指结点存有权值信息的并查集。并查集以森林的形式存在,而结点的权值,大多是记录该结点与祖先关系的信息。比如权值可以记录该结点到根节点的距离。
例题
在排队过程中,初始时,一人一列。一共有如下两种操作。
例题解析
我们不妨设 size[]为集合中的元素个数,dist[]为元素到队首的距离,合并时,dist[A.root]需要加上size[B.root] (每个元素到队首的距离应该是到根路径上所有点的dist[]求和),size[B.root]需要加上size[A.root] (每个元素所在集合的元素个数只需查询该集合中根的size[x.root])。
1)初始化:
void init() {
for(int i = 1; i <= n; i++) {
father[i] = i, dist[i] = 0, size[i] = 1;
}
}
2)查找:查找元素所在的集合,即根节点。
int get(int x) {
if(father[x] == x) {
return x;
}
int y = father[x];
father[x] = get(y);
dist[x] += dist[y]; // x 到根结点的距离等于 x 到之前父亲结点距离加上之前父亲结点到根结点的距离
return father[x];
}
路径压缩的时候,不需要考虑 size[],但 dist[] 需要更新成到整个集合根的距离。
3)合并
将两个元素所在的集合合并为一个集合。
通常来说,合并之前,应先判断两个元素是否属于同一个集合,这可用上面的“查找”操作实现。
void merge(int a, int b) {
a = get(a);
b = get(b);
if(a != b) { // 判断两个元素是否属于同一集合
father[a] = b;
dist[a] = size[b];
size[b] += size[a];
}
}
通过小小的改动,我们就可以查询并查集这一森林中,每个元素到祖先的相关信息。
怎么样,明白些了吗?
我们来做一道题目吧!
在社交的过程中,通过朋友,也能认识新的朋友。在某个朋友关系图中,假定 A 和 B 是朋友,B 和 C 是朋友,那么 A 和 C 也会成为朋友。即,我们规定朋友的朋友也是朋友。
现在,已知若干对朋友关系,询问某两个人是不是朋友。
请编写一个程序来解决这个问题吧。
第一行:三个整数 n,m,p(n≤5000,m≤5000,p≤5000)分别表示有n 个人,m 个朋友关系,询问p 对朋友关系。
接下来 m 行:每行两个数Ai,Bi1≤Ai,Bi≤N,表示Ai 和 Bi具有朋友关系。
接下来 p 行:每行两个数,询问两人是否为朋友。
输出共 p 行,每行一个Yes
或No
。表示第i个询问的答案为是否朋友。
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
Yes Yes No
题解::
#include
#include
#include
using namespace std;
int mark[5005],fa[5005];
int get(int x)
{
if(fa[x]==x)
return x;
return fa[x]=get(fa[x]);
}
int main()
{
int n,m,p;
cin>>n>>m>>p;
int a,b;
memset(mark,0,sizeof(mark));
for(int i=1;i<=n;i++)
fa[i]=i;
while(m--)
{
cin>>a>>b;
a=get(a);
b=get(b);
if(a!=b)
{
fa[a]=b;
}
}
for(int i=1;i<=p;i++)
{
cin>>a>>b;
a=get(a);
b=get(b);
if(a==b)
mark[i]=1;
}
for(int i=1;i<=p;i++)
{
if(mark[i])
cout<<"Yes"<
谢谢