算法学习笔记(一) 学不会的并查集

并查集被认为是最简洁而优雅的数据结构之一,并查集明明这么简洁而优雅,但我却学了这么久。。主要用于解决一些元素分组的问题。它管理一系列不相交的集合,并支持两种操作:

合并(Union)查询(Find)

合并:把两个不相关的元素集合合并为一个集合。

查询:查询两个元素是否在同一个集合。

我们用图来形象说明并查集的原理。

首先所有元素各自在一个集合(自身是自身的父结点)

然后2 和3并入到1所属的集合(2和3把1当作父结点)

 接着4和5并入到6所属的集合(4和5把6当作父结点)

 

此时原先的6个集合变成了两个分别以1为父结点和6为父结点的集合。

此时我们再把6的父结点变为1

 

此时 6是4 5的父结点 1是2 3 6的父结点     1是2 3 4 5 6的根结点 1的结点是它本身。这时1 2 3 4 5 6在一个集合内。 

按照这个思路 可以写出简单版本的并查集代码。

初始化

int F[maxn];
void init(int n)
{
    for(int i=1;i<=n;i++)
        fa[i]=i;//每个元素的父结点是自身
}

先将每个元素的父结点设为自己。

int find(int x)
{
    if(F[x]==x)
        return x;
    else 
        return find(F[x]);
}

通过递归的方式实现对元素的查询 :一层层访问父结点直至根节点(根节点的特点是父结点是他本身)这样可以判断两个元素是否在同一个集合。

void Union(int i,int j)
{
    F[find(i)]=find[j];//将i节点的父结点变为j结点的父(根)结点
}

这样的并查集 由于查找通过递归的方式进行是很浪费时间 效率非常低的,因此要采取路径压缩

路径压缩

先举个例子来说明路径压缩的效果

 我们现在union(2,3)通过Find(2);1将连接到3即F[1]=3,于是

 再来一个元素4  union(2,4):

 会先从2找到1,再找到3,然后让F[3]=4;变成上图所示。

如果元素变多  会形成一条长长的链,会增加每次Find的时间。

而如果采用路径压缩,我们可以将每个元素只指向它的根节点 就像这样:

 这样每次查找就是一步到位省去很多时间。

int Find(int x)
{
    if(x==F[x])
        return x;
    else {
        F[x]=Find(F[x]);
        return F[x];
        }
}

按秩合并

现在 ,假设我们有一个复杂的树和一个单元素要和合并 ,我们选择union(7,8),是将7设为节点更好还是8设为节点更好?

显然,将7作为8的节点更好,因为如果让8作为7的节点,会增加树的深度,这样查找就会耗时,就算使用路径压缩也是要浪费时间的。

这表明,在合并中,要尽可能将简单的树合并到复杂的树上,防止树的深度过长消耗不必要的时间。

我们用一个数组rank[]来记录根结点对应的树的深度(默认初始为1)。

这里要注意的是  路径压缩和秩一起用的时间复杂度接近O(n) ,但路径压缩会破坏rank的准确性使得树的结构被破坏。

初始化

void init(int n)
{
    for(int i=1;i<=n;i++)
    {
        F[i]=i;
        rank[i]=i;
    }
}

合并

void Union(int i,int j)
{
    int x=find(i),y=find(j);
    if(rank[x]<=rank[y])
        F[x]=y;
    else 
        F[y]=x;
    if(rank[x]==rank[y]&&x!=y)
        rank[y]++;                //如果深度相同但根节点不同  深度+1
}

例题

P1621 集合

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<cstdlib>
#include<cmath>
#define maxn 100010
using namespace std;
int f[maxn];
int a,b,p,ans;
bool prime[maxn];
int vis[maxn];
int cnt;
int find(int x)
{
    if(f[x]==x) return x;
    else
    {
        f[x]=find(f[x]);
        return f[x];
    }
}
int Union(int x,int y)
{
    int t1=find(x),t2=find(y);
    if(t1!=t2)
    {
        f[t2]=t1;
        return 1;
    }
    return 0;
}
int make_prime()      //普通筛 
{
    memset(prime,1,sizeof(prime));
    int k=sqrt(b);
    prime[0]=prime[1]=0;
    for(int i=2;i<=k;i++)
        if(prime[i])
            for(int j=2*i;j<maxn;j+=i) prime[j]=0;
}
int main()
{
    cin>>a>>b>>p;
    for(int i=a;i<=b;i++) f[i]=i;
    make_prime();
    for(int i=p;i<=b;i++)      //找出p~b之间的素数 
        if(prime[i]) vis[++cnt]=i;  //记录 
    for(int i=1;i<=cnt;i++)     //找出a~b之间符合条件的数,合并 
    {
        int cc=0;
        while(cc*vis[i]<a) cc++;     //确保是a~b之间的,不要超范围,不然后面没法统计 
        while(vis[i]*(cc+1)<=b)
        {
            Union(vis[i]*cc,vis[i]*(cc+1));     //合并 
            cc++;
        }
    }
    for(int i=a;i<=b;i++)
        if(f[i]==i) ans++;     //统计个数 
    cout<<ans<<endl;
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值