恩,前两周学习了并查集,是时候总结一下了。
等价关系与等价类
从数学上看,等价类是一个对象(或成员)的集合,在此集合中的所有对象应满足等价关系。若用符号"≡"表示集合上的等价关系,那么对于该集合中的任意对象x,y, z,下列性质成立:
1、自反性:x ≡ x
2、对称性:若 x ≡ y 则 y ≡ x
3、传递性:若 x ≡ y 且 y ≡ z 则 x ≡ z
因此,等价关系是集合上的一个自反、对称、传递的关系。
通过金属线连接起来的电器的连通性,就是一种等价关系。这种关系显然具有自反性,因为任何一个器件都是与自身连通的;如果a 电连通b,那么b一定也电连通a,因此这种关系具有对称性; 若a连通到b,并且b连通到c,那么a连通到c 。
并查集
并查集的一般用途就是用来维护某种具有自反、对称、传递性质的关系的等价类。并查集一般以树形结构存储,多棵树构成一个森林,每棵树构成一个集合,树中的每个节点就是该集合的元素,找一个代表元素作为该树(集合)的祖先。
并查集支持以下三种操作:
1、Make_Set(x) 把每一个元素初始化为一个集合
初始化后每一个元素的父亲节点是它本身,每一个元素的祖先节点也是它本身。
int father[1200],rank[1200],sum;
void makeset()
{
int i;
for(i=0; i<1200; i++)
{
father[i]=i;
rank[i]=1;
}
}
2、Find_Set(x) 查找一个元素所在的集合
查找一个元素所在的集合,只要找到这个元素所在集合的祖先即可。判断两个元素是否属于同一集合,只要看他们所在集合的祖先是否相同即可。
int find(int x)
{
return father[x] != x ? father[x]=find(father[x]) : x;//状态压缩、
}
3、Union(x,y) 合并x,y所在的两个集合
合并两个不相交集合操作很简单:首先设置一个数组Father[x],表示x的"父亲"的编号。那么,合并两个不相交集合的方法就是,找到其中一个集合的祖先,将另外一个集合的祖先指向它。
void Union(int x,int y)
{
x=find(x);
y=find(y);
if(x==y)
return;
if(rank[x]>rank[y])
{
father[y]=x;
rank[x]+=rank[y];
}
else
{
father[x]=y;
rank[y]+=rank[x];
}
}
并查集的优化
1、find(x)时 路径压缩
寻找祖先时我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度,有没有办法减小这个复杂度呢?
答案是肯定的,这就是路径压缩,即当我们经过"递推"找到祖先节点后,"回归"的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了。
2、Union(x,y)时 按秩合并
即合并的时候将元素少的集合合并到元素多的集合中,这样合并之后树的高度会相对较小。
一道并查集的模板题目:NYOJ608畅通工程
4 2 1 3 4 3 3 3 1 2 1 3 2 3 5 2 1 2 3 5 999 0 0
1 0 2 998
#include
int father[1200],runk[1200];
void makeset() //初始化
{
for(int i=0;i<1200;i++){
father[i]=i;runk[i]=1;
}
}
int find(int x) //查找+状态压缩
{
return father[x] != x ? father[x]=find(father[x]) : x;//状态压缩、
}
int Union(int x,int y,int ans) //合并
{
x=find(x);
y=find(y);
if(x==y)
return ans;
else if(runk[x]>runk[y])
{
father[y]=x;
runk[x]+=runk[y];
ans++;
}
else{
father[x]=y;
runk[y]+=runk[x];
ans++;
}
return ans;
}
int main()
{
int num_town,num_road,a,b;
while(~scanf("%d",&num_town)&&num_town)
{
scanf("%d",&num_road);
int ans=0;
makeset();
while(num_road--)
{
scanf("%d%d",&a,&b);
ans=Union(a,b,ans);
}
printf("%d\n",num_town-1-ans);
}
return 0;
}