本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~
一.并查集模板(路径压缩+小挂大)
题目:并查集的实现
算法原理
-
并查集基础概念
- 并查集主要用于处理不相交集合的合并与查询问题。
- 在这个代码中,每个元素最初都属于自己单独的集合,然后通过合并操作将一些集合合并在一起,并且可以查询两个元素是否在同一个集合中。
-
数据结构部分(build函数)
- father数组
- 用于存储每个元素的父节点,初始时每个元素的父节点是它自己,即
father[i]=i
。这表示每个元素都是一个单独集合的代表。 - 随着合并操作的进行,
father
数组会不断更新,使得同一集合中的元素最终指向同一个代表元素。
- 用于存储每个元素的父节点,初始时每个元素的父节点是它自己,即
- size数组
- 用于存储每个集合的大小。初始时每个集合只有一个元素,所以
size[i]=1
。 - 在合并操作时,会根据集合大小来决定合并的方式,以保持树形结构相对平衡,提高后续查询操作的效率。
- 用于存储每个集合的大小。初始时每个集合只有一个元素,所以
- stack数组
- 这是一个辅助数组,用于在查找操作(
find
函数)中的路径压缩过程。
- 这是一个辅助数组,用于在查找操作(
- father数组
-
查找操作(find函数)
- 目的是找到给定元素所在集合的代表元素。
- 从给定元素
i
开始,沿着father
指针不断向上查找,直到找到一个元素j
使得father[j]=j
,这个j
就是i
所在集合的代表元素。 - 在向上查找过程中,将经过的元素存储到
stack
数组中,记录经过的元素个数size
。 - 找到代表元素后,再将
stack
数组中的元素的father
指针直接指向代表元素,实现路径压缩。路径压缩可以减少后续查找操作的时间复杂度,使得查找操作接近常数时间。
-
判断是否在同一集合(isSameSet函数)
- 通过调用
find
函数分别找到元素x
和y
的代表元素。 - 如果这两个代表元素相同,就说明
x
和y
在同一个集合中,返回true
(输出"Yes");否则,返回false
(输出"No")。
- 通过调用
-
合并操作(union函数)
- 首先调用
find
函数找到元素x
和y
的代表元素fx
和fy
。 - 如果
fx
和fy
不相等,说明x
和y
不在同一个集合中,需要进行合并操作。 - 比较
fx
和fy
所代表集合的大小(通过size
数组)。 - 如果
size[fx]>=size[fy]
,则将fy
所在集合合并到fx
所在集合中,即将fy
的父节点设为fx
(father[fy]=fx
),并且更新fx
所在集合的大小(size[fx]+=size[fy]
);否则,将fx
所在集合合并到fy
所在集合中,相应地更新father
和size
数组。
- 首先调用
代码实现
// 并查集模版(牛客)
// 路径压缩 + 小挂大
// 测试链接 : https://www.nowcoder.com/practice/e7ed657974934a30b2010046536a5372
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
public class Code01_UnionFindNowCoder {
public static int MAXN = 1000001;
public static int[] father = new int[MAXN];
public static int[] size = new int[MAXN];
public static int[] stack = new int[MAXN];
public static int n;
public static void build() {
for (int i = 0; i <= n; i++) {
father[i] = i;
size[i] = 1;
}
}
// i号节点,往上一直找,找到代表节点返回!
public static int find(int i) {
// 沿途收集了几个点
int size = 0;
while (i != father[i]) {
stack[size++] = i;
i = father[i];
}
// 沿途节点收集好了,i已经跳到代表节点了
while (size > 0) {
father[stack[--size]] = i;
}
return i;
}
public static boolean isSameSet(int x, int y) {
return find(x) == find(y);
}
public static void union(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
// fx是集合的代表:拿大小
// fy是集合的代表:拿大小
if (size[fx] >= size[fy]) {
size[fx] += size[fy];
father[fy] = fx;
} else {
size[fy] += size[fx];
father[fx] = fy;
}
}
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
n = (int) in.nval;
build();
in.nextToken();
int m = (int) in.nval;
for (int i = 0; i < m; i++) {
in.nextToken();
int op = (int) in.nval;
in.nextToken();
int x = (int) in.nval;
in.nextToken();
int y = (int) in.nval;
if (op == 1) {
out.println(isSameSet(x, y) ? "Yes" : "No");
} else {
union(x, y);
}
}
}
out.flush();
out.close();
br.close();
}
}
二.并查集模板(只有路径压缩)
题目:【模板】并查集
算法原理
-
基础概念与数据结构
- 并查集用于处理不相交集合的合并与查询问题。
- father数组
- 这个数组用于存储每个元素的父节点。初始时,在
build
函数中,每个元素的父节点被设置为它自己,即father[i]=i
,表示每个元素是一个单独集合的初始代表。
- 这个数组用于存储每个元素的父节点。初始时,在
-
查找操作(find函数)
- 这个函数的目的是找到元素所在集合的代表元素。
- 采用递归的方式实现路径压缩。当
i!=father[i]
时,先递归调用find
函数找到father[i]
的代表元素,然后将father[i]
直接指向这个代表元素。这样做的好处是在后续查询中,可以减少查找的深度,提高查询效率。最后返回找到的代表元素father[i]
。
-
判断是否在同一集合(isSameSet函数)
- 分别调用
find
函数找到元素x
和y
的代表元素。 - 如果这两个代表元素相同,说明
x
和y
在同一个集合中,返回true
(输出"Y");否则,返回false
(输出"N")。
- 分别调用
-
合并操作(union函数)
- 直接将
x
所在集合的代表元素(通过find(x)
找到)的父节点设置为y
所在集合的代表元素(通过find(y)
找到)。这里没有像之前的代码那样进行小挂大的优化,在数据规模不是非常大或者对效率要求不是极高的情况下,这种简单的合并操作也是可行的。
- 直接将
代码实现
// 并查集模版(洛谷)
// 本实现用递归函数实现路径压缩,而且省掉了小挂大的优化,一般情况下可以省略
// 测试链接 : https://www.luogu.com.cn/problem/P3367
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
public class Code02_UnionFindLuogu {
public static int MAXN = 200001;
public static int[] father = new int[MAXN];
public static int n;
public static void build() {
for (int i = 0; i <= n; i++) {
father[i] = i;
}
}
public static int find(int i) {
if (i != father[i]) {
father[i] = find(father[i]);
}
return father[i];
}
public static boolean isSameSet(int x, int y) {
return find(x) == find(y);
}
public static void union(int x, int y) {
father[find(x)] = find(y);
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
n = (int) in.nval;
build();
in.nextToken();
int m = (int) in.nval;
for (int i = 0; i < m; i++) {
in.nextToken();
int z = (int) in.nval;
in.nextToken();
int x = (int) in.nval;
in.nextToken();
int y = (int) in.nval;
if (z == 1) {
union(x, y);
} else {
out.println(isSameSet(x, y) ? "Y" : "N");
}
}
}
out.flush();
out.close();
br.close();
}
}
三.情侣牵手
题目:情侣牵手
算法原理
-
数据结构部分
- father数组:用于构建并查集的数据结构,表示元素之间的关系。这里
father[i]
初始表示i
的父节点,在初始构建(build
函数)时,每个元素的父节点是它自己,即每个元素是一个单独集合的代表。 - sets变量:用于记录当前集合的数量。初始时,集合数量为
n / 2
,因为有n / 2
对情侣,即n / 2
个初始独立的集合。
- father数组:用于构建并查集的数据结构,表示元素之间的关系。这里
-
构建并查集(build函数)
- 对于给定的
m
(这里m = n / 2
),函数遍历从0
到m - 1
的每个元素,将其father
设置为自己,表示每个元素是一个单独的集合,并且将sets
初始化为m
,代表初始的集合数量。
- 对于给定的
-
查找操作(find函数)
- 目的是找到给定元素所在集合的代表元素。
- 如果
i
不等于father[i]
,说明i
不是所在集合的代表元素,通过递归调用find
函数找到father[i]
的代表元素,然后将father[i]
直接指向这个代表元素,实现路径压缩,以提高后续查询效率。最后返回找到的代表元素father[i]
。
-
合并操作(union函数)
- 首先通过
find
函数找到x
和y
的代表元素fx
和fy
。 - 如果
fx
和fy
不相等,说明x
和y
属于不同的集合,此时将fx
的父节点设置为fy
,表示将x
所在的集合合并到y
所在的集合中,并且集合数量sets
减1,因为合并后集合数量减少了一个。
- 首先通过
-
主函数(minSwapsCouples)
- 首先确定情侣对数
n = row.length
,然后构建并查集(build(n / 2)
)。 - 接着遍历座位数组
row
,每两个座位一组(i
和i + 1
),将对应的情侣编号(row[i] / 2
和row[i + 1] / 2
)进行合并操作(union
)。 - 最后,最少交换次数等于初始情侣对数(
n / 2
)减去最终的集合数量(sets
)。这是因为每成功合并一对情侣(减少一个集合),就意味着减少了一次交换的需求,最终剩下的集合数量就是没有配对成功的情侣对数,用总的情侣对数减去它就是最少交换次数。
- 首先确定情侣对数
代码实现
// 情侣牵手
// n对情侣坐在连续排列的 2n 个座位上,想要牵到对方的手
// 人和座位由一个整数数组 row 表示,其中 row[i] 是坐在第 i 个座位上的人的ID
// 情侣们按顺序编号,第一对是 (0, 1),第二对是 (2, 3),以此类推,最后一对是 (2n-2, 2n-1)
// 返回 最少交换座位的次数,以便每对情侣可以并肩坐在一起
// 每次交换可选择任意两人,让他们站起来交换座位
// 测试链接 : https://leetcode.cn/problems/couples-holding-hands/
public class Code03_CouplesHoldingHands {
public static int minSwapsCouples(int[] row) {
int n = row.length;
build(n / 2);
for (int i = 0; i < n; i += 2) {
union(row[i] / 2, row[i + 1] / 2);
}
return n / 2 - sets;
}
public static int MAXN = 31;
public static int[] father = new int[MAXN];
public static int sets;
public static void build(int m) {
for (int i = 0; i < m; i++) {
father[i] = i;
}
sets = m;
}
public static int find(int i) {
if (i != father[i]) {
father[i] = find(father[i]);
}
return father[i];
}
public static void union(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
father[fx] = fy;
sets--;
}
}
}
四.相似字符串组
题目:相似字符串组
算法原理
-
数据结构
- father数组:用于构建并查集,
father[i]
表示第i
个字符串在并查集中的父节点,初始时每个字符串的父节点是它自己,即每个字符串是一个单独集合的代表。 - sets变量:用于记录当前的集合数量,初始化为字符串的总数
n
。
- father数组:用于构建并查集,
-
构建并查集(build函数)
- 对于给定的字符串数量
n
,函数遍历从0
到n - 1
的每个索引。 - 将
father[i]
设置为i
,表示每个字符串初始为独立的集合,并且将sets
设置为n
,代表初始有n
个集合。
- 对于给定的字符串数量
-
查找操作(find函数)
- 查找操作的目的是找到给定字符串索引
i
所在集合的代表元素。 - 如果
i
不等于father[i]
,说明i
不是所在集合的代表元素,通过递归调用find
函数找到father[i]
的代表元素,然后将father[i]
直接指向这个代表元素,实现路径压缩,这有助于提高后续查找操作的效率。最后返回找到的代表元素father[i]
。
- 查找操作的目的是找到给定字符串索引
-
合并操作(union函数)
- 首先通过
find
函数找到x
和y
的代表元素fx
和fy
。 - 如果
fx
和fy
不相等,说明x
和y
属于不同的集合,此时将fx
的父节点设置为fy
,表示将x
所在的集合合并到y
所在的集合中,并且集合数量sets
减1,因为合并后集合数量减少了一个。
- 首先通过
-
计算相似字符串组数量(numSimilarGroups函数)
- 首先获取字符串的数量
n
和每个字符串的长度m
。 - 构建并查集(
build(n)
)。 - 然后进行双层循环,外层循环
i
从0
到n - 1
,内层循环j
从i + 1
到n - 1
。 - 对于每一对字符串
strs[i]
和strs[j]
,如果它们不在同一个集合(find(i)!=find(j)
),则计算它们不同字符的数量diff
。 - 如果
diff
为0
(字符串相等)或者diff
为2
(满足相似字符串的条件),则将这两个字符串所在的集合合并(union(i, j)
)。 - 最后返回
sets
,即最终的集合数量,这个数量就是字符串组的数量。
- 首先获取字符串的数量
代码实现
// 相似字符串组
// 如果交换字符串 X 中的两个不同位置的字母,使得它和字符串 Y 相等
// 那么称 X 和 Y 两个字符串相似
// 如果这两个字符串本身是相等的,那它们也是相似的
// 例如,"tars" 和 "rats" 是相似的 (交换 0 与 2 的位置);
// "rats" 和 "arts" 也是相似的,但是 "star" 不与 "tars","rats",或 "arts" 相似
// 总之,它们通过相似性形成了两个关联组:{"tars", "rats", "arts"} 和 {"star"}
// 注意,"tars" 和 "arts" 是在同一组中,即使它们并不相似
// 形式上,对每个组而言,要确定一个单词在组中,只需要这个词和该组中至少一个单词相似。
// 给你一个字符串列表 strs列表中的每个字符串都是 strs 中其它所有字符串的一个字母异位词。
// 返回 strs 中有多少字符串组
// 测试链接 : https://leetcode.cn/problems/similar-string-groups/
public class Code04_SimilarStringGroups {
public static int MAXN = 301;
public static int[] father = new int[MAXN];
public static int sets;
public static void build(int n) {
for (int i = 0; i < n; i++) {
father[i] = i;
}
sets = n;
}
public static int find(int i) {
if (i != father[i]) {
father[i] = find(father[i]);
}
return father[i];
}
public static void union(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
father[fx] = fy;
sets--;
}
}
public static int numSimilarGroups(String[] strs) {
int n = strs.length;
int m = strs[0].length();
build(n);
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (find(i) != find(j)) {
int diff = 0;
for (int k = 0; k < m && diff < 3; k++) {
if (strs[i].charAt(k) != strs[j].charAt(k)) {
diff++;
}
}
if (diff == 0 || diff == 2) {
union(i, j);
}
}
}
}
return sets;
}
}
五.岛屿数量
题目:岛屿数量
算法原理
-
数据结构
- father数组:用于构建并查集,
father[i]
表示第i
个元素(这里对应网格中的陆地格子)在并查集中的父节点。初始时,每个陆地格子的父节点是它自己,表示每个陆地格子是一个单独集合的代表。 - cols变量:用于记录网格的列数,方便在将二维坐标转换为一维索引时使用。
- sets变量:用于记录当前的集合数量,初始时每一个独立的陆地格子都是一个集合,所以每发现一个'1'的格子,
sets
就加1。
- father数组:用于构建并查集,
-
构建并查集(build函数)
- 首先确定网格的列数
cols = m
,并将sets
初始化为0。 - 然后遍历二维网格,对于每个格子,如果它是陆地(
board[a][b] == '1'
),计算其在一维数组中的索引index = a * cols + b
,将father[index]
设置为index
,表示这个格子是一个单独的集合,同时sets
加1,表示发现了一个新的可能的岛屿。
- 首先确定网格的列数
-
查找操作(find函数)
- 查找操作的目的是找到给定元素所在集合的代表元素。
- 如果
i
不等于father[i]
,说明i
不是所在集合的代表元素,通过递归调用find
函数找到father[i]
的代表元素,然后将father[i]
直接指向这个代表元素,实现路径压缩,这有助于提高后续查找操作的效率。最后返回找到的代表元素father[i]
。
-
合并操作(union函数)
- 这个函数用于合并两个集合。首先通过
find
函数分别找到坐标为(a, b)
和(c, d)
的陆地格子在并查集中的代表元素fx
和fy
。 - 如果
fx
和fy
不相等,说明这两个格子属于不同的集合,此时将fx
的父节点设置为fy
,表示将fx
所在的集合合并到fy
所在的集合中,并且集合数量sets
减1,因为合并后集合数量减少了一个。
- 这个函数用于合并两个集合。首先通过
-
计算岛屿数量(numIslands函数)
- 首先获取网格的行数
n
和列数m
,然后构建并查集(build(n, m, board)
)。 - 接着遍历二维网格,对于每个陆地格子(
board[i][j] == '1'
),检查其左边(j > 0 && board[i][j - 1] == '1'
)和上边(i > 0 && board[i - 1][j] == '1'
)是否也是陆地,如果是,则将它们所在的集合合并(union(i, j, i, j - 1)
或union(i, j, i - 1, j)
)。 - 最后返回
sets
,即最终的集合数量,这个数量就是岛屿的数量,因为每个岛屿最终会被合并成一个集合。
- 首先获取网格的行数
代码实现
// 岛屿数量
// 给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量
// 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成
// 此外,你可以假设该网格的四条边均被水包围
// 测试链接 : https://leetcode.cn/problems/number-of-islands/
public class Code05_NumberOfIslands {
// 并查集的做法
public static int numIslands(char[][] board) {
int n = board.length;
int m = board[0].length;
build(n, m, board);
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (board[i][j] == '1') {
if (j > 0 && board[i][j - 1] == '1') {
union(i, j, i, j - 1);
}
if (i > 0 && board[i - 1][j] == '1') {
union(i, j, i - 1, j);
}
}
}
}
return sets;
}
public static int MAXSIZE = 100001;
public static int[] father = new int[MAXSIZE];
public static int cols;
public static int sets;
public static void build(int n, int m, char[][] board) {
cols = m;
sets = 0;
for (int a = 0; a < n; a++) {
for (int b = 0, index; b < m; b++) {
if (board[a][b] == '1') {
index = index(a, b);
father[index] = index;
sets++;
}
}
}
}
public static int index(int a, int b) {
return a * cols + b;
}
public static int find(int i) {
if (i != father[i]) {
father[i] = find(father[i]);
}
return father[i];
}
public static void union(int a, int b, int c, int d) {
int fx = find(index(a, b));
int fy = find(index(c, d));
if (fx != fy) {
father[fx] = fy;
sets--;
}
}
}