并查集(简单易懂)

一、适用场景

并查集是一种非常简单的数据结构,适合具有传递性关系的问题,尤其是连通性问题。

二、结构定义

1、并查集的实质是一颗森林,由多棵树组成,每一棵树代表一个集合

2、每个集合的代表元是这棵树的根节点

3、C++定义如下,一般采用静态数组存储:

class UnionFind {
    //父节点数组 pa[i] 表示节点 i 的父节点下标
    std::vector<int> pa;
    //大小数组 sizes[i] 表示以 i 作为根的集合内的元素个数
    std::vector<int> sizes;
    //连通分量数量,即互不相连的孤岛数量
    int c;
};

其中:

①pa:存放节点的父节点的下标

②sizes:存放树的大小

③c:森林中树的个数

三、使用

并查集的核心操作只有两个:查询和合并

1、查询find

//目的:找到节点x所在集合的根

//方法:递归向上寻找,x所在集合的根即其父节点所在集合的根(递归出口:x本身就是一个根)

//代码:

int Find(int x) {
    if (pa[x] == x) return x; // 根的父节点是本身
    return Find(pa[x]);
}

//路径压缩:找到x的根后可以直接把x嫁接到根上,拉近结点和根的距离,下次找就更快了。这种把查询结果利用起来的方式,就是路径压缩,代码如下:

// 查找一个节点的根节点
int Find(int x) {
    if (pa[x] == x) return x;
    //这个递归到底后才会再开始执行
    pa[x] = Find(pa[x]); // 重设 x 的父节点到根
    return pa[x];
}

这样,原本向上递归的路径上的所有节点都会直接嫁接在根下面去

经过路径压缩后,树会变得更加扁平,下次find会更加迅速 

2、合并union

//目的:合并两个节点a和b所在的集合

//方法:先用find分别找出两个节点的集合的根,然后把其中一个根的父节点指向另一个根

void Union(int a, int b) {
    pa[Find(b)] = Find(a);
}

当然有的时候,合并后的树会变得不平衡,但没关系,只要合并后属于同一个集合就可以了,我们只关心连通性。不过,有一种方法是把更小的树合并到更大的树上去,这样可以一定程度上控制有效性,代码如下:

void Union(int a, int b) {
    a = Find(a), b = Find(b);
    if (a == b) return; // 已经在同一集合
    if (sizes[a] > sizes[b]) { // 把 b 并入 a
        pa[b] = a;
        sizes[a] += sizes[b];
    } else {
        pa[a] = b;
        sizes[b] += sizes[a];
    }
    c--; // 连通分量减一
}

四、应用

最经典的模板题就是岛屿题,如:

200. 岛屿数量 - 力扣(LeetCode)

//把每个岛屿看作一个集合,那么这就是一个求连通分量的问题

//定义并查集中的节点符号

auto f = [=](int i, int j) { return i * n + j; };

f(i,j)的值就是代表方格(i,j)的节点符号,一共有m*n个

//接着,只用顺序扫描整个方格,把1的格子合并到各自归属的集合中去

//由于是自左向右、自上向下扫描,所以只用合并左边和上边相邻的格子的集合

//最后,合并完成后,输出连通分量即可,代码如下:

// 省略 union-find 代码
class Solution {
   public:
    int numIslands(vector<vector<char>>& g) {
        int m = g.size(), n = g[0].size();

        //构造并查集
        UnionFind uf(m * n);

        // 映射到节点标号
        auto f = [=](int i, int j) { return i * n + j; };

        int k0 = 0; // 0 的方格数量,最后要剔除

        for (auto i = 0; i < m; i++) {
            for (auto j = 0; j < n; j++) {
                if (g[i][j] == '1') {
                    // 上
                    if (i > 0 && g[i - 1][j] == '1')
                        uf.Union(f(i, j), f(i - 1, j));
                    // 左
                    if (j > 0 && g[i][j - 1] == '1')
                        uf.Union(f(i, j), f(i, j - 1));
                } else
                    //这里并查集不合0
                    //所以每个0都看作一个独立的连通分量
                    //所以后面直接减去0的个数即可,不用水域相邻问题
                    k0++;
            }
        }

        return uf.C() - k0; // C() 返回并查集的连通分量
    }
};

//注意,最后要减去0的格子的数量

参考:

1、并查集简记 | 春水煎茶 (writings.sh)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值