并查集

22 阅读3分钟

前言

并查集主要用于解决一些元素分组集合的问题。它管理一系列不相交的集合,顾名思义,并查集支持两种操作:

  • 合并(Merge):把两个不相交的集合合并为一个集合。
  • 查询(Find):查询两个元素是否在同一个集合中。

它可以两个节点在不在一个集合,也可以将两个节点添加到一个集合中

并查集的思路

并查集的重要思想在于,用集合中的一个元素代表集合。也可以理解为,每个集合都有自己的“代表元素”,下面的操作都是针对代表元素来进行的。

假设一开始有很多元素,他们自己本身就是自己的代表。

image.png

然后,下面进行集合合并。

image.png

最后合并成了下面这个样子

image.png

可以看出来,最后的集合结构就像是一棵树。

可以写出并查集的初始化代码:

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

我们用一个数组fa[]来存储每个元素的父节点,先将每个节点的父节点设置为自己本身。

查询代码:

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

合并代码:

inline void merge(int i, int j)
{
    fa[find(i)] = find(j);
}

上面的操作都是最原始最直接的,还有待优化。

路径压缩的原理:

如果只是单纯的合并,并查集的效率是比较低的,因为可能会出现下面的情况:

image.png 这样可能会形成一条长长的,树的深度会越来越大,随着链越来越长,我们想要从底部找到根节点会变得越来越难。

这种情况可以使用路径压缩的方法。既然我们只关心一个元素对应的根节点,那我们希望每个元素到根节点的路径尽可能短,最好只需要一步,像这样:

image.png

只要我们在查询的过程中,把沿途的每个节点的父节点都设为根节点即可。下一次再查询时,我们就可以省很多麻烦。 代码如下:

int find(int i)//寻找数值i的所在的那个树的根节点的值
    {
        if(i == father[i])//说明找到了
        return i;
        father[i] = find(father[i]);//否则,寻找他的父节点的根节点,直到找到根节点为止。这一步很关键,因为相当于路径压缩了。把寻找路径上的所有节点的父节点都设置成当前树的根节点
        return father[i];
        //return i == father[i]? i:(father[i] = find(father[i]));上面的代码可以简写为这一行
    }

但是这样做还不够。

因为我们只是在查询的时候才会进行路径压缩,由于路径压缩只在查询时进行,也只压缩一条路径,所以并查集最终的结构仍然可能是比较复杂的。

所以,我们还需要记录每个节点所对应的深度。

比如下图:

image.png

我们显然希望8能够合并到根节点7上去,而不是反过来。

因为不然的话会让树的深度越来越高,查询越来越慢。

所以我们需要按秩合并。 ,应该把简单的树往复杂的树上合并,而不是相反。因为这样合并后,到根节点距离变长的节点个数比较少。

初始化按秩合并:

void init(int size)
    {
        for(int i = 1;i <= size;i ++)
        {
            father[i] = i;//当前节点的父节点就是他自己
            deepth[i] = 1;//深度都是1
        }
    }

一开始的深度都是1.

合并(按秩合并)

void merge(int i,int j)//把i和j所在的树结合到一起
    {
        int fa_1 = find(i);
        int fa_2 = find(j);
        //这个时候合并很关键,因为还要进行deepth的更新
        if(deepth[fa_1] <= deepth[fa_2])
        {
            father[fa_1] = fa_2;//把深度少的那个树的根节点的新的根节点设置成深度大的那个根节点
        }else{
            father[fa_2] = fa_1;
        }
        //要注意更新deepth的值
        if(fa_1 != fa_2 && deepth[fa_1] == deepth[fa_2])//这里的思想很关键,只有这个时候才会更新deepth数组的值。首先,要保证不是同一棵树。然后,如果两个树的深度不相等的话也不用更新,因为永远都是小树合并到大树上面去,小树的根直接连到大树的根上去,此时大树根的深度根本没发生变化。
        deepth[fa_2] ++;//之所以是fa_2++,是因为上面的第一个if里面,相等的时候,我们让fa_2当了根节点
    }

典型例题:

684. 冗余连接 - 力扣(LeetCode)

image.png

完整代码:

class Solution {
public:
    //直接写vector<int>(1000)的话会有歧义,编译器不知道时候成员变量声明还是成员函数声明
    //vector<int>father = vector<int>(1001);//记录当前节点的父节点的值。当前节点的值就是节点所在的下标
    vector<int>father = vector<int>(1001);
    vector<int>deepth = vector<int>(1001);//记录以当前节点为根节点的树的深度,也就是层数
    vector<int> findRedundantConnection(vector<vector<int>>& edges) {
        int size = edges.size();
        init(size);
        vector<int>res(2);
        for(int i = 0;i < size;i ++)
        {
            int ai = edges[i][0];
            int bi = edges[i][1];
            if(find(ai) != find(bi))//说明不在同一棵树上
            {
                //就合并这不同的两棵树
                merge(ai,bi);
            }else{
                res = edges[i];
            }
        }
        return res;
    }
private:
    void init(int size)
    {
        for(int i = 1;i <= size;i ++)
        {
            father[i] = i;//当前节点的父节点就是他自己
            deepth[i] = 1;//深度都是1
        }
    }
    int find(int i)//寻找数值i的所在的那个树的根节点的值
    {
        if(i == father[i])//说明找到了
        return i;
        father[i] = find(father[i]);//否则,寻找他的父节点的根节点,直到找到根节点为止。这一步很关键,因为相当于路径压缩了。把寻找路径上的所有节点的父节点都设置成当前树的根节点
        return father[i];
        //return i == father[i]? i:(father[i] = find(father[i]));上面的代码可以简写为这一行
    }
    void merge(int i,int j)//把i和j所在的树结合到一起
    {
        int fa_1 = find(i);
        int fa_2 = find(j);
        //这个时候合并很关键,因为还要进行deepth的更新
        if(deepth[fa_1] <= deepth[fa_2])
        {
            father[fa_1] = fa_2;//把深度少的那个树的根节点的新的根节点设置成深度大的那个根节点
        }else{
            father[fa_2] = fa_1;
        }
        //要注意更新deepth的值
        if(fa_1 != fa_2 && deepth[fa_1] == deepth[fa_2])//这里的思想很关键,只有这个时候才会更新deepth数组的值。首先,要保证不是同一棵树。然后,如果两个树的深度不相等的话也不用更新,因为永远都是小树合并到大树上面去,小树的根直接连到大树的根上去,此时大树根的深度根本没发生变化。
        deepth[fa_2] ++;//之所以是fa_2++,是因为上面的第一个if里面,相等的时候,我们让fa_2当了根节点
    }
    bool isSame(int i,int j)//看看i和j是否在同一个树上
    {
        return find(i) == find(j);
    }
};