zoukankan      html  css  js  c++  java
  • 并查集

    介绍

    我们遇到一些有 \(n\) 个元素的集合应用问题中,当给出两个元素的一个无序对 \((a,b)\) 时,需要快速合并 \(a\)\(b\) 分别所在的集合,并查集就是这样的用于处理分离集合的抽象数据类型。它的作用就是动态地维护和处理集合元素之间的复杂关系。

    操作

    使用并查集应首先记录一组分离的动态集合 \(S=\{S_1,S_2,...,S_k\}\),每个集合还要设置一个代表来识别,代表只要选择该集合中的某个元素(成员)即可,集合中的哪个元素被选作代表是无所谓的。并查集有三种操作:初始化、查找与合并。

    这里的并查集采用树实现。树结构用静态数组模拟,设 fa[x] 表示 \(x\) 元素所指向的父亲。

    1. 初始化

    建立一个新的集合,其中仅有的成员是 \(x\),同时 \(x\) 也是代表。因为各集合是分离的,所以要求 \(x\) 没有在其他集合中出现过。使用并查集之前都需要执行一次初始化操作。树结构并查集初始化操作的实现很简单,只需要执行 fa[x]=x 即可。

    for(int i=1;i<=n;++i) fa[i]=i;
    
    2. 查找

    查找一个元素所在的集合,这个操作返回该集合的代表,也就是树根。查找操作是并查集的核心操作,也是优化并查集效率的重点。

    对于树结构,查找时需要以 \(x\) 为基点,向上寻找它的父结点,再以父节点为基点,向上寻找父节点的父节点……直到找到根结点为止。

    路径压缩:在查找一个结点所在树的根结点的过程中,一条从待查结点到根结点的路径。我们不妨让这些路径上的结点直接指向根结点,即作为根结点的子节点。这样,这些路径上的结点仍在分离集合中,整棵树仍然满足分离集合树的要求,但是路径上结点的深度就减小了。这就是路径压缩。

    ll fin(ll x)//路径压缩
    {
        if(fa[x]!=x) fa[x]=fin(fa[x]);
        return fa[x];
        
        /* 上下两种写法等价
        if(fa[x]==x) return x;
        else return fa[x]=fin(fa[x]);
        */
    }
    

    我们看到,查找过程是一个递归的过程,实现非常简洁。同时,我们的方法也可以保证递归深度不会很深。

    3. 合并

    合并即把两个集合合并成一个集合。一般来说,在不同的实现中通常都以 \(S_x\) 或者 \(S_y\) 的代表作为新集合的代表。合并之前先判断两个元素是否属于同一集合,这个可以通过查找来实现。

    void merge(ll x,ll y)
    {
        x=find(x);
        y=find(y);
        fa[y]=x;
    }
    

    按秩合并:使得树拥有比较小的深度。我们可以用结点数(称为“秩”,rank)代替树的深度进行比较。

    void merge(ll x,ll y) //按秩合并
    {
        x=find(x);
        y=find(y);
        if(x==y) return;
        if(ran[x]<ran[y])
            fa[x]=y;
        else
        {
            fa[y]=x;
            if(ran[x]==ran[y])
                ran[x]++;
        }
    }
    

    如果将路径压缩和按秩合并一起使用,则可以证明,\(n\) 次查找最多需要用 \(O(n*\alpha(n))\) 的时间,其中 \(\alpha(n)\) 是单变量阿克曼函数的逆函数,它是一个增长速度比 \(\log_2n\) 慢得多但又不是常数的函数。在一般情况下 \(\alpha(n) \leq 4\),可以当作常数。

    相关习题

    1. 亲戚

    luoguP1551(数据弱)

    一本通1346(数据强)

    这道题是一个并查集的模板题,对于较弱的数据,查找过程中只需要用路径压缩即可。对于较强的数据,既要路径压缩还要按秩合并,甚至还需要快读和卡常优化(不是)

    对于一本通的题目描述,Code:

    #include<头文件>
    using namespace std;
    typedef long long ll;
    
    inline ll read(){
        快读;
    }
    
    ll ans=0;
    ll fa[50002],ran[50002];
    
    void start(ll n)//初始化
    {
        for(int i=1;i<=n;++i)
        {
            fa[i]=i;
            ran[i]=0;
        }
    }
    
    ll fin(ll x)//查找-路径压缩
    {
        if(fa[x]!=x) fa[x]=fin(fa[x]);
        return fa[x];
        // if(fa[x]==x) return x;
        // else return fa[x]=fin(fa[x]);
    }
    
    void merge(ll x,ll y)//按秩合并
    {
        if(x==y) return;
        if(ran[x]<ran[y])
            fa[x]=y;
        else
        {
            fa[y]=x;
            if(ran[x]==ran[y])
                ran[x]++;
        }
    }
    
    int main()
    {
        ll n,m,p;
        n=read();
        m=read();
        start(n); //初始化
        for(int i=1;i<=m;++i) //合并
        {
            ll x,y;
            x=read();
            y=read();
            ll fax=fin(x);
            ll fay=fin(y);
            merge(fax,fay);
        }
        p=read();
        for(int i=1;i<=p;++i) //询问
        {
            ll x,y;
            x=read();
            y=read();
            if( fin(x) == fin(y) )
                puts("Yes");
            else puts("No");
        }
        return 0;
    }
    
    
    2. 家谱

    luoguP2814

    一本通1388

    巧妙使用STL的 map,把结点和它的父亲直接连接起来即可。

    Code:

    #include<头文件>
    using namespace std;
    typedef long long ll;
    
    inline ll read()
    
    map<string,string> fa;
    
    string fin(string x) //路径压缩
    {
        if(x!=fa[x]) fa[x]=fin(fa[x]);
        return fa[x];
    }
    
    string s,s1;
    
    int main()
    {
        char ch;
        cin>>ch;
        while(ch!='$')
        {
            cin>>s;
            if(ch=='#')
            {
                s1=s;
                if(fa[s]=="") //自己的父亲是自己(没有修改过)
                fa[s]=s; //设为真正的自己
                //否则不更改
            }
            else if(ch=='+')
                fa[s]=s1;
            else
                cout<<s<<" "<<fin(s)<<endl;
            cin>>ch;
        }    
        return 0;
    }
    
    3. 食物链

    luoguP2024

    一本通1390

    本题中一共有3种动物,我们给每个动物一个权值,同一类的动物对于3取余。

    我们发现这样一个特性:如果知道A与B的关系、B与C的关系,那么可以推出A和C的关系。也就是说。对于已知相互关系的动物集合 \(S_1,S_2\),我们只要直到其中 \(a\) 属于 \(S_1\)\(b\) 属于 \(S_2\) ,那么 \(S_1,S_2\) 中任意两个元素的关系就可以推出来了。由此,我们可以使用带权的并查集来维护它们之间的关系。

    Code:

    #include <头文件>
    using namespace std;
    typedef long long ll;
    
    inline ll read()
    
    ll n, k, x, y, d, ans;
    ll fa[100002], ran[100002];
    
    void start(int n) //初始化
    {
        for (int i = 1; i <= n; ++i)
            fa[i] = i;
    }
    
    ll fin(ll x) //查找+路径压缩
    {
        if (x != fa[x])
        {
            ll tmp = fa[x];
            fa[x] = fin(fa[x]);
            ran[x] = (ran[x] + ran[tmp]) % 3;
        }
        return fa[x];
    }
    
    int main()
    {
        n = read();
        k = read();
        start(n); //初始化
    
        for (int i = 1; i <= k; ++i)
        {
            d = read();
            x = read();
            y = read();
            if (x > n or y > n) //第一种不符合条件的情况
            {
                ++ans;
                continue;
            }
            if (d == 1)
            {
                if (fin(x) == fin(y))
                {
                    if (ran[x] != ran[y]) 
                        ++ans;
                }
                else
                {
                    {
                        ran[fa[x]] = (ran[y] - ran[x] + 3) % 3;
                        fa[fa[x]] = fa[y];
                    }
                }
            }
            else
            {
                if (x == y) //自己吃自己
                {
                    ++ans;
                    continue;
                }
                if (fin(x) == fin(y))
                {
                    if (ran[x] != (ran[y] + 1) % 3) //与前面冲突
                        ++ans;
                }
                else
                {
                    ran[fa[x]] = (ran[y] - ran[x] + 4) % 3;
                    fa[fa[x]] = fa[y];
                }
            }
        }
        
        printf("%lld\n", ans);
        return 0;
    }
    

    EdisonBa

    2021.2.2 初次编辑

  • 相关阅读:
    windows10使用arcgis注意事项
    andriod arcgis加载影像TIF
    FeatureTable()
    arcgis Listview
    ArcGIS for Android图层记录数,图层选择记录,图层字段数
    ArcGIS for Android 点击选择
    ArcGIS for Android地图上实际距离与对应的屏幕像素值计算
    Android如何检查对象的类型
    北美内地电影票房总排行榜
    XSS漏洞的渗透利用另类玩法
  • 原文地址:https://www.cnblogs.com/EdisonBa/p/14361173.html
Copyright © 2011-2022 走看看