一、应用场景
用于处理不相交集合的合并和查询问题
示例:
n 个元素(分属不同的的 n 个集合),进行两种操作:
- 并 —— 给出两个元素的关系,合并两个集合
- 查 —— 查询两个元素是否在同一个集合
二、并查集的实现思路
-
一个集合构建一颗树,任选一个元素作为该树的根节点
-
并操作 —— 将一个集合的根节点的父节点改为另一个集合的根节点
pre[B的根结点] = A 的根节点
- 查操作 —— 从该元素开始访问父节点(使用递归进行查找),直到访问到根节点,在对两个元素的根节点进行比较判断
x = pre(x) // 此时停止,找到了根节点
三、并查集代码模板
并查集的代码模板由以下几部分组成 ——
- 初始化每个节点父节点
- 查找每个节点的根节点操作
- 合并两个节点操作
- 判断两个节点是否属于同一个根
最终得到的代码模板如下 ——
int n = 1005; // 节点数量
int father[n];
// 并查集的初始化 —— 把每个节点的父节点初始化为其自身
void init() {
for(int i=0; i < n; i++) {
father[i] = i;
}
}
// 通过递归的方式找根节点
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]); // 找到根节点为自身的不再查找
}
// 合并操作 —— 将 v -> u 加入并查集
void join(int u, int v) {
// 找到 u , v 的根节点,然后两个根节点接上
u = find(u);
v = find(v);
father[v] = u;
}
// 判断 u, v 是否属于同一个根
boolean same(int u, int v){
u = find(u);
v = find(v);
return u == v;
}
四、例题
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
思路:
另开一个数组,存放每个节点的最终根节点。然后遍历这个最终根节点数组,判断其中含有几个不同的元素,就是几个省份。
优化 ——
我们可以初始化省份数量为城市数量,然后在合并两个节点过程中,每合并一次,省份数减一,这样得到的最终结果也是正确的,而且代码更加简洁。
代码:
最开始初始化省份数量为城市数量 n , 在查找过程中,每合并两个城市,将 n-- , 最后得到的结果就是最终的省份数量
class Solution {
int n; // 节点数量
int[] father;
int circleNum;
// 不同的省份数量
public int findCircleNum(int[][] isConnected) {
n = isConnected.length;
circleNum = n;
father = new int[n];
init(); // 初始化
int n = isConnected.length;
for(int i=0; i < n; i++) { // 对称矩阵, 只遍历下三角矩阵即可
for(int j=0; j<i; j++) {
if(isConnected[i][j] == 1){ // 合并
join(i, j);
}
}
}
return circleNum;
}
// 并查集的初始化 —— 把每个节点的父节点初始化为其自身
void init() {
for(int i=0; i < n; i++) {
father[i] = i;
}
}
// 通过递归的方式找根节点
public int find(int u) {
if(u == father[u]){
return u;
} else {
return find(father[u]);
}
}
// 合并操作 —— 将 v -> u 加入并查集
void join(int u, int v) {
// 找到 u , v 的根节点,然后两个根节点接上
u = find(u);
v = find(v);
father[v] = u;
if(u != v){ // 每合并一个,省份的数量 -1
circleNum--;
}
}
}