并查集
例题:首先在地图上给你若干个城镇,这些城镇都可以看作点,然后告诉你哪些对城镇之间是有道路直接相连的。最后要解决的是整幅图的连通性问题。比如随意给你两个点,让你判断它们是否连通,或者问你整幅图一共有几个连通分支,也就是被分成了几个互相独立的块。像畅通工程这题,问还需要修几条路,实质就是求有几个连通分支。如果是1个连通分支,说明整幅图上的点都连起来了,不用再修路了;如果是2个连通分支,则只要再修1条路,从两个分支中各选一个点,把它们连起来,那么所有的点都是连起来的了;如果是3个连通分支,则只要再修两条路……
算法描述:
1.用集合中的某个元素来代表这个集合,该元素称为集合的代表元素。
2.一个集合内的所有元素组织成以代表元素为根的树形结构
3.对于每一个元素pre[x]指向x在树形结构上的父亲亲节点。如果x是根节点,则令pre[x]=x
4.对于查找操作,假设需要确定x所在的集合也就是确定集合的代表元素。可以沿着pre[x]不断在树形结构中向上移动,直到到达根节点。
其中应该包含find和join两个函数,find函数是用来找一个集合里有没有某个元素,或者找两个元素在不在一个集合里。join函数是实现将两个不再一个集合里的元素合并在一个集合里。
路径压缩算法
通过将前导点直接设置为代表元素来减少寻找根的路径长度。平均复杂度为Ackerman函数的反函数(这个函数是啥我也不清楚,度娘就是这么说的),可粗略认为是一个常数。
用途
1.维护无向图的连通性。支持判断两个点是否在同一连通块内,和判断增加一条边是否会产生环。
2.用在求解最小生成树的克鲁斯卡尔算法里。
一般来说,一个并查集对应三个操作:
- 初始化
- 查找根结点函数
- 合并集合函数
1.初始化
就是将每个节点的前导点设置为自己,相当于每一个点都是一个独立的子集。
2.查找函数
就是找到pre指针的源头,可以把函数命名为find_pre,如果集合的pre等于集合的编号(即还没有被合并或者没有同类),那么自然返回自身编号。 如果不同(即经过合并操作后指针指向了源头(合并后选出的rank高的集合))那么就可以调用递归函数,如下面的代码:
//查找集合i(一个元素是一个集合)的源头(递归实现)
int Find_pre(int i)
{
//如果集合i的父亲是自己,说明自己就是源头,返回自己的标号
if(pre[i]==i)
return pre[i];
//否则查找集合i的父亲的源头
return Find_pre(pre[i]);
}
//while循环实现
int find(int x)
{
int r = x;
while(pre[r]!=r)
{
r = pre[r];
}
return r;
}
//个人感觉还是下面这种while循环的方法比较好。
3.合并函数
就是将两个代表元素不同的子集合并为同一集合,具体操作:因为代表元素的前导点是其自身,只要将两集合之一的代表元素设置为另一集合的代表元素,就可以实现合并了,但是具体要怎么合并还是要看具体题意。
void join(int x,int y)
{
int fx = find(x); //分别找到要合并元素的代表元素
int fy = find(y);
if(fx != fy) //看两个元素使不是属于一个集合
{
pre[fx] = fy; //不属于同一个集合就将一个集合的根元素的前驱元素改为另一个根
}
}
4.路径压缩
当每个集合的树的节点程线性排列的时候,查找效率十分低下,因此可以采用路径压缩算法来进行优化。所谓路径压缩就是将查找路径变短,就是将每个元素的前驱直接设置为根节点,这样查找起来一下就可以找到根节点。
//非递归方式进行路径压缩
int find(int x)
{
int k, j, r;
r = x;
while(r != parent[r]) //查找跟节点
r = parent[r]; //找到跟节点,用r记录下
k = x;
while(k != r) //非递归路径压缩操作
{
j = parent[k]; //用j暂存parent[k]的父节点
parent[k] = r; //parent[x]指向跟节点
k = j; //k移到父节点
}
return r; //返回根节点的值
}
//递归
int find(int x) //查找x元素所在的集合,回溯时压缩路径
{
if (x != parent[x])
{
parent[x] = find(parent[x]); //回溯时的压缩路径
} //从x结点搜索到祖先结点所经过的结点都指向该祖先结点
return parent[x];
}
递归的方式很好理解,就是直接用find函数找每个元素的根,找到之后直接把这个根设置为这个元素的根节点,但是递归的通病是占用资源太大,出题人稍微不善良就RE了。用非递归的方式还有一种方法也比较好理解,就是套我上面写的非递归的find函数来找元素的根,但是这样会找会重复查很多次容易TLE,我上面写的这种非递归的方法就会节省很多查找次数。每次可以找到一个集合的一支,将这一支上的全部节点压缩。
(盗取大佬的图)
例如这张图,可以将白面葫芦娃这一支上的三个非根节点路径压缩,然后再将仙子狗尾巴花这一支上的路径压缩,可以节省很多查找次数。