四个示例搞懂并查集(Java)
并查集简介
并查集适用的问题:元素分组问题
并查集操作:
- 合并(Merge),一般用于把两个集合合并,集合用一个树状结构表示,最终汇集到一个根节点;
- 查询(Find),查询两个节点是否具有相同的根节点。
并查集的两个优化操作:
- 路径压缩:在查询的同时,通过
parent[i] = find(parent[i]);
进行路径压缩,避免了树结构的重复遍历,直接将节点的父节点化为根节点,提高查询效率。 - 秩优化:合并过程中为了使树尽可能平衡,引入秩数组减小根节点左右子树高度差。在开启路径压缩时,秩优化对性能提升有限且会增加编码复杂度,一般不做。
示例1(LeetCode 1319 连通网络的操作次数)
用以太网线缆将 n 台计算机连接成一个网络,计算机的编号从 0 到 n-1。线缆用 connections 表示,其中 connections[i] = [a, b] 连接了计算机 a 和 b。
网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其他任意一台计算机。
给你这个计算机网络的初始布线 connections,你可以拔开任意两台直连计算机之间的线缆,并用它连接一对未直连的计算机。请你计算并返回使所有计算机都连通所需的最少操作次数。如果不可能,则返回 -1 。
示例 1:
输入:n = 4, connections = [[0,1],[0,2],[1,2]]输出:1解释:拔下计算机 1 和 2 之间的线缆,并将它插到计算机 1 和 3 上。
示例 2:
输入:n = 6, connections = [[0,1],[0,2],[0,3],[1,2],[1,3]]输出:2
示例 3:
输入:n = 6, connections = [[0,1],[0,2],[0,3],[1,2]]输出:-1解释:线缆数量不足。
示例 4:
输入:n = 5, connections = [[0,1],[0,2],[3,4],[2,3]]输出:0
提示:1 <= n <= 10^51 <= connections.length <= min(n*(n-1)/2, 10^5)connections[i].length == 20 <= connections[i][0], connections[i][1] < nconnections[i][0] != connections[i][1]没有重复的连接。两台计算机不会通过多条线缆连接。
思路:
实际上题目只需要在利用并查集合并后判断最后的树的数量即可。若初始布线数不足则直接返回-1,否则返回将各个树连接起来需要的边(树的数量-1)即可。
class Solution {
int[] parent;
int[] rank;
public int makeConnected(int n, int[][] connections) {
int num = connections.length;
if (num < n-1) {
return -1;
}
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 1;
}
for (int[] connection : connections) {
int x = connection[0];
int y = connection[1];
if (find(x) != find(y)) {
union(x, y);
}
}
Set<Integer> set = new HashSet<>();
for (int i = 0; i < n; i++) {
set.add(find(i));
}
return set.size()-1;
}
private int find (int i) {
if (i == parent[i]) {
return i;
}
parent[i] = find(parent[i]);
return parent[i];
}
private void union (int i, int j) {
int x = find(i);
int y = find(j);
if (x == y) {
return;
}
if (rank[x] > rank[y]) {
parent[y] = x;
}else if (rank[y] >= rank[x]) {
parent[x] = y;
}
if (rank[y] == rank[x]) {
rank[y]++;
}
}
}
示例2(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
思路:这题是最常规的并查集的题目,只需用并查集的思想将该合并的进行合并即可。
代码:
import java.util.*;
class Main{
public static int[] parent;
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
parent = new int[n+1];
for(int i = 1; i <= n; i++){
parent[i] = i;
}
int m = scan.nextInt();
List<String> res= new ArrayList<>();
for(int i = 0; i < m; i++){
String temp = scan.next();
int j = scan.nextInt();
int k = scan.nextInt();
if("M".equals(temp)){
merge(j,k);
}else{
if(getParent(j)==getParent(k)){
res.add("Yes");
}else{
res.add("No");
}
}
}
for(String s : res){
System.out.println(s);
}
}
public static int getParent(int i){
if(i==parent[i]){
return i;
}else{
parent[i] = getParent(parent[i]);
return parent[i];
}
}
public static void merge(int i, int j){
int x = getParent(i);
int y = getParent(j);
parent[y] = x;
}
}
示例3(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≤10^5
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
思路:基本思路和示例2一致,不同之处在于维护一个权值数组存储并查集中每个根节点所在树的节点个数即可。更新过程也很简单。
代码:
import java.util.*;
class Main{
public static int[] parent;
public static int[] nums;
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
parent = new int[n+1];
nums = new int[n+1];
for(int i = 1; i <= n ;i++){
parent[i] = i;
nums[i] = 1;
}
int m = scan.nextInt();
List<String> res = new ArrayList<String>();
for(int i = 0; i < m ; i++){
String order = scan.next();
if("C".equals(order)){
int j = scan.nextInt();
int k = scan.nextInt();
merge(j,k);
}else if("Q1".equals(order)){
int j = scan.nextInt();
int k = scan.nextInt();
if(find(j)==find(k)){
res.add("Yes");
}else{
res.add("No");
}
}else{
int j = scan.nextInt();
res.add(String.valueOf(nums[find(j)]));
}
}
for(String s: res){
System.out.println(s);
}
}
public static int find(int i){
if(i==parent[i]){
return i;
}else{
parent[i] = find(parent[i]);
return parent[i];
}
}
public static void merge(int i, int j){
int x = find(i);
int y = find(j);
if(x!=y){
parent[y] = x;
nums[x] += nums[y];
}
}
}
示例4(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
思路:这题首先要理解清楚题目的意思,题目规定了只有三种生物组成的食物链,也即一条食物链是不会包含4种即以上的生物的。最初就因为没理解清这个条件用了两个并查集分别存储相同的和处在一条食物链里的,而实际上这样是没办法解决此题的。
因为食物链只有三种生物组成且必构成环,所以可以维护一个带权的并查集,对每一个节点存储一个该节点到根节点的距离,可以推导出一条路径上距离相邻的三个节点必构成一个食物链。也即对距离d而言,将其对3取余:
- 余1:可以被根节点吃
- 余2:可以吃根节点
- 余0:与根节点是同类
此外,在维护距离数组d时,要注意:
- d数组初始化为0,表示当前节点到父节点的距离
- 在find操作中维护d,核心在于d[i] += d[parent[i]]; 仍然是做路径压缩,不同之处在于更新d[i]为到根节点的距离。
- order==1,i和j是同类,其根节点分别为x和y,此时合并操作用parent[y] = x即可,但由于i,j同类,有最终(d[i]+d[y]-d[j])%3==0,所以更新d[y] = d[j]-d[i],d[j]后续会在find更新(代码中的merge1)
- order==2,i吃j,其根节点分别为x和y,此时合并操作用parent[y] = x即可,但由于i吃j,有最终(d[i]-d[j]+1-d[y])%3==0,所以更新d[y] = d[j]-d[i],d[j]后续会在find更新(代码中的merge2)
答案代码:
import java.util.*;
class Main{
static int[] parent;
static int[] d;
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
parent = new int[n+1];
d = new int[n+1];
for(int i = 1; i <= n; i++){
parent[i] = i;
}
int res = 0;
int k = scan.nextInt();
for(int i = 0; i < k; i++){
int order = scan.nextInt();
int x = scan.nextInt();
int y = scan.nextInt();
if(x>n||y>n){
res++;
continue;
}
if(order == 1){
if(x!=y){
if(find(x)!=find(y)){
merge1(x, y);
}else{
if((d[x]-d[y])%3!=0)
res++;
}
}
}else{
if(x==y){
res++;
}else{
if(find(x)!=find(y)){
merge2(x, y);
}else{
if((d[y]-d[x]-1)%3!=0)
res++;
}
}
}
}
System.out.println(res);
}
public static int find(int i){
if(i==parent[i]){
return i;
}else{
int temp = find(parent[i]);
d[i] += d[parent[i]];
parent[i] = temp;
return parent[i];
}
}
public static void merge1(int i, int j){
int x = find(i);
int y = find(j);
if(x!=y){
parent[y] = x;
d[y]=d[i]-d[j];
}
}
public static void merge2(int i, int j){
int x = find(i);
int y = find(j);
if(x!=y){
parent[y] = x;
d[y]=d[i]-d[j]+1;
}
}
}