引入
例题:(洛谷 P3367 【模板】并查集)
第一行包含两个整数 N , M N,M N,M ,表示共有 N N N 个元素和 M M M 个操作。
接下来 M M M 行,每行包含三个整数 Z i , X i , Y i Z_i,X_i,Y_i Zi,Xi,Yi 。
当 Z i = 1 Z_i=1 Zi=1 时,将 X i X_i Xi 与 Y i Y_i Yi 所在的集合合并。
当 Z i = 2 Z_i=2 Zi=2 时,输出 X i X_i Xi 与 Y i Y_i Yi 是否在同一集合内,是的输出Y
;否则输出N
。
显然可以暴力解决。。。
这就要用到一种数据结构——并查集了!
并查集
并查集可以看做是一种森林,初始时每个节点都指向它自己。
我们来打一个合适的比喻:现在有四位武林高手,它们各自创建了自己的门派,开始都只有他们自己一人:
现在1号大师和2号大师打了一架,2号大师输了,加入了1号大师的门派:
3号大师和4号大师打了一架,4号大师输了,加入了3号大师的门派:
这时,4号(已经不能称之为大师了)去找2号打了一架,徒弟惹了事自然要师傅担着了,所以3号大师率领整个门派加入了1号大师:
这就是并查集的建立过程,那如果我们想知道一位好汉是哪一门派的怎么办?当然是找他师傅,通过他师父找他师傅的师傅,再找他师傅的师傅的师傅。。。一直找到掌门人。
用代码怎么实现呢?
int root(int x)
{
if(x==f[x])//如果他是掌门人
return x;//他所在的门派就是他自己的
return root(f[x]);//找师傅
}
那么帮派之间的合并又是怎样完成的呢?自然是找到两位好汉所在门派的掌门人,然后让一个掌门人加入另一个掌门人:
void join(int x,int y)
{
int r1=root(x),r2=root(y);//找掌门人
if(r1!=r2)//如果它们不是一个门派
f[r1]=r2;//打不过就加入
}
并查集的优化——路径压缩
假设我们有这样一个门派:
那这样不行啊,下达命令太慢,友军还没等到救援就已经被灭了。。。
那应该怎么办呢?采用 中央集权统治 路径压缩!
路径压缩后的并查集变成了这样:
这么神奇的功能是怎么实现的呢?
int root(int x)
{
if(x==f[x])
return x;
f[x]=root(f[x]);//路径压缩,直属中央管辖
return f[x];
}
注意!路径压缩会破坏树形结构!在题目不需要维护树形结构时才可使用。
并查集的优化——按秩合并
假设有两个帮派为了对付共同的敌人,同仇敌忾,想要友好建交,将两个帮派合并,这时它们的合并不在乎谁是老大,只要合并就行。
那么,我们应该让哪个帮派并入哪个帮派更好呢?显然,将更小的帮派并入更大的帮派更好,这样省事省力。
事实上,在并查集中,一般并不需要维护两个节点的先后关系,而只关心两个节点的所在集合。使用按秩合并,可以减少查询的时间。
按秩合并分为两种:按点的数量合并和按树的高度合并。这里采用树的高度。
void join(int x,int y)
{
int r1=root(x),r2=root(y);
if(r1!=r2)
{
if(h[r1]<h[r2])
f[r1]=r2;
else
{
f[r2]=r1;
if(h[r1]==h[r2])
h[r1]++;//增加树高
}
}
}
注意,使用前将 h [ ] h[] h[]数组初始化为 1 1 1。
为什么两个树高度相同,合并时就要加 1 1 1呢?我们来看以下例子:
它们高度相同,假设我们将节点
4
4
4加到节点
1
1
1:
显然,树高加
1
1
1。
并查集的终极优化——路径压缩&按秩合并
我们要想达到真正飞一般的速度,我们要既使用路径压缩,又使用按秩合并!
代码:(其实就是放到一起)
int root(int x)
{
if(x==f[x])
return x;
f[x]=root(f[x]);
return f[x];
}
void join(int x,int y)
{
int r1=root(x),r2=root(y);
if(r1!=r2)
{
if(h[r1]<h[r2])
f[r1]=r2;
else
{
f[r2]=r1;
if(h[r1]==h[r2])
h[r1]++;
}
}
}
这个方法有多快呢?它的复杂度为 O ( α ( n ) ) O(\alpha(n)) O(α(n)),其中 α ( n ) \alpha(n) α(n)是阿克曼函数的反函数。
不清楚?这么说吧,百度百科对 α ( n ) \alpha(n) α(n)函数是这么说的:
因为Ackermann函数的增长很快,所以其反函数 α ( x ) α(x) α(x)的增长是非常慢的,对所有在实际问题中有意义的 x x x, α ( x ) ≤ 4 α(x)≤4 α(x)≤4,所以在算法时间复杂度分析等问题中,可以把 α ( x ) α(x) α(x)看成常数。
事实上,当
x
≤
2
2
1
0
19279
x≤2^{2^{10^{19279}}}
x≤221019279
时,
α
(
x
)
≤
5
α(x)≤5
α(x)≤5。
完整代码
#include<iostream>
#include<cstdio>
#define MAXN 10010
using namespace std;
int n,m;
int f[MAXN];
int h[MAXN];
int root(int x)
{
if(x==f[x])
return x;
f[x]=root(f[x]);
return f[x];
}
void join(int x,int y)
{
int r1=root(x),r2=root(y);
if(r1!=r2)
{
if(h[r1]<h[r2])
f[r1]=r2;
else
{
f[r2]=r1;
if(h[r1]==h[r2])
h[r1]++;
}
}
}
int op,x,y;
void init()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
f[i]=i,h[i]=1;
}
int main()
{
init();
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&op,&x,&y);
if(op==1)
join(x,y);
else
if(root(x)==root(y))
printf("Y\n");
else
printf("N\n");
}
return 0;
}