并查集
1.0 并查集概念
对于具有传递性质、联通集合的题目可以考虑并查集。
1.1 并查集模板
以下模板来自于
有n个数,编号是 1~n,最开始每个数各自在一个集合中,
现在要进行 m 个操作,操作共有两种:
1. M a b,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
2. 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
in: 4 5 M 1 2 M 3 4 Q 1 2 Q 1 3 Q 3 4 out: Yes No Yes
import java.util.Scanner; public class Main { static int N = 100010; static int[] p = new int[N]; static int n, m; static int find(int x) {// 路径压缩 if (p[x] != x) p[x] = find(p[x]); return p[x]; }
static void merge(int a, int b) { int pa = find(a), pb = find(b); if (pa != pb) { p[pa] = pb; } } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); n = scanner.nextInt(); m = scanner.nextInt(); for (int i = 1; i <= n; i++) p[i] = i; while (m-- > 0) { String ch = scanner.next(); int a = scanner.nextInt(); int b = scanner.nextInt(); if (ch.equals("M")) { merge(a, b); } else { if (find(a) == find(b)) System.out.println("Yes"); else System.out.println("No"); } } } }
// 不带路径压缩的find static int find(int x) { if (p[x] == x) { return x; // 如果当前元素是根节点,则直接返回 } else { return find(p[x]); // 否则递归调用 find 函数查找父节点的根节点 } }
离散化并查集模版
如果数据范围改为1 ≤n,m ≤ 109 应该如何处理?
import java.util.HashMap; import java.util.Scanner; public class Main {
//{x,p[x]} static HashMap<Integer, Integer> p = new HashMap<>(); static int n, m; static int find(int x) { if (!p.containsKey(x)) return x; p.put(x, find(p.get(x))); return p.get(x); } static void merge(int a, int b) { int pa = find(a), pb = find(b); if (pa != pb) { p.put(pa, pb); } } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); n = scanner.nextInt(); m = scanner.nextInt(); while (m-- > 0) { String ch = scanner.next(); int a = scanner.nextInt(); int b = scanner.nextInt(); if (ch.equals("M")) { merge(a, b); } else { if (find(a) == find(b)) System.out.println("Yes"); else System.out.println("No"); } } } }
Kruskal
算法的步骤:
-
定义数据结构:
- 定义边的结构体,包括边的起点、终点和权重。
- 定义并查集结构,包括
find
和union
操作。
-
构建图和边集:
- 将图表示为边集合,按照边的权重进行排序。
-
Kruskal 算法核心步骤:
- 初始化并查集,将每个顶点视为一个独立的集合。
- 按照边的权重从小到大遍历边集:
- 对于每条边
(u, v, w)
,判断顶点u
和v
是否属于同一个集合(使用并查集的find
操作): - 如果不属于同一个集合,则将它们合并(使用并查集的
union
操作),并将该边加入最小生成树的边集。
- 对于每条边
- 当最小生成树的边数达到
n-1
条时(n
为图的顶点数),停止算法。
-
输出最小生成树:
- 将最小生成树的边集合输出即可。
import java.util.*; public class KruskalMST { static class Edge implements Comparable<Edge> { int u, v, weight; Edge(int u, int v, int weight) { this.u = u; this.v = v; this.weight = weight; } @Override public int compareTo(Edge other) { return this.weight - other.weight; } } static class UnionFind { int[] parent; int[] rank; UnionFind(int n) { parent = new int[n]; rank = new int[n]; for (int i = 0; i < n; i++) { parent[i] = i; rank[i] = 0; } } int find(int u) { if (parent[u] != u) { parent[u] = find(parent[u]); } return parent[u]; } void union(int u, int v) { int rootU = find(u); int rootV = find(v); if (rootU != rootV) { if (rank[rootU] > rank[rootV]) { parent[rootV] = rootU; } else if (rank[rootU] < rank[rootV]) { parent[rootU] = rootV; } else { parent[rootV] = rootU; rank[rootU]++; } } } } public static List<Edge> kruskalMST(List<Edge> edges, int n) { // Step 1: Sort edges by weight Collections.sort(edges); // Step 2: Initialize Union-Find data structure UnionFind uf = new UnionFind(n); // Step 3: Process edges to construct MST List<Edge> mstEdges = new ArrayList<>(); for (Edge edge : edges) { int u = edge.u; int v = edge.v; int weight = edge.weight; if (uf.find(u) != uf.find(v)) { uf.union(u, v); mstEdges.add(edge); if (mstEdges.size() == n - 1) { break; // Found n-1 edges, MST is complete } } } return mstEdges; } public static void main(String[] args) { // Example usage List<Edge> edges = new ArrayList<>(); edges.add(new Edge(0, 1, 4)); edges.add(new Edge(0, 7, 8)); edges.add(new Edge(1, 2, 8)); edges.add(new Edge(1, 7, 11)); edges.add(new Edge(2, 3, 7)); edges.add(new Edge(2, 8, 2)); edges.add(new Edge(2, 5, 4)); edges.add(new Edge(3, 4, 9)); edges.add(new Edge(3, 5, 14)); edges.add(new Edge(4, 5, 10)); edges.add(new Edge(5, 6, 2)); edges.add(new Edge(6, 7, 1)); edges.add(new Edge(6, 8, 6)); edges.add(new Edge(7, 8, 7)); List<Edge> mst = kruskalMST(edges, 9); for (Edge edge : mst) { System.out.println(edge.u + " - " + edge.v + " : " + edge.weight); } } }
20240410华为实习第一场
题目描述
小明要根据图片的相似度对图片进行分类。他通过一个N*N的矩阵M来表示任意两张图片的相似度,其中M[i][j]表示第i张图片和第j张图片的相似度。根据以下规则判断图片的相似类:
1)如果M[i][j] > 0,则认为第i张图片和第j张图片相似。
2)若A和B相似,B和C相似,但A和B不相似,则A和C间接相似,可以将A、B、C归为一类,但不考虑AC的相似度。
3)若A与所有其他图片不相似,则A被归为自己的一类,相似度总和为0。
请按照相似类的相似度总和从大到小的顺序返回每个相似类中所有图片的相似度之和。
输入
第一行一个数N,代表矩阵M中有N个图片。下面跟着N行,每行有N列数据,空格分隔(为了显示整齐,空格可能为多个),代表N个图片之间的相似度。
约束:
1. 0<N<=900
2. 0<=M[i][j]<=100,输入保证M[i][j]=0,M[i][j]=M[j][i]
3. 输入的矩阵中分隔符为1个或连续多个空格
输出
每个相似类的相似度之和。格式为:一行数字,分隔符为1个空格
样例1
输入
5 0 0 50 0 0 0 0 0 25 0 50 0 0 0 15 0 25 0 0 0 0 0 15 0 0
输出
65 25
说明 把1~5看成A,B,C,D,E 矩阵显示,A和C相似度为50,C和E的相似度为15,B和D相似度为25。
划分出2个相似类,分别为 1.{A,C,E},相似度之和为65 2.{B,D},相似度之和25 排序输出相似度之和,结果为: 65 25
思路:并查集,遍历每个联通分量/2收集权值。
package com.coedes.union_and_find.huawei20240410; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; /** * @description:https://i.cnblogs.com/posts/edit;postId=18152280#postBody * @author: wenLiu * @create: 2024/4/23 12:25 */ public class Main { static final int N = 910; //p : parent[] v : 存储当前节点作为父节点子树的权重 static int[] p = new int[N], v = new int[N]; static int find(int x) { if (p[x] != x) return p[x] = find(p[x]); return x; } static void merge(int x, int y, int val) { int root_x = find(x); int root_y = find(y); if (root_y != root_x) { p[root_x] = root_y; v[root_y] = v[root_x] + val; } else { v[root_y] += val; } } static void init(int n) { for (int i = 0; i < n; i++) { p[i] = i; } } public static void main(String[] args) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); int n = Integer.parseInt(reader.readLine()); init(n); int[][] M = new int[n][n]; for (int i = 0; i < n; i++) { String[] s = reader.readLine().split(" "); for (int j = 0; j < s.length; j++) { int inputVal = Integer.parseInt(s[j]); M[i][j] = inputVal; if (inputVal > 0) { merge(i, j, inputVal); } } } ArrayList<Integer> res = new ArrayList<>(); for (int i = 0; i < n; i++) { if (p[i] == i) res.add(v[i] >> 1); } Collections.sort(res); for (int i = res.size()-1; i >= 0; i--) { if (i == res.size() - 1) { System.out.print(res.get(i)+" "); } else { System.out.print(res.get(i)); } } } }
2024美团-人际关系
题目描述:
事件共有2种:
1 u v:代表编号u的人和编号v的人淡忘了他们的朋友关系。
2 u v:代表查询编号u的人和编号v的人是否能通过朋友介绍互相认识. 注:介绍可以有多层,比如2号把1号介绍给3号,然后3号再把1号介绍给4号,这样1号和 4 号就认识了。
第一行输入三个正整数n,m,q,代表总人数,初始的朋友关系数量,发生的事件数量
接下来的m行,每行输入两个正整数u,v代表切始编号u的人和编号v的人是朋友关系
接下来的q行,每行输入三个正整数op,u,v,含义如题目描述所述,
1≤n≤10e9 //一般数组最多只能开到10^7 , 因此本题需要使用离散化并查集...
1≤m,q≤10e5 1≤u,v≤n 1≤op≤2 输出描述 对于每次2号操作,输出一行字符用代表查询的答案。 如果编号u的人和编号v的人能通过朋友介绍互相认识,则输出"Yes"。否则输出"No 样例 输入 5 3 5 1 2 2 3 4 5 1 1 5 2 1 3 2 1 4 1 1 2 2 1 3 输出 Yes No No
import java.util.*; public class Main { static Map<Integer, Integer> fa = new HashMap<>(); // 存储每个节点的父节点 static Set<Pair> fr = new HashSet<>(); // 存储朋友关系的集合 static List<Pair> qs = new ArrayList<>(); // 存储查询操作 static List<String> ans = new ArrayList<>(); // 存储查询结果 static class Pair { int first; int second; int third; Pair(int first, int second) { this.first = first; this.second = second; } Pair(int first, int second, int third) { this.first = first; this.second = second; this.third = third; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Pair pair = (Pair) o; return first == pair.first && second == pair.second && third == pair.third; } @Override public int hashCode() { return Objects.hash(first, second, third); } } // 并查集的查找操作,使用路径压缩优化 static int find(int x) { if (!fa.containsKey(x)) return x; // 如果 x 没有父节点,则 x 自成一个集合 fa.put(x, find(fa.get(x))); // 路径压缩,将 x 的父节点更新为根节点 return fa.get(x); } // 并查集的合并操作 static void merge(int x, int y) { x = find(x); y = find(y); if (x != y) { fa.put(x, y); // 将 x 的根节点指向 y 的根节点,实现集合合并 } } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int n = scanner.nextInt(); // 总人数 int m = scanner.nextInt(); // 初始朋友关系数 int q = scanner.nextInt(); // 查询操作数 // 读取初始朋友关系 for (int i = 0; i < m; i++) { int u = scanner.nextInt(); int v = scanner.nextInt(); fr.add(new Pair(u, v)); // 将朋友关系加入集合 fr } // 读取查询操作 for (int i = 0; i < q; i++) { int op = scanner.nextInt(); int u = scanner.nextInt(); int v = scanner.nextInt(); if (op == 1) { fr.remove(new Pair(u, v)); // 删除朋友关系 } qs.add(new Pair(op, u, v)); // 加入查询操作列表 } // 按照逆序处理查询操作 Collections.reverse(qs); // 将初始的朋友关系进行合并操作,构建并查集 for (Pair pair : fr) { merge(pair.first, pair.second); } // 处理查询操作 for (Pair pair : qs) { if (pair.first == 1) { merge(pair.second, pair.third); // 执行合并操作 } else { // 执行查询操作,判断两个节点是否连通 ans.add(find(pair.second) == find(pair.third) ? "Yes" : "No"); } } // 将查询结果按照输入顺序输出 Collections.reverse(ans); for (String s : ans) { System.out.println(s); } } }
利用克鲁斯卡尔构造最小生成树里面的选边操作:遍历每一条边,判断这条边连接的两个顶点是否属于相同的连通分量。
-
如果两个顶点属于不同的连通分量,则说明在遍历到当前的边之前,这两个顶点之间不连通,因此当前的边不会导致环出现,合并这两个顶点的连通分量。
-
如果两个顶点属于相同的连通分量,则说明在遍历到当前的边之前,这两个顶点之间已经连通,因此当前的边导致环出现,为附加的边,将当前的边作为答案返回。
package com.coedes.union_and_find.likou684; /** * @description:https://leetcode.cn/problems/redundant-connection/description/ * @author: wenLiu * @create: 2024/5/8 13:41 */ public class Solution { public int[] findRedundantConnection(int[][] edges) { //最小生成树性质:n个节点n-1条边 , 题目给的n个节点n条边,说明一定有一条边冗余导致出现环 int n = edges.length; int[] p = new int[n+1];//从1开始 for (int i = 1; i <= n; i++) { p[i] = i; } for (int[] edge : edges) { int u = edge[0]; int v = edge[1]; if (find(u, p) != find(v, p)) {//判断u,v是否处于同一连通分量 merge(u, v, p); }else { return edge; } } return new int[0]; } static void merge(int u, int v, int[] p) { int p_u = find(u, p); int p_v = find(v, p); if (p_u != p_v) { p[p_u] = p_v; } } static int find(int x, int[] p) { if (p[x] != x) { p[x] = find(p[x], p); } return p[x]; } }