并查集
- 将两个集合合并
- 询问两个元素是否在一个集合中
并查集就是在O(1)时间复杂度下支持以上两种操作
基本原理:每个集合用一棵树来表示,树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x] 表示x的父节点
问题1:如何判断树根:if(p[x] == x)
问题2:如何求x的集合编号:while(p[x] != x) x = p[x];
问题3:如何合并两个集合:px是x的集合编号,py是y的集合编号,让p[x] = y;
并查集的精髓优化:在第一次执行查询元素所属集合后,该集合内所有元素的父节点均修改为根节点,后续查询该集合内所有元素的所属集合即可达到O(1)速度。此优化常称为路径压缩。
模版题1
一共有 nn 个数,编号是 1∼n1∼n,最开始每个数各自在一个集合中。
现在要进行 mm 个操作,操作共有两种:
M a b
,将编号为 aa 和 bb 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;Q a b
,询问编号为 aa 和 bb 的两个数是否在同一个集合中;
输入格式
第一行输入整数 nn 和 mm。
接下来 mm 行,每行包含一个操作指令,指令为 M a b
或 Q a b
中的一种。
输出格式
对于每个询问指令 Q a b
,都要输出一个结果,如果 aa 和 bb 在同一集合内,则输出 Yes
,否则输出 No
。
每个结果占一行。
数据范围
1≤n,m≤1051≤n,m≤105
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
import java.util.*;
public class Main {
static final int N = 100010;
static int[] p = new int[N];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
for(int i = 1; i <= n; ++i) {
p[i] = i;
}
while(m-- > 0) {
String ops = sc.next();
if(ops.equals("M")) {
int a = sc.nextInt();
int b = sc.nextInt();
p[find(a)] = find(b);
} else {
int a = sc.nextInt();
int b = sc.nextInt();
if(find(a) == find(b)) {
System.out.println("Yes");
} else {
System.out.println("No");
}
}
}
}
// 查询元素x的祖宗节点 + 路径压缩
public static int find(int x) {
if(p[x] != x) {
p[x] = find(p[x]);
}
return p[x];
}
}
一般算法题中,不会考察单纯的并查集,而是需要维护一些额外信息,例如每个集合中元素的数量等
模版题2
给定一个包含 nn 个点(编号为 1∼n1∼n)的无向图,初始时图中没有边。
现在要进行 mm 个操作,操作共有三种:
C a b
,在点 aa 和点 bb 之间连一条边,aa 和 bb 可能相等;Q1 a b
,询问点 aa 和点 bb 是否在同一个连通块中,aa 和 bb 可能相等;Q2 a
,询问点 aa 所在连通块中点的数量;
输入格式
第一行输入整数 nn 和 mm。
接下来 mm 行,每行包含一个操作指令,指令为 C a b
,Q1 a b
或 Q2 a
中的一种。
输出格式
对于每个询问指令 Q1 a b
,如果 aa 和 bb 在同一个连通块中,则输出 Yes
,否则输出 No
。
对于每个询问指令 Q2 a
,输出一个整数表示点 aa 所在连通块中点的数量
每个结果占一行。
数据范围
1≤n,m≤1051≤n,m≤105
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
import java.util.*;
public class Main {
static final int N = 100010;
static int[] p = new int[N];
static int[] size = new int[N];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
for(int i = 1; i <= n; ++i) {
p[i] = i;
size[i] = 1;
}
while(m-- > 0) {
String ops = sc.next();
if(ops.equals("C")) {
int a = sc.nextInt();
int b = sc.nextInt();
if(find(a) == find(b)) continue; // 两个元素已在一个集合,不再合并
size[find(b)] += size[find(a)]; // 合并两个集合的size
p[find(a)] = p[find(b)]; // 一个集合的根指向另一个集合
} else if(ops.equals("Q1")) {
int a = sc.nextInt();
int b = sc.nextInt();
if(find(a) == find(b)) {
System.out.println("Yes");
} else {
System.out.println("No");
}
} else {
int a = sc.nextInt();
System.out.println(size[find(a)]);
}
}
}
// 查询元素x的祖宗节点 + 路径压缩
public static int find(int x) {
if(p[x] != x) {
p[x] = find(p[x]);
}
return p[x];
}
}
评论区