并查集
本质就是一个数组 int[] father
;
father[i] = j
表示元素 i 属于 集合 j ;
" 集合 j " 说明这个集合以元素 j 为根; 只要一个元素 i 沿着 father 数组能往上找到 j , 那么 i 就属于根为 j 的集合;
例如 father = {1, 3, 3, 5, 1, 5};
我们希望找到 下标0 所属集合的根, father[0] = 1, father[1] = 3, father[3] = 5, father[5] = 5;
于是我们沿着 father 数组找到了 下标0 所属集合的根, 即 下标5;
并且这个集合中, 包含的元素有下标 {0, 1, 3, 5}
能做什么?
- 能快速求两个点是否在同一集合; 本文有例题;
- 能快速求集合大小; 本文有例题;
- 能快速判断无向图是否有环; 下一篇文章讲 Kruskal 算法时用到;
有哪些操作?
- 初始化一个并查集
class Union{
private int[] father;
public Union(int size){
father = new int[size];
// father[i] = i 表示 i 是当前集合的 根;
for(int i = 0; i < size; i++){
father[i] = i;
}
}
}
- 找到元素所属的集合(或者说找到元素的根)
public int root(int i){
int temp = i;
while(father[temp] != temp){
temp = father[temp];
}
// 路径压缩, 后面会介绍
father[i] = temp;
return temp;
}
- 判断两个元素是否属于相同的集合(有相同的根)
public boolean isSame(int i1, int i2){
return root(i1) == root(i2);
}
- 合并两个元素所在的集合;
public void join(int i1, int i2){
father[root(i2)] = root(i1);
}
性能如何?
一般情况下, 各个元素通过 father 数组在逻辑上构成一个 k叉树 的结构, 树的形状与插入以及合并的具体操作息息相关, 最坏时间复杂度为 O(n);
好消息是, 我们可以进行[路径压缩]来进行性能优化;
public int root(int i){
int temp = i;
while(father[temp] != temp){
temp = father[temp];
}
// 路径压缩
father[i] = temp;
return temp;
}
通过此优化, 每次调用 root 函数 (isSame 和 join 都会调用 root), 都会使得元素 i 能直接一步找到集合的根;
如果进行 [路径压缩], 那么方法调用次数越多, 性能越好, 越接近于 O(1);
题目: 判断任意两点间是否存在路径
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int k = sc.nextInt();
Union union = new Union(n);
for(int i = 0; i < k; i++){
union.join(sc.nextInt(), sc.nextInt());
}
int from = sc.nextInt();
int to = sc.nextInt();
System.out.println(union.isSame(from, to)? 1 : 0);
}
}
class Union{
private int[] father;
public Union(int size){
father = new int[size + 1];
for(int i = 1; i <= size; i++){
father[i] = i;
}
}
public boolean isSame(int i1, int i2){
return root(i1) == root(i2);
}
public void join(int i1, int i2){
int root1 = root(i1);
int root2 = root(i2);
if(root1 != root2){
father[root2] = root1;
}
}
public int root(int i){
int temp = i;
while(father[temp] != temp){
temp = father[temp];
}
father[i] = temp;
return temp;
}
}
题目: 求岛屿的最大面积
res = 4
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int m = sc.nextInt();
int n = sc.nextInt();
boolean[][] grid = new boolean[m][n];
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
grid[i][j] = (sc.nextInt() == 1);
}
}
System.out.println(getMaxAreaUnion(grid));
}
private static int getMaxAreaUnion(boolean[][] grid){
int m = grid.length;
int n = grid[0].length;
// 对坐标 i, j 进行编码, 好与并查集适配;
// 点[i,j] 在并查集中的下标为 i * n + j
Union union = new Union(m * n);
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(grid[i][j]){
union.add(i * n + j);
if(i > 0 && grid[i - 1][j]){
union.join(i * n + j, (i - 1) * n + j);
}
if(j > 0 && grid[i][j - 1]){
union.join(i * n + j, i * n + j -1);
}
}
}
}
return union.getMaxArea();
}
}
class Union{
private int[] father;
// 并查集变种, 增加统计集合大小的功能
private int[] area;
private int maxArea;
public Union(int size){
father = new int[size];
for(int i = 0; i < size; i++){
father[i] = i;
}
area = new int[size];
}
public int root(int index){
int root = index;
while(root != father[root]){
root = father[root];
}
father[index] = root;
return root;
}
// 合并岛屿时, 需要将root2的面积, 加到root1
public void join(int i1, int i2){
int root1 = root(i1);
int root2 = root(i2);
if(root1 != root2){
area[root1] += area[root2];
father[root2] = root1;
maxArea = Math.max(maxArea, area[root1]);
}
}
// 遇到一个值为 1 的点, 就将其标记为岛屿, 面积设置为1
public void add(int index){
area[index] = 1;
if(maxArea == 0){
maxArea = 1;
}
}
public int getMaxArea(){
return maxArea;
}
}