操作
- 将两个集合合并
- 查询两个元素是否在同一个集合当中
- 时间复杂度, 接近O(1), 因为集合合并的过程需要一些时间
基本原理
每个集合用一棵树来进行表示。树根的编号就是整个集合的编号。每个结点存储它的父节点。p[x] 表示x的父结点。
问题
- 如何判断树根: if(p[x] == x)
- 如何求x的集合编号: while(p[x] != x) x = p[x]
- 如何合并两个集合: p[a] = b; 即可, 把a集合连接到以b为根的集合中,其中p[a]是集合a的编号, p[b]是集合b的编号
实例
对于这个集合,如果要找到结点f的根节点,那么需要一直往上找。按照f->e->d->c->b->a这个流程,查找次数等于集合树的高度。 如何优化?
路径压缩优化
使用递归的方式,找到根节点直接,递归返回根节点,让整个路径上的结点都直接指向根节点。 代码是非常的简洁的,不理解找个简单的例子去走一遍逻辑就知道了。
int find(int a){
if(p[a] != a) p[a] = find(p[a]);
return p[a];
}
例题
AcWing 836. 合并集合
一共有 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≤10^5
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
Code:
import java.util.*;
import java.lang.*;
import java.io.*;
public class Main{
static int [] p = new int[100010];
public static int find(int x){
if(x != p[x]) p[x] = find(p[x]);
return p[x];
}
public static void main(String [] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
int n, m;
String [] a = br.readLine().split(" ");
n = Integer.parseInt(a[0]);
for(int i = 1; i <= n;i++) p[i] = i;
m = Integer.parseInt(a[1]);
for(int i = 0 ; i < m;i++){
String [] s = br.readLine().split(" ");
int c = Integer.parseInt(s[1]), b = Integer.parseInt(s[2]);
if(s[0].equals("M")){
// 找到根节点
int pc = find(c);
int pb = find(b);
if(pc != pb){
p[pc] = pb;
}
}else {
int pc = find(c);
int pb = find(b);
// 两个根节点相同说明在一个集合
if(pc!= pb){
bw.write("No" + "\n");
}else bw.write("Yes" +"\n");
}
}
bw.flush();
br.close();
bw.close();
}
}
AcWing 837. 连通块中点的数量
给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。
现在要进行 m 个操作,操作共有三种:
C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;Q2 a,询问点 a 所在连通块中点的数量;
输入格式
第一行输入整数 n 和 m。
接下来 m 行,每行包含一个操作指令,指令为 C a b,Q1 a b 或 Q2 a 中的一种。
输出格式
对于每个询问指令 Q1 a b,如果 a 和 b 在同一个连通块中,则输出 Yes,否则输出 No。
对于每个询问指令 Q2 a,输出一个整数表示点 a 所在连通块中点的数量
每个结果占一行。
数据范围
1≤n,m≤105
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
Code:
import java.util.*;
import java.lang.*;
import java.io.*;
public class Main{
static int [] p = new int[100010];
// 维护每个结合的点数
static int [] size = new int[100010];
public static int find(int x){
if(x != p[x]) p[x] = find(p[x]);
return p[x];
}
public static void main(String [] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
int n, m;
String [] a = br.readLine().split(" ");
n = Integer.parseInt(a[0]);
for(int i = 1; i <= n;i++) {
p[i] = i;
}
// 初始每个集合的点数都只有自己,也就是1,我们只需要保证根节点的size有意义就可以,每次查根节点,因为我们每次插入集合都是直接插入到根结点下,更新数量就好了
Arrays.fill(size, 1);
m = Integer.parseInt(a[1]);
for(int i = 0 ; i < m;i++){
String [] s = br.readLine().split(" ");
int c = Integer.parseInt(s[1]);
if(s[0].equals("C")){
int b = Integer.parseInt(s[2]);
int pc = find(c);
int pb = find(b);
if(pc != pb){
p[pc] = pb;
size[pb] += size[pc];
}
}else if(s[0].equals("Q1")){
int b = Integer.parseInt(s[2]);
int pc = find(c);
int pb = find(b);
if(pc != pb){
bw.write("No" + "\n");
}else bw.write("Yes" +"\n");
}else {
int pc = find(c);
bw.write(size[pc] + "\n");
}
}
bw.flush();
br.close();
bw.close();
}
}
AcWing 240. 食物链
动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。
A 吃 B,B 吃 C,C 吃 A。
现有 N 个动物,以 1∼N 编号。
每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N 个动物所构成的食物链关系进行描述:
第一种说法是 1 X Y,表示 X 和 Y 是同类。
第二种说法是 2 X Y,表示 X 吃 Y。
此人对 N个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
- 当前的话与前面的某些真的话冲突,就是假话;
- 当前的话中 X 或 Y 比 N 大,就是假话;
- 当前的话表示 X 吃 X,就是假话。
你的任务是根据给定的 N 和 K 句话,输出假话的总数。
输入格式
第一行是两个整数 N 和 K,以一个空格分隔。
以下 K 行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中 D 表示说法的种类。
若 D=1,则表示 X 和 Y 是同类。
若 D=2,则表示 X 吃 Y。
输出格式
只有一个整数,表示假话的数目。
数据范围
1≤N≤50000,
0≤K≤100000
输入样例:
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
输出样例:
3
解法1:带扩展域的并查集
- 规定在带扩展域的并查集当中 x不再只表示一个值,而是一个事件
- 规定 x为 "事件x的同类"
- 规定 x + N 为"事件x的捕食类"
- 规定 x + 2 * N 为 "事件x的天敌类"
p[find(x)] = find(y)表示 事件x为A类动物和事件y为A类动物可以同时发生 so:
-
X 与 Y 为同种动物 等价于 下面三种都在同一类
-
p[ find(X) ] = find(Y); // x和y的同类在一起 p[ find(X + N)] = find(Y + N); // x和y的捕食类在一起 p[ find(X + N * 2)] = find(Y + N * 2); // x和y的天敌也在一起 -
p[find(X)] = find(Y + N) 表示 事件 X 为 A 类动物 和 事件 Y 为 B 类动物 同时发生
-
X 吃 Y 等价于
-
p[ find(X + N) ] = find(Y); // Y同类中加入X的捕食类 p[ find(X)] = find(Y + N * 2); // y的天敌类中加入 p[ find(X + N * 2)] = find(Y + N); // Y的捕食类的捕食类是X的天敌类
Code1: 带扩展域的并查集。
import java.util.Scanner;
public class Main {
static final int N = 50010;
static final int M = N * 3;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int m = scanner.nextInt();
int[] p = new int[M];
for (int i = 1; i < M; i++) {
p[i] = i;
}
int res = 0;
while (m-- > 0) {
int d = scanner.nextInt();
int a = scanner.nextInt();
int b = scanner.nextInt();
// a 或者b大于n是假话
if (a > n || b > n) {
res++;
continue;
}
if (a == b) {
// a和b是同类的情况下, 如果说a吃b的话就是假话
if (d == 2) {
res++;
}
continue;
}
if (d == 1) {
// 如果 b的捕食类有a或者a的捕食类有b就是假话
if (find(p, a) == find(p, b + N) || find(p, a + N) == find(p, b)) {
res++;
continue;
}
// 是同类的话就更新几种状态
p[find(p, a)] = find(p, b);
p[find(p, a + N)] = find(p, b + N);
p[find(p, a + N * 2)] = find(p, b + N * 2);
} else {
// 如果b的捕食类有a或者a和b是同类 ,则 a 吃 b 是假话
if (find(p, a + N) == find(p, b) || find(p, a) == find(p, b)) {
res++;
continue;
}
p[find(p, a)] = find(p, b + N);
p[find(p, a + N)] = find(p, b + N * 2);
p[find(p, a + N * 2)] = find(p, b);
}
}
System.out.println(res);
}
static int find(int[] p, int x) {
if (p[x] != x) {
p[x] = find(p, p[x]);
}
return p[x];
}
}
解法2:带边权的并查集
import java.util.Scanner;
public class Main {
static final int N = 50010;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int k = scanner.nextInt();
int[] p = new int[N];
//d数组保存的是到根结点的距离,初始到自己的距离为0
int[] d = new int[N];
int res = 0;
for (int i = 0; i < n; i++) {
p[i] = i;
}
while (k-- > 0) {
int c = scanner.nextInt();
int x = scanner.nextInt();
int y = scanner.nextInt();
int px = find(p, x, d); // px, py分别储存根节点
int py = find(p, y, d);
if (x > n || y > n) {
res++;
} else if (c == 1) {
if (px == py && (d[x] - d[y]) % 3 != 0) { // X和y在一颗树上,但是x和y不是同类,表示说的是假话
res++;
}
if (px != py) { // 如果不在一颗树上但是是同类,更新px和py之间的距离
p[px] = py;
d[px] = d[y] - d[x]; // 如果把x的根连到y的根上的话,x到py的距离 设为k,那么d[x] + k 和d[y]模三都为0,所以
// k = d[y] - d[x];
}
} else if (c == 2) {
if (px == py && (d[x] - d[y] - 1) % 3 != 0) {
res++;
// X和y在一颗树上,但是x不能吃y,表示说的是假话
//为什么-1,因为距离差1吗,那么在模的运算上位距离之差-1再取模运算
}
if (px != py) {
p[px] = py;
d[px] = d[y] + 1 - d[x];
// 如果把x的根连到y的根上的话,x到py的距离 设为k,那么d[x] + k 和d[y]模三都为0,所以 k = d[y] - d[x] + 1;
}
}
}
System.out.println(res);
}
static int find(int[] p, int x, int[] d) {
if (p[x] != x) {
int t = find(p, p[x], d); // 开始递归;最终用t储存根节点
d[x] += d[p[x]]; // 在递归过程中求得每个点到根节点的距离
p[x] = t; // 将每个点的父节点指向根节点
}
return p[x]; // 返回父节点
}
}
参考
[目目目大佬](AcWing 240. 食物链(带扩展域 和 带边权 并查集 两种做法) - AcWing)和[秦淮大佬](AcWing 240. 食物链 - AcWing)对并查集的理解