并查集总结

并查集

原题:杭电hdu1232畅通工程

例题:首先在地图上给你若干个城镇,这些城镇都可以看作点,然后告诉你哪些对城镇之间是有道路直接相连的。最后要解决的是整幅图的连通性问题。比如随意给你两个点,让你判断它们是否连通,或者问你整幅图一共有几个连通分支,也就是被分成了几个互相独立的块。像畅通工程这题,问还需要修几条路,实质就是求有几个连通分支。如果是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. 查找根结点函数
  3. 合并集合函数

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,我上面写的这种非递归的方法就会节省很多查找次数。每次可以找到一个集合的一支,将这一支上的全部节点压缩。

 

(盗取大佬的图)

例如这张图,可以将白面葫芦娃这一支上的三个非根节点路径压缩,然后再将仙子狗尾巴花这一支上的路径压缩,可以节省很多查找次数。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值