Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6
1359 字
7 分钟
并查集

1. 引言部分#

  • 痛点分析

    • 若无,则难以判断元素是否在同一集合下
  • 价值阐述

    • 能够元素是否在同一集合中,记录集合大小等数据,若是有属性联系,还可以加上带权并查集,得到点与点的联系(比如 A 与 B点的奇偶性,A吃B等等)
    • 将几个零散的点合并成一个点,简化问题
  • 应用场景

    • 三元关系中说谎话 A->B, B->C, C->A
    • A B之间奇偶关系

引言#

在算法竞赛和工程开发中,我们经常遇到这样的问题,题目给出A,B,C,D,让我们去维护它们之间的关系 传统的暴力解法需要O(n²)的时间,在数据量大时完全不可行。 这时候用并查集的思想,将所有相同类型的数据合并即可(这里的思想有点像缩点 缩边) 只用处理祖先元素即可

2. 基本概念解释#

什么是并查集?#

想象一下,有一个衣架,下面挂着很多很多衣服,同时挂在同一个衣架上有一个特性,都是一个人穿的衣服,这样就是我们并查集模型,我们能直接通过拿去衣架,知道,该衣架下的属性(衣服个数,属于谁等等)

定义#

并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。

3. 核心操作详解#

每个操作包含:

  • 算法步骤

    • 这里假定 A 和 B 两者链接:
    • 首先得得到 A 和 B的祖先 PA 和 PB
    • 再更新 PA 和 PB 之间的关系(通过find路径压缩也会同时更新在原先祖先节点为PA的所有点)
    • 最后将 PA的祖先指向 PB 即形成链接
  • 复杂度分析

    • 有路径压缩的原因:查找基本上在O(1)左右
    • 空间复杂度在 O(n)
  • 示例说明

    • 问 A 和 B 是否在同一集合,只用查询 两个人的祖先是否同一个
    • 即:find(a) == find(b) ? 1 : 0;

4. 完整代码实现#

  • 本题以食物链为原型,详细题解查看食物链
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5e4 + 10;
int n, m;
//p代表 并查集, d代表该子节点 与 根节点的距离
//根据题目意思 A B C 三者必定存在回想制约的关系
//所以 可以设 距离 余0 是相同 余 1 是吃根 余而 2是 被根吃
int p[N], d[N];
//查找的同时 并更新状态
int find(int x)
{
if(x != p[x])
{
int t = find(p[x]);
//因为 根节点发生转变, 先添加此时父亲节点 的值 再连接到根节点
d[x] += d[p[x]];
//再将 x 的父节点 转变为 新查找到的 父节点
p[x] = t;
}
return p[x];
}
int main()
{
scanf("%d%d", &n , &m);
for(int i = 1; i <= n; i++) p[i] = i;
int res = 0;
while(m --)
{
int t ,x, y;
scanf("%d%d%d", &t, &x, &y);
int px = find(x), py = find(y);
//将两个 根节点查找
if(x > n || y > n) res++;
else
{
if(t == 1)
{
//如果相等 则 判断 是否 不等
if(px == py && (d[x] - d[y]) % 3) res++;
//如果不等
else if(px != py)
{
//将 x 的根 连接到 p 的根
p[px] = p[y];
// 加入某个值 使得 整体效果不变
//又因为 x 和 y 同族 所以 d[x] + ? = d[y] (mod 3);
d[px] = d[y] - d[x];
}
}
else
{
if(px == py && (d[x] - d[y] - 1) % 3) res ++;
else if(px != py)
{
p[px] = p[y];
//余 1 吃
//同理 此时 d[x] - 1 + ? = d[y] (mod 3)
d[px] = d[y] + 1 - d[x];
}
}
}
}
printf("%d\n",res);
return 0;
}

5. 例题解析#

选择梯度式例题:

  1. 基础题

  2. 进阶题:食物链,奇偶游戏

6. 复杂度分析表#

操作时间复杂度空间复杂度备注
初始祖先O(n)O(n)初始祖先指向自己
单点更新O(1)-递归深度

7. 实战技巧#

  • 常见错误及调试方法
    • 对于带权并查集中的find函数使用需注意:优先更新祖先的节点,再将跟新完后的节点反馈给自己,比如
    //查找的同时 并更新状态

int find(int x) { if(x != p[x]) { int t = find(p[x]); //因为 根节点发生转变, 先添加此时父亲节点 的值 再连接到根节点 d[x] += d[p[x]]; //再将 x 的父节点 转变为 新查找到的 父节点 p[x] = t; } return p[x]; } ``` - 同时还需要注意:不能对祖先赋值完后,在没更新祖先的新祖先前,重复使用find函数,会导致d[x] 不断累加 例如:

if(s=='M'){
if(find(a)!=find(b)){
d[find(a)]=sum[find(b)];
sum[find(b)]+=sum[find(a)]; //每次find 都会累加
p[find(a)]=find(b);
}
}
  • 还有方向性, p[pa] = pb是让 pb成为pa新的祖先,若题目有要求则需要额外判定,如没有则无需

  • 模板代码整理

//带权
int find(int x)
{
if(x != p[x])
{
int t = find(p[x]);
//因为 根节点发生转变, 先添加此时父亲节点 的值 再连接到根节点
d[x] += d[p[x]];
//再将 x 的父节点 转变为 新查找到的 父节点
p[x] = t;
}
return p[x];
}
//不带权
int find(x){
if(x != p[x]) p[x] = find(p[x]);
return p[x];
}
并查集
https://blog.rookiesama.space/posts/算法/数据结构/并查集/
作者
Rookiesama
发布于
2026-01-22
许可协议
CC BY

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00