• 并查集


    介绍

    我们遇到一些有 \(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 初次编辑

  • 相关阅读:
    Android游戏开发22:Android动画的实现J2me游戏类库用于Android开发
    android sqlite SQLiteDatabase 操作大全 不看后悔!必收藏!看后精通SQLITE (第三部分,完整代码)
    使用OGR创建dxf格式矢量数据
    mysql 数据库引擎 MyISAM InnoDB 大比拼 区别
    android sqlite SQLiteDatabase 操作大全 不看后悔!必收藏!看后精通SQLITE (第二部分)
    mysql 更改数据库引擎
    android sqlite SQLiteDatabase 操作大全 不看后悔!必收藏!看后精通SQLITE (第一部分)
    android 数字键盘使用
    MySQL Innodb数据库性能实践
    eclipse : Error while performing database login with the driver null
  • 原文地址:https://www.cnblogs.com/EdisonBa/p/14361173.html
Copyright © 2020-2023  润新知