并查集基本概念
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。
并查集主要操作
初始化
把每个点所在集合初始化为其自身。
通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为O(N)。
查找
查找元素所在的集合,即根节点。在返回的时候,进行路径压缩,这是并查集的核心所在,是减少时间复杂度的关键!!!
合并
将两个元素所在的集合合并为一个集合。
通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。
路径压缩示例图
如果不进行路径压缩,4要找祖先,需要经过3,2,1,最后找到0。但是路径压缩后直接就可以找到0,减少时间复杂度。
并查集例题
Description(题目描述)
若某个家族人员过于庞大,要判断两个是否是亲戚,确实不容易,给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。
Input(输入)
第一行:三个整数n,m,p,(n< =5000,m< =5000,p< =5000)
分别表示有n个人,m个亲戚关系,询问p对亲戚关系。
以下m行:每行两个数 x ,y ,1< = x,y < =n,表示 和 具有亲戚关系。
接下来p行:每行两个数 x ,y ,询问 x 和 y 是否具有亲戚关系。
Output(输出)
共P行,每行一个’Yes’或’No’。表示第 个询问的答案为“有”或“没有”亲戚关系。
分析问题实质
初步分析觉得本题是一个图论中判断两个点是否在同一个连通子图中的问题。对于题目中的样例,以人为点,关系为边,建立无向图如下:
图0-0-1 {请补充图解}
比如判断3和4是否为亲戚时,我们检查3和4是否在同一个连通子图中,结果是在,于是他们是亲戚。又如7和10不在同一个连通子图中,所以他们不是亲戚。
用图的数据结构的最大问题是,我们无法存下多至(M=)2 000 000条边的图,后面关于算法时效等诸多问题就免谈了。
用图表示关系过于“奢侈”了。其实本题只是一个对分离集合(并查集)操作的问题。
例如样例:
9 7 1
2 4
5 7
1 3
8 9
1 2
5 6
2 3
1 9
我们可以给每个人建立一个集合,集合的元素值有他自己,表示最开始时他不知道任何人是它的亲戚。以后每次给出一个亲戚关系a, b,则a和他的亲戚与b和他的亲戚就互为亲戚了,将a所在集合与b所在集合合并。对于样例数据的操作全过程如下:
初始状态:{1} {2} {3} {4} {5} {6} {7} {8} {9}
输入关系 分离集合
(2,4) {2,4}{1} {3} {5} {6} {7} {8} {9}
(5,7) {2,4} {5,7} {1} {3} {6} {8} {9}
(1,3) {1,3} {2,4} {5,7}{6} {8} {9}
(8,9) {1,3} {2,4} {5,7} {8,9}{6}
(1,2) {1,2,3,4} {5,7} {8,9}{6}
(5,6) {1,2,3,4} {5,6,7} {8,9}
(2,3) {1,2,3,4} {5,6,7} {8,9}
判断亲戚关系
(1,9),因为1,9不在同一集合内,所以输出"NO"。
最后我们得到3个集合{1,2,3,4}、{5,6,7}、{8,9},于是判断两个人是否亲戚的问题就变成判断两个数是否在同一个集合中的问题。如此一来,需要的数据结构就没有图结构那样庞大了。
算法需要以下几个子过程:
(1) 开始时,为每个人建立一个集合FHM_ak_ioi(x);
(2) 得到一个关系a b,合并相应集合FHM_ak_noi(a,b);
(3) 此外我们还需要判断两个人是否在同一个集合中,这就涉及到如何标识集合的问题。我们可以在每个集合中选一个代表标识集合,因此我们需要一个子过程给出每个集合的代表元FHM_ak_csp(a)。于是判断两个人是否在同一个集合中,即两个人是否为亲戚,等价于判断FHM_ak_csp(a)=FHM_ak_csp(b)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int maxx=5e3+100;
int f[maxx];
int n,m,p;
inline void init()
{
for(int i=0;i<=n;i++) f[i]=i;
}
inline int getf(int u)//这个函数是寻找u的祖先,并查集的路径压缩!!
{
if(u==f[u]) return u;//如果自己就是自己的祖先,就返回自己就可以了。
else return f[u]=getf(f[u]);//路径压缩,如果上一个条件不符合的话,就去寻找u父亲节点的祖先,然后将u的父亲节点直接更新为祖先,这样下一次找的时候就可以节省时间,很快就找到了。
}
inline void merge(int x,int y)
{
int u=getf(x);
int v=getf(y);//找到两个人的祖先
if(u!=v)//两个人的祖先不是一个人
{
f[u]=v;//这样就保证了将两个人的亲戚集合指向同一亲戚,代表着合并两个亲戚集合。
}
}
int main()
{
scanf("%d%d%d",&n,&m,&p);
init();//初始化,初始的每个人自己是自己的亲戚,其余的关系不知道。
int x,y;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
merge(x,y);//合并,有亲戚关系的合并到一个集合里面。
}
while(p--)
{
scanf("%d%d",&x,&y);
if(getf(x)==getf(y)) cout<<"Yes"<<endl;//这里要用getf(u)而不是直接用f[u],因为有一部分还没有完全更新到祖先上。
else cout<<"No"<<endl;
}
return 0;
}
并查集优化
①在路径压缩的时候,递归有可能爆栈,所以我们采用迭代的方法进行路径压缩。
//迭代形式的路径压缩
int getf(int v) {
int p = v, t;
while (f[p] != p) p = f[p];//找到祖先p
while (v != p) { t = f[x]; f[x] = p; x = t; } //路径压缩
return v;
}
②按秩合并
该方法使用秩来表示树高度的上界,在合并时,总是将具有较小秩的树根指向具有较大秩的树根。简单的说,就是总是将比较矮的树作为子树,添加到较高的树中。为了保存秩,需要额外使用一个与 uset 同长度的数组,并将所有元素都初始化为 0。这样找祖先会减少递归迭代的次数,最坏只有logN次。
void Merge(int x,int y)
{
int t1=getf(x),t2=getf(y);
if(t1==t2) return ;//已合并返回
if(rnk[t1]>rnk[t2]) f[t2]=t1; //把y的祖先t2和并到x的祖先t1上。因以t1为根的树更高
else {
f[t1]=t2;
if(rnk[t1]==rnk[t2]) rnk[t2]++; //若两树一样高,那么合并后,高度加一。
}
}
除此之外,还有带权并查集,以及设置虚父节点等并查集操作。等用到的时候再补上。
努力加油a啊,(o)/~