算法基础课—数据结构(六) 并查集
基本原理与相关操作
时间复杂度O(1)
基本原理:用树来维护集合
根节点的编号就是当前集合的编号
如果想知道每个元素属于哪个集合——找父节点直到根节点
代码的结构
- 一个一维数组p[N],用于存储其父节点
- 初始:所有p[N]指向自身,一个点为一个集合
- 集合合并:改变根节点的父节点
- 集合查询,找到根节点的位置
进一步优化——路径压缩
走第一遍后就把所有的都指向父节点
代码
所有p[x] 最后都指向祖宗节点
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
合并集合
题目
一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。
现在要进行 m 个操作,操作共有两种:
M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;
输入格式
第一行输入整数 n 和 m。
接下来 m 行,每行包含一个操作指令,指令为 M a b 或 Q a b 中的一种。
输出格式
对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
1≤n,m≤105
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
算法思想
用树来表示集合,集合的合并,就是树的合并——这样合并和查找操作都较为简单
一棵树即是一个集合
普通做法:
用p[N]来表示其父节点,其根节点就是集合编号,判断是否到根节点就是p[x] == x
优化做法:
遍历一次后,每个p[N]都直接指向其所在集合编号,省略向上遍历的过程。
初始条件:一开始的时候,每个数都为一个集合,p[x] = x。
集合合并:则其增加父节点,指向新的集合编号
集合查询,判断两个集合编号是否相等。
p[N]本质上是指向集合编号的数组,由于集合之间可能存在嵌套,所以需要向上遍历到最高点。
集合合并也是两个集合的合并,要找到其所属的最大集合进行合并。
优化就是省略了后续遍历的重复
代码
#include <iostream>
using namespace std;
const int N = 2e6;
int p[N];
int find(int x){
if(p[x] != x)
p[x] = find(p[x]);
return p[x];//返回祖宗节点,同时也让期间的所有点都指向祖宗节点
}
int main(){
int n,m,i;
int a,b;
char op[2];
cin>>n>>m;
for(i = 1;i <= n; i ++) p[i] = i;
for(i = 0; i < m; i ++){
cin>>op>>a>>b;
if(op[0