并查集(Disjoint Set Union,DSU)详解

 由DS倾情奉献,由CJ详细修改

 

 

并查集:
对集合进行合并merge、查找find两个操作
f[i]:i所在的集合代表是谁(i所在学校的校长是谁/i的上级/i的爹/i的父亲/i的祖先)

 

初始化f[i] = i : 每个人是自己的上级

 

合并merge(x,y):合并x和y所在的集合,可能是x合并到y,也可能是y合并到x
俗称:x认y为爹,或y认x为爹

 

查询find(x):查询x的上级是谁
朴素查询:逐级往上找上级,好处是可以清晰的知道层级关系
坏处:慢,O(N)

 

路径压缩:通过一次查询,改变f[i]等于整个集合的祖先(最高上级)
好处是,第一次O(N),接下来都是O(1)
坏处是,只知道祖先是谁,不知道爹是谁,忘本
扩展题:权值并查集:在忘本之前维护一些数据

 

性质:执行完n个点,m条边的并查集后
可以通过f[i] == i的条件数量判断n个点被区分成了几个集合

 

 

 

 

 

 

 

 

 

一、基本概念

并查集是一种树型数据结构,用于处理不相交集合的合并与查询问题。它支持以下两种操作:

  • Find:查询元素所属集合

  • Union:合并两个集合

核心思想

通过维护一个父节点数组,使得:

  • 同一集合的元素最终指向同一个根节点

  • 不同集合的元素的根节点不同

二、基础实现

1. 初始化

const int N = 1e5 + 10;
int f[N]; // 父节点数组

void init(int n) {
    for(int i = 1; i <= n; i++) 
        f[i] = i; // 初始时每个元素自成一集合
}

 

2. 查找(Find)

// 普通查找(无路径压缩)
int find(int x) {
    while(f[x] != x)
        x = f[x];
    return x;
}

// 带路径压缩的查找
int find(int x) {
    if(f[x] != x)
        f[x] = find(f[x]); // 路径压缩
    return f[x];
}

3. 合并(Union)

void merge(int x, int y) {
    int fx = find(x), fy = find(y);
    if(fx != fy)
        f[fy] = fx; // 将fy的父节点设为fx
}

三、优化技巧

1. 路径压缩

在查找时将节点直接指向根节点,使树更扁平

2. 按秩合并

int rank[N]; // 秩数组

void merge(int x, int y) {
    int fx = find(x), fy = find(y);
    if(fx != fy) {
        if(rank[fx] > rank[fy])
            f[fy] = fx;
        else {
            f[fx] = fy;
            if(rank[fx] == rank[fy])
                rank[fy]++;
        }
    }
}

四、扩展应用

1. 带权并查集

维护节点到父节点的距离信息

int f[N], d[N]; // d[i]表示i到f[i]的距离

int find(int x) {
    if(f[x] != x) {
        int root = find(f[x]);
        d[x] += d[f[x]]; // 维护距离
        f[x] = root;
    }
    return f[x];
}

2. 种类并查集(扩展域)

通过扩大域表示多种关系

        if(find(x) != find(y)){ //x和y是敌人 
            merge(x,y + n);  //x和y的敌人(y+n)是朋友 
            merge(y,x + n);  //y和x的敌人(x+n)是朋友 
        }

五、典型例题解析

例题1:银河英雄传说(带权并查集)

问题:维护战舰队列,支持合并和查询两战舰间距离

解决方案

int f[N], size[N], d[N]; // size记录集合大小,d记录到父节点距离

int find(int x) {
    if(f[x] != x) {
        int root = find(f[x]);
        d[x] += d[f[x]]; // 维护距离
        f[x] = root;
    }
    return f[x];
}

void merge(int x, int y) {
    int fx = find(x), fy = find(y);
    if(fx != fy) {
        f[fx] = fy;
        d[fx] = size[fy]; // 新距离为fy集合大小
        size[fy] += size[fx];
    }
}

例题2:关押罪犯(种类并查集)

问题:将罪犯分到两个监狱,使最大冲突最小

解决方案

for(int i = 1; i <= 2 * n; i++) f[i] = i; //1~n敌人,n+1~2n朋友 
for(int i = 1; i <= m; i++) {
    int x = t[i].x, y = t[i].y;
    if(find(x) == find(y)) { // 冲突不可避免
        cout << t[i].z;
        return 0;
    }
    merge(x, y + n); // x和y必须分开
    merge(y, x + n);
}
  • 1~n:表示罪犯在监狱A的状态

  • n+1~2n:表示同一个罪犯在监狱B的"镜像状态"(不是简单的朋友/敌人关系)

六、复杂度分析

操作普通实现路径压缩路径压缩+按秩合并
Find O(n) O(α(n)) O(α(n))
Union O(n) O(α(n)) O(α(n))

其中α(n)是反阿克曼函数,在可预见的n范围内不超过4

七、应用场景

  1. 连通性问题

  2. 图的动态连通性

  3. 分组问题

  4. 最近公共祖先(离线算法)

  5. 关系推理(敌对、友好等)

八、模板代码

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;  // 定义最大节点数量
int f[N];  // 并查集父节点数组

// 初始化并查集
void init(int n) {
    for(int i = 1; i <= n; i++) {
        f[i] = i;  // 初始时每个节点都是自己的父节点
    }
}

// 查找操作(带路径压缩优化)
int find(int x) {
    if(f[x] != x)  // 如果x不是根节点
        f[x] = find(f[x]);  // 路径压缩:将x直接指向根节点
    return f[x];  // 返回根节点
}

// 合并操作
void merge(int x, int y) {
    int fx = find(x);  // 找到x的根节点
    int fy = find(y);  // 找到y的根节点
    if(fx != fy) {     // 如果不在同一个集合
        f[fx] = fy;    // 将fx的父节点设为fy
    }
}

/*
 * 基础并查集实现说明:
 * 1. 只有路径压缩优化,没有按秩合并
 * 2. 适用于大多数不需要严格平衡树的场景
 * 3. 均摊时间复杂度接近O(1)
 */

九、练习题推荐

  1. P3367 【模板】并查集

  2. P1551 亲戚

  3. P1196 [NOI2002]银河英雄传说

  4. P1525 [NOIP2010]关押罪犯

  5. P2024 [NOI2001]食物链

掌握并查集的关键在于理解其树型结构和路径压缩原理,并熟练应用带权和种类扩展等高级技巧。

 

posted @ 2025-04-23 18:51  CRt0729  阅读(256)  评论(0)    收藏  举报