极其巧妙的并查集

《极其巧妙的并查集》

  处理有传递性关系的问题,可以使用「并查集」。发明并查集的人获得了图灵奖,也用并查集告诉了世人大道至简的真理。并查集的思路和代码极其简洁明了,但是却能让复杂的问题束手就擒。直观来说,并查集解决的问题是组团和配对的问题,判断两个个体是否在一个集合中(find),合并两个个体到一个集合(unite),以及并查集中有多少集合(count),解决的问题也相对比较固定,朋友圈,以图判树,岛屿个数…,而对于解决实际问题,也是非常得力的工具,比如我们在做猪脸识别自动建档的时候就用到了并查集。

Key Words:并查集、组团、配对


Beijing, 2020

Agile Pioneer  


并查集的逻辑

  1. init: 每个个体的parent都是其本身
  2. find: 查找个体的头目就是找到 x == parent[x] 的元素
  3. unite: 合并两个个体,先找到两个个体的集体头目,如果不在一个集体中,则合并两个集体头目即可
  4. count: 计算并查集中有多少个集体,就是查有多少个元素满足 x == parent[x]

最简单的并查集实现


class UnionFind
{
private:

vector<int> parent;
vector<int> rank;  // 后面优化会用到

public:

    UnionFind(int n):parent(n, -1)
    {
        for (int i = 0; i < n; ++i)
        {
            parent[i] = i;
        }
    }
    
    void unite(int x, int y)
    {
        int px = find(x);
        int py = find(y);
        if (px != py)
        {
            parent[px] = py;
        }
    }

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

    int count()
    {
        int res = 0;
        for (int i = 0; i < parent.size(); ++i)
        {
            res += (parent[i] == i);
        }
        return res;
    }

};


利用路径压缩优化 find 方法

基于循环的路径压缩优化

  相当于对路径进行了折叠,每个父节点都直接指向了父节点的父节点,如下图所示:

int find(int x)
{
   while(x != parent[x])
   {
       parent[x] = parent[parent[x]];
       x = parent[x];
   }
   return x;
}
基于递归的路径压缩优化

  由于是深度优先搜索,所以每次 find 相当于把所有的节点都直接指向了根节点,从父节点开始,每个节点都指向了根节点,如下图所示:

int find(int x)
{
    if (x == parent[x]) // 只有根节点满足条件
    {
        return x;
    } else {
        parent[x] = find(parent[x]);
        return parent[x];
    }
	
	// --- or --- 

	if (x != parent[x])
	{
		parent[x] = find(parent[x]);
    }
    return parent[x]; // 这里必须要返回 parent[x]
}

利用 rank 优化 unite 方法

  • rank 表示树(集合)的深度

  我们在合并集合的时候,有时候随机的合并可能会造成合并成了上面图的类似链表的样子,因为我们没有考虑到两个树(集合)的深度,一味的向一个树(集合)合并,那么就会导致find 方法的复杂度变高,而一个比较明智的选择是我利用rank来记录每个树(集合)的高度,利用如下策略来合并:

  1. 如果 集合A 的秩 > 集合B 的秩,那么由 A 牵头合并
  2. 如果 集合B 的秩 > 集合A 的秩,那么由 B 牵头合并
  3. 如果两个集合的秩相等,那么由 B牵头合并,然后 rank[B]++

可以在纸上画一画,感受一下为什么要这么做。

void unite(int x, int y)
{
    int px = find(x);
    int py = find(y);
    
    if (px != py)
    {
        if (rank[px] > rank[py])
        {
            parent[py] = px;
        } else if (rank[py] > rank[px]){
            parent[px] = py;
        } else {
            parent[px] = py;
            rank[py]++;
        }
    }
}

复杂度

  • 性能跟树的深度有关系,简称 O(h)

  • 并查集并不是一个二叉树,而是一个多叉树,所以并查集的查询和合并时间复杂度并不是O(log n)

  • 在加上rank和路径压缩优化后 ,并查集的时间复杂度为 O(log n)

参考

https://blog.csdn.net/weixin_40374341/article/details/93979529

https://blog.csdn.net/weixin_40374341/article/details/93979529

https://blog.csdn.net/yuzhiqiang666/article/details/80721436

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值