算法模式:并查集

在上一篇文章 算法模式:前缀树 介绍一种关于特殊的树的算法模式。本篇文章,再介绍一种关于特殊的树的算法模式:并查集。
并查集
并查集算法,英文是 Union-Find,是解决动态连通性(Dynamic Conectivity)问题的一种算法。动态连通性是计算机图论中的一种数据结构,动态维护图结构中相连信息。简单的说就是,图中各个节点之间是否相连、如何将两个节点连接,连接后还剩多少个连通分量。
动态连通性其实可以抽象成给一幅图连线。假设用一个数组表示一堆节点,每个节点都是一个连通分量。初始化视图如下:

并查集的一个重要操作是 union(a, b),就是将节点 a 和节点 b 建立连接。如图所示:

union(a, b) 还可以将已经建立的两个“子网”进行连接:

并查集除了 union,还有一个重要操作是 connnected(a, b)。判断方法也很简单,从节点 a 和 b 开始,向上查找,直到两个节点的根节点,判断两个根节点是否相等即可判断两个节点是否已经连接。为了加快这个判断速度,可以对其进行“路径压缩”,直白点说,就是将所有树的节点,都直接指向根节点,这样只需要一步即可到达根节点。路径压缩如图所示:

简单代码实现如下:
package com.diguage.labs;
import java.util.ArrayList;
import java.util.List;
/**
* 并查集
*
* PS:没想到代码竟然一次通过。
*
* @author D瓜哥 · https://www.diguage.com
* @since 2025-04-03 15:22:41
*/
public class UnionFind {
/**
* 连通分量
*/
private int size;
/**
* 每个节点及对应的父节点
*/
private int[] parent;
public UnionFind(int size) {
this.size = size;
parent = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
}
}
/**
* a 和 b 建立连接
*/
public void union(int a, int b) {
int ap = find(a);
int bp = find(b);
if (ap == bp) {
return;
}
parent[ap] = bp;
size--;
}
/**
* a 和 b 是否连通
*/
public boolean connected(int a, int b) {
int ap = find(a);
int bp = find(b);
return ap == bp;
}
/**
* 连通分量
*/
public int count() {
return size;
}
/**
* 查找节点 a 的根节点
*/
private int find(int a) {
int ap = parent[a];
if (a != ap) {
List<Integer> path = new ArrayList<>();
path.add(a);
// 向上查找根节点
while (parent[ap] != ap) {
path.add(ap);
ap = parent[ap];
}
// 路径压缩
// 只有一步,无需缩短路径
if (path.size() == 1) {
return ap;
}
for (Integer idx : path) {
parent[idx] = ap;
}
}
return ap;
}
public static void main(String[] args) {
UnionFind uf = new UnionFind(10);
uf.union(0, 1);
uf.union(1, 2);
uf.union(2, 3);
uf.union(3, 4);
uf.union(5, 6);
uf.union(6, 7);
uf.union(7, 8);
uf.union(8, 9);
uf.union(0, 5);
System.out.println(uf.count() + ", " + uf.connected(0, 9));
System.out.println(uf.count() + ", " + uf.connected(2, 9));
System.out.println(uf.count() + ", " + uf.connected(3, 9));
System.out.println(uf.count() + ", " + uf.connected(5, 9));
}
}LeetCode 547. 省份数量
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
示例 1:

输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]] 输出:2
示例 2:

输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]] 输出:3
提示:
1 <= n <= 200n == isConnected.lengthn == isConnected[i].lengthisConnected[i][j]为1或0isConnected[i][i] == 1isConnected[i][j] == isConnected[j][i]
思路分析
这就是一道典型的并查集题目,所谓的“返回省份数量”就是求解连通分量。这道题的连通性是通过一个矩阵表示的,所以,首先需要将这个矩阵转换成一个上面讲解的数组。由于 (i, j) 和 (j, i) 表示的含义一样,所以只需要扫描矩阵的右上部分或者左下部分即可。代码如下:
/**
* @author D瓜哥 · https://www.diguage.com
* @since 2025-04-03 22:15:52
*/
public int findCircleNum(int[][] isConnected) {
int len = isConnected.length;
UnionFind un = new UnionFind(len);
for (int i = 0; i < len; i++) {
// 只处理矩阵右上半部分
for (int j = len - 1; j > i; j--) {
if (isConnected[i][j] == 1) {
un.union(i, j);
}
}
}
return un.count();
}
private static class UnionFind {
/**
* 连通分量
*/
int size;
/**
* 每个节点及对应的父节点
*/
int[] parent;
public UnionFind(int size) {
this.size = size;
parent = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
}
}
/**
* 连通分量
*/
public int count() {
return size;
}
/**
* a 和 b 建立连接
*/
public void union(int a, int b) {
int ap = find(a);
int bp = find(b);
if (ap == bp) {
return;
}
parent[ap] = bp;
size--;
}
/**
* 查找节点 a 的根节点
*/
private int find(int a) {
int ap = parent[a];
if (ap != a) {
List<Integer> path = new ArrayList<>();
path.add(a);
// 向上查找根节点
while (ap != parent[ap]) {
path.add(ap);
ap = parent[ap];
}
// 路径压缩
// 只有一步,无需缩短路径
if (path.size() == 1) {
return ap;
}
for (Integer idx : path) {
parent[idx] = ap;
}
}
return ap;
}
}


