并查集从超时到满分:4种迭代写法与性能优化终极复盘

46 阅读9分钟

在算法刷题的江湖里,有一个简单却极易翻车的经典数据结构——并查集。

它专门解决「集合连通性」问题,比如经典的朋友圈好友判断、网络节点连通、最小生成树判环等场景。看似逻辑通俗易懂,但很多同学写出来的代码要么部分用例超时,要么大数据直接TLE,死活拿不到满分。

今天我们就以经典的「朋友圈连通性判断」真题为载体,从零复盘4版并查集迭代写法:从最原始的暴力实现,到逐步优化,最后解锁竞赛最优解。全程带原理图解、完整可运行代码、真实评测数据对比,帮你彻底吃透并查集的优化核心!

Bilibili 同步视频

并查集从超时到满分:4种迭代写法与性能优化终极复盘

一、前置场景:朋友圈连通性真题解析📌

1. 题目需求

给定 n 个人、m 次操作,所有人默认互不认识,每次操作分为两种类型:

  • 操作1(合并):1 b c,让 b、c 两人成为好友(合并两个集合)

  • 操作2(查询):2 b c,判断 b、c 是否在同一个朋友圈(判断集合连通)

查询连通则输出 YES,不连通则输出 NO。

2. 样例推演(6人场景)

初始状态:6个人各自独立,互不连通

操作流程:合并1、2 → 查询1、3 → 合并2、4 → 合并4、3 → 查询1、3

结果:第一次查询无交集输出 NO,全部合并后二次查询连通输出 YES。

这道题看似简单,却能完美测出不同并查集写法的性能短板,下面我们逐一代入算法迭代优化!

二、版本1:Quick-Find 暴力查找版(入门小白款)

1. 核心原理

Quick-Find 是最朴素的并查集实现思路,核心逻辑:用数组直接记录每个节点的根节点编号。

查找根节点时,直接读取数组值,时间复杂度 O(1),查找速度极快,这也是它「Quick-Find」名字的由来。

但致命短板在合并操作:每次合并两个集合,需要遍历全部节点,批量修改同集合节点的根标识,数据量大时效率直接崩盘。

2. 原理示意图(Plain Text)

# 初始状态 n=6
parent = [0,1,2,3,4,5,6]

# 合并1、2后:所有根为2的节点改为根1
parent = [0,1,1,3,4,5,6]

# 核心痛点:合并需要全局遍历,节点越多,耗时越爆炸

3. 完整C++代码

#include <iostream>
using namespace std;

const int N = 100010;
int parent[N];

// 初始化:每个节点的根是自己
void init(int n) {
    for(int i = 1; i <= n; i++) parent[i] = i;
}

// 查找根节点:O(1)极速查找
int find(int x) {
    return parent[x];
}

// 合并集合:暴力遍历,效率极低
void unite(int x, int y) {
    int fx = find(x), fy = find(y);
    if(fx == fy) return;
    // 遍历所有节点,修改根节点
    for(int i = 1; i <= 100000; i++) {
        if(parent[i] == fy) parent[i] = fx;
    }
}

int main() {
    // IO优化,避免输入输出超时
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n, m;
    cin >> n >> m;
    init(n);
    while(m--) {
        int a, b, c;
        cin >> a >> b >> c;
        if(a == 1) unite(b, c);
        else {
            if(find(b) == find(c)) cout << "YESn";
            else cout << "NOn";
        }
    }
    return 0;
}

4. 真实评测表现

✅ 优点:查找逻辑极简、新手好理解、单次查询速度快

❌ 缺点:合并操作暴力遍历,时间复杂度 O(n)

🏆 评测结果:90分,大数据用例TLE超时,总运行耗时1788ms

三、版本2:朴素 Quick-Union 树形版(反向翻车款)

1. 核心原理

为了解决 Quick-Find 合并过慢的问题,Quick-Union 放弃了「全局根标识」,改用树形存储结构:数组记录每个节点的父节点,而非直接记录根节点。

合并时无需全局遍历,只需要将一棵树的根节点挂载到另一棵树的根上,合并速度大幅提升。

但新的问题来了:树会退化成长链表!如果合并顺序不合理,树形结构会变成一条长链,查找根节点需要逐层遍历,效率比 Quick-Find 还要拉胯。

2. 原理示意图(Plain Text)

# 树形结构存储,parent[x]代表x的父节点
初始:1-6各自为根
合并1、2:parent[2] = 1
合并1、3:parent[3] = 1
# 最坏情况退化:1<-2<-3<-4<-5<-6,查找6的根需要遍历5次

3. 完整C++代码

#include <iostream>
using namespace std;

const int N = 100010;
int parent[N];

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

// 逐层查找根节点
int find(int x) {
    if(parent[x] != x) x = parent[x];
    return x;
}

// 朴素合并:直接挂载根节点,无任何优化
void unite(int x, int y) {
    int fx = find(x), fy = find(y);
    if(fx != fy) parent[fy] = fx;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n, m;
    cin >> n >> m;
    init(n);
    while(m--) {
        int a, b, c;
        cin >> a >> b >> c;
        if(a == 1) unite(b, c);
        else cout << (find(b)==find(c) ? "YESn" : "NOn");
    }
    return 0;
}

4. 真实评测表现

✅ 优点:合并操作极简,无需全局遍历

❌ 缺点:树形极易退化,查询效率暴跌

🏆 评测结果:仅50分,性能全面落后Quick-Find,堪称反向优化典范

四、版本3:按秩优化 Quick-Union(控树高优化款)

1. 核心优化思想

朴素 Quick-Union 的核心bug是「树高低失控」,为了稳住树形结构,我们引入按大小(秩)优化:

新增 size 数组,记录每棵树的节点总数。合并时小树挂大树,永远不让短树变长树,从根源限制树的高度,避免链表退化。

2. 原理示意图(Plain Text)

# size数组记录树节点数量
树A:根1,节点数3
树B:根4,节点数2

# 优化规则:小树挂大树
将树B根4挂载到树A根1下,树高始终保持最小,不会超长

3. 完整C++代码

#include <iostream>
using namespace std;

const int N = 100010;
int parent[N];
int sz[N]; // 记录每棵树的节点数量

void init(int n) {
    for(int i = 1; i <= n; i++) {
        parent[i] = i;
        sz[i] = 1; // 初始每个树只有1个节点
    }
}

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

// 按大小优化合并:小树嫁大树
void unite(int x, int y) {
    int fx = find(x), fy = find(y);
    if(fx == fy) return;
    // 小集合合并到大集合
    if(sz[fx] < sz[fy]) swap(fx, fy);
    parent[fy] = fx;
    sz[fx] += sz[fy];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n, m;
    cin >> n >> m;
    init(n);
    while(m--) {
        int a, b, c;
        cin >> a >> b >> c;
        if(a == 1) unite(b, c);
        else cout << (find(b)==find(c) ? "YESn" : "NOn");
    }
    return 0;
}

4. 真实评测表现

✅ 优点:严格控制树高,杜绝链表退化,性能大幅提升

❌ 缺点:仍存在层级冗余,查询次数多了依然会耗时

🏆 评测结果:70分,相比朴素版提升明显,但仍无法满分

五、版本4:按秩+路径压缩(满分进阶款)

1. 封神优化:路径压缩(扁平化管理)

这里给大家引入并查集最核心、最无敌的优化思想——路径压缩,可以用企业管理的思路通俗理解:

并查集的树形层级毫无意义,我们只需要知道「节点的根是谁」,不需要中间层层父节点。就像企业扁平化管理:所有员工直接对接CEO,跳过所有中层领导,效率拉满!

路径压缩的核心逻辑:在 find 查找根节点的过程中,把路径上所有节点直接挂载到根节点上,一次查询,永久扁平化。

2. 原理示意图(Plain Text)

# 优化前:层级繁琐
1(根) <- 2 <- 3 <- 4

# 查询4的根后,路径压缩扁平化
1(根)、2、3、4 全部直接指向根1
# 后续所有查询都是O(1)极速响应

3. 完整C++代码(双优化叠加)

#include <iostream>
using namespace std;

const int N = 100010;
int parent[N];
int sz[N];

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

// 带路径压缩的查找
int find(int x) {
    if(parent[x] != x) {
        parent[x] = find(parent[x]); // 递归路径压缩,直接挂根
    }
    return parent[x];
}

// 按秩优化合并
void unite(int x, int y) {
    int fx = find(x), fy = find(y);
    if(fx == fy) return;
    if(sz[fx] < sz[fy]) swap(fx, fy);
    parent[fy] = fx;
    sz[fx] += sz[fy];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n, m;
    cin >> n >> m;
    init(n);
    while(m--) {
        int a, b, c;
        cin >> a >> b >> c;
        if(a == 1) unite(b, c);
        else cout << (find(b)==find(c) ? "YESn" : "NOn");
    }
    return 0;
}

4. 真实评测表现

🏆 评测结果:满分AC,运行耗时仅720ms,性能碾压前三个版本

✅ 双重优化:树高可控 + 路径扁平化,几乎达到常数级时间复杂度

六、版本5:仅路径压缩(竞赛最简最优解🔥)

1. 核心结论

很多同学以为「按秩+路径压缩」是最强组合,但实测发现:单纯路径压缩的效率,和双优化几乎无差距!

更关键的是:路径压缩代码更短、逻辑更简单、不易出错,完全适配竞赛所有数据规模,这也是大佬们刷题的通用写法!

2. 最简AC代码

#include <iostream>
using namespace std;

const int N = 100010;
int parent[N];

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

// 纯路径压缩优化,最简核心
int find(int x) {
    if(parent[x] != x) parent[x] = find(parent[x]);
    return parent[x];
}

// 朴素合并,无需按秩
void unite(int x, int y) {
    int fx = find(x), fy = find(y);
    if(fx != fy) parent[fy] = fx;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n, m, a, b, c;
    cin >> n >> m;
    init(n);
    while(m--) {
        cin >> a >> b >> c;
        if(a == 1) unite(b, c);
        else cout << (find(b)==find(c) ? "YESn" : "NOn");
    }
    return 0;
}

七、五版并查集全方位性能对比📊

版本核心特点评测得分运行效率适用场景
Quick-Find查找快、合并暴力遍历90分极差(1788ms)新手入门理解原理
朴素Quick-Union树形存储、无优化50分最差不推荐使用
按秩优化小树嫁大树、控树高70分中等基础优化练习
按秩+路径压缩双重优化、极致稳定100分优秀(720ms)超大数据严苛场景
纯路径压缩代码极简、效率拉满100分优秀竞赛刷题通用最优解

八、最终刷题总结💡

  1. 不要用朴素版并查集:极易超时,纯属踩坑

  2. 路径压缩是性价比最高的优化:一行代码实现,性能提升翻倍,编码零负担

  3. 竞赛通用模板:直接背诵「纯路径压缩」版本,适配99%并查集题目

  4. 额外小技巧:代码开头加上 ios::sync_with_stdio(false);cin.tie(0);,解决cin/cout输入输出超时问题,刷题必备!

并查集从超时到满分:4种迭代写法与性能优化终极复盘

并查集的核心精髓从来不是复杂的逻辑,而是用极简的优化解决结构冗余问题。吃透这5个版本的迭代逻辑,从此告别并查集超时问题!