【算法基础】并查集

并查集

一、并查集的功能

  1. 将两个集合进行合并。
  2. 查询两个元素是否属于同一个集合。
  3. 将两个元素并到成一个集合。

以基本 O(1) 的时间复杂度实现上面两个操作

二、并查集的实现

2.1 实现的基本原理

并查集基本使用用树来维护的,用一个 P 数组来维护每个节点的父节点

  • 每个独立的集合都是一棵树,在树中,树根的编号就是集合的编号

  • 每个节点存的都是父节点; P[x] 存储 x 的父节点。

2.2 如何判断树根、求得编号

  • 如何判断是不是树根

if(P[x]==x)

如果上述条件是满足的,就说明 x 这个点是树根,也就是集合的根节点。

  • 如何求得编号是多少

while(x!=p[x])x=P[x];

因为 P[x] 存储 x 的父节点。所以如果 x 当前不是树根(不是根节点),我们就一直往上找,找父节点就可以了。

  • 如何合并两个集合

此时,有 P[x]x 元素的集合编号,P[y]y 元素的集合编号,我们仅需要进行。

P[x]=y;

2.3 路径压缩

在并查集中,最费时间复杂度的一个操作是求得集合编号。

不带路径压缩的情况,是我们的左图情况,在左图中我们们想求得某个点的集合,需要一层一层上去,非常费时间。所以我们要把并查集进行路径压缩成右图的样子,即每个点的父节点就是祖宗节点。这样的话就是 O(1) 可以找到路径了。

三、并查集实现代码

3.0 并查集初始化

for(int i = 1; i <= n; i++) p[x] = i;
p = [x for x in range(n)]

3.1 判断根节点

if(x == p[x]) return true;
if x == p[x]: return True;

3.2 找元素的集合编号(带路径压缩)

int find(x){
	while(x != p[x])
		x = p[x] = p[p[x]];
	return x;
}
def find(x):
    while x != p[x]:
        p[x] = p[p[x]]
        x = p[p[x]]
    return x

# 递归写法
def find(x):
    if x != p[x]: p[x] = find(p[x]);
    return p[x]

3.3 判断两个元素是否在同一个集合里面

return find(x) == find(y);

3.4 合并两个元素

p[find(x)] = find(y);

3.5 Jiangly DSU板子

3.5.1 带维护集合数量的并查集

/**   并查集(DSU)
 *    2023-08-04: https://ac.nowcoder.com/acm/contest/view-submission?submissionId=63239142
**/
struct DSU {
    std::vector<int> f, siz;
    
    DSU() {}
    DSU(int n) {
        init(n);
    }
    
    void init(int n) {
        f.resize(n);
        std::iota(f.begin(), f.end(), 0);
        siz.assign(n, 1);
    }
    
    int find(int x) {
        while (x != f[x]) {
            x = f[x] = f[f[x]];
        }
        return x;
    }
    
    bool same(int x, int y) {
        return find(x) == find(y);
    }
    
    bool merge(int x, int y) {
        x = find(x);
        y = find(y);
        if (x == y) {
            return false;
        }
        siz[x] += siz[y];
        f[y] = x;
        return true;
    }
    
    int size(int x) {
        return siz[find(x)];
    }
};

3.5.2 带删除的并查集 (DSU With Rollback)

/**   可撤销并查集(DSU With Rollback)
 *    2024-09-17: https://qoj.ac/submission/569639
**/
struct DSU {
    std::vector<int> siz;
    std::vector<int> f;
    std::vector<std::array<int, 2>> his;
    
    DSU(int n) : siz(n + 1, 1), f(n + 1) {
        std::iota(f.begin(), f.end(), 0);
    }
    
    int find(int x) {
        while (f[x] != x) {
            x = f[x];
        }
        return x;
    }
    
    bool merge(int x, int y) {
        x = find(x);
        y = find(y);
        if (x == y) {
            return false;
        }
        if (siz[x] < siz[y]) {
            std::swap(x, y);
        }
        his.push_back({x, y});
        siz[x] += siz[y];
        f[y] = x;
        return true;
    }
    
    int time() {
        return his.size();
    }
    
    void revert(int tm) {
        while (his.size() > tm) {
            auto [x, y] = his.back();
            his.pop_back();
            f[y] = y;
            siz[x] -= siz[y];
        }
    }
};

四、并查集维护一些值

在并查集的习题中不一定只会问你存不存在、是不是一个集合,通常要并查集再维护一些值。

  • 集合中最大值
  • 集合中节点数量
  • 集合最小值.......

这里一般都是在合并集合的时候做操作,我们此处以个数为例。

在合并集合的时候,大家肯定会知道,把 A 集合合并到 B 集合中,无疑就是 Cntnew=CntA+CntB

如果我们开个 Cnt 数组表示集合中元素的数量的话,在合并的时候也无疑是

cnt[A]+=cnt[B]

但在此之前,我们需要判断,假如 AB 已经是一个集合了,就不能加了。故完整的代码为:

bool merge(int x, int y) {
    x = find(x);
    y = find(y);
    if (x == y) {
        return false;
    }
    siz[x] += siz[y];
    f[y] = x;
    return true;
}

可见,在哥哥的板子中,他已经都封装好了带集合数量的维护。

def merge(x, y):
    x = find(x)
    y = find(y)
    if x == y: return False
    cnt[x] += cnt[y]  # 将 y 合并到 x 集合中
    f[y] = x;
    return True
posted @   wxzcch  阅读(301)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示