并查集理论基础
题目讲解:代码随想录
背景
- 并查集常用来解决连通性问题
- 并查集主要有两个功能:
1.将两个元素添加到一个集合中。
2.判断两个元素在不在同一个集合原理讲解
- 如何将两个元素添加到同一个集合中呢?
我们将三个元素A,B,C放在同一个集合,其实就是将三个元素连通在一起。只需要用一个一维数组来表示,即:father[A] = B,father[B] = C 这样就表述 A 与 B 与 C连通了(有向连通图)。// 将v,u 这条边加入并查集 void join(int u, int v) { u = find(u); // 寻找u的根 v = find(v); // 寻找v的根 if (u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 father[v] = u; }
- 只要 A ,B,C 在同一个根下就是同一个集合。
- find函数是如何实现的呢?
通过数组下标找到数组元素,一层一层寻根过程// 并查集里寻根的过程 int find(int u) { if (u == father[u]) return u; // 如果根就是自己,直接返回 else return find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找 }
- father数组初始化的时候要 father[i] = i
我们需要 father[C] = C,即C的根也为C,这样就方便表示 A,B,C 都在同一个集合里了。// 并查集初始化 void init() { for (int i = 0; i < n; ++i) { father[i] = i; } }
- 如何判断两个元素是否在同一个集合里?
通过 find函数 找到 两个元素属于同一个根的话,那么这两个元素就是同一个集合// 判断 u 和 v是否找到同一个根 bool isSame(int u, int v) { u = find(u); v = find(v); return u == v; }路径压缩
- find 函数,通过递归的方式,不断获取father数组下标对应的数值,最终找到这个集合的根。如果这棵多叉树高度很深的话,每次find函数 去寻找根的过程就要递归很多次。
- 路径压缩:将非根节点的所有节点直接指向根节点
在递归的过程中,让 father[u] 接住 递归函数 find(father[u]) 的返回结果// 并查集里寻根的过程,路径压缩 int find(int u) { if (u == father[u]) return u; else return father[u] = find(father[u]); // 路径压缩 }代码模板
- 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
- 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
- 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好 vector<int> father = vector<int> (n, 0); // C++里的一种数组结构 // 并查集初始化 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]); // 路径压缩 } // 判断 u 和 v是否找到同一个根 bool isSame(int u, int v) { u = find(u); v = find(v); return u == v; } // 将v->u 这条边加入并查集 void join(int u, int v) { u = find(u); // 寻找u的根 v = find(v); // 寻找v的根 if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 father[v] = u; }常见误区
- join里不能调用isSame来精简。
join(1, 2); join(3, 2);
- 如果join里调用isSame而不是find,生成的图如下:
此时调用 isSame(1, 3),返回false,代码告诉我们 1 和 3 不在同一个集合,这明显不符合我们的预期- 如果join里调用的是find,生成的图如下:
此时调用 isSame(1, 3),find(1) 返回的是3,find(3) 返回的也是3,返回true,即告诉我们 元素 1 和 元素3 是 在同一个集合里的。模拟过程
- join(1, 8);
- join(3, 8);
为什么 将 元素1 连向元素 3 而不是将 元素 8 连向 元素 3?
因为在join(int u, int v)函数里 要分别对 u 和 v 寻根之后再把根进行关联。- join(1, 7);
- join(8, 5);
为什么 图中 8 又直接指向了 3 了呢?
因为在join调用find,find里路经压缩了- join(6, 9);
- isSame(8, 7):true
- isSame(7, 2):false
拓展
- 按秩(rank)合并
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好 vector<int> father = vector<int> (n, 0); // C++里的一种数组结构 vector<int> rank = vector<int> (n, 1); // 初始每棵树的高度都为1 // 并查集初始化 void init() { for (int i = 0; i < n; ++i) { father[i] = i; rank[i] = 1; // 也可以不写 } } // 并查集里寻根的过程 int find(int u) { return u == father[u] ? u : find(father[u]);// 注意这里不做路径压缩 } // 判断 u 和 v是否找到同一个根 bool isSame(int u, int v) { u = find(u); v = find(v); return u == v; } // 将v->u 这条边加入并查集 void join(int u, int v) { u = find(u); // 寻找u的根 v = find(v); // 寻找v的根 if (rank[u] <= rank[v]) father[u] = v; // rank小的树合入到rank大的树 else father[v] = u; if (rank[u] == rank[v] && u != v) rank[v]++; // 如果两棵树高度相同,则v的高度+1,因为上面 if (rank[u] <= rank[v]) father[u] = v; 注意是 <= }
- 我们在优化并查集查询效率的时候,只用路径压缩的思路就够了,不仅代码实现精简,而且效率足够高。按秩合并的思路并没有将树形结构尽可能的扁平化,所以在整理效率上是没有路径压缩高的。
- 无论使用并查集模板里哪一个函数(除了init函数),都会有路径压缩的过程,第二次访问相同节点的时候,这个节点就是直连根节点的,即 第一次访问的时候它的路径就被压缩了。
复杂度分析
- 空间复杂度: O(n) ,申请一个father数组。
- 时间复杂度在O(logn)与O(1)之间,且随着查询或者合并操作的增加,时间复杂度会越来越趋于O(1)。
107. 寻找存在的路径
题目讲解:代码随想录
重点:
- 并查集的代码模板理解并记住
思路:
- 初始化并查集
private static int[] father; private static void init(int n) { father = new int[n + 1]; for (int i = 0; i < n; i++) { father[i + 1] = i + 1; } }
- 完成find函数并进行路径压缩,方便后面的join和isSame
private static int find(int u) { if (u == father[u]) return u; return father[u] = find(father[u]); }
- 把scanner获取到的边用join连接
private static void join(int u, int v) { u = find(u); v = find(v); if (u == v) return; father[v] = u; } for (int i = 0; i < m; i++) { int s = scanner.nextInt(); int t = scanner.nextInt(); join(t, s); }
- 用isSame判断是否在同一集合中,在同一集合中的节点都是连通的
private static boolean isSame(int u, int v) { u = find(u); v = find(v); return u == v; } if (isSame(source, destination)) System.out.println(1); else System.out.println(0);
public class FindPathOfExistence {
// 并查集数组
private static int[] father;
// 初始化并查集数组
private static void init(int n) {
father = new int[n + 1];
for (int i = 0; i < n; i++) {
father[i + 1] = i + 1;
}
}
// 寻根, 并进行路径压缩方便第二次及之后的查找
private static int find(int u) {
if (u == father[u]) return u;
return father[u] = find(father[u]);
}
// 把u和v的根连接起来
private static void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return;
father[v] = u;
}
// 判断u和v是否在同一集合中
private static boolean isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int m = scanner.nextInt();
// 初始化
init(n);
// 连接边
for (int i = 0; i < m; i++) {
int s = scanner.nextInt();
int t = scanner.nextInt();
join(t, s);
}
// 判断是否连接
int source = scanner.nextInt();
int destination = scanner.nextInt();
if (isSame(source, destination)) System.out.println(1);
else System.out.println(0);
}
}
108. 冗余连接
题目讲解:代码随想录
重点:
- 理解题目是在树的基础上添加一条边,所以冗余边仅仅是一条。
思路:
- 初始化并查集数组
private static int[] father; private static void init(int n) { father = new int[n + 1]; for (int i = 0; i< n + 1; i++) father[i] = i; } init(n);
- 判断当前要加入的边的起点和终点是否已经连通了。如果已经连通了,则是冗余边。如果没有连通,则执行join
private static int find(int u) { if (father[u] == u) return u; return father[u] = find(father[u]); } private static void join(int u, int v) { u = find(u); v = find(v); if (u == v) return; father[v] = u; } private static boolean isSame(int u, int v) { u = find(u); v = find(v); return u == v; } if (isSame(s, t)) { System.out.print(s + " "); System.out.println(t); } else join(t, s);
public class RedundantConnection {
// 并查集数组
private static int[] father;
// 初始化并查集数组
private static void init(int n) {
father = new int[n + 1];
for (int i = 0; i< n + 1; i++) {
father[i] = i;
}
}
// 寻根, 并路径压缩
private static int find(int u) {
if (father[u] == u) return u;
return father[u] = find(father[u]);
}
// 连接u和v
private static void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return;
father[v] = u;
}
// 判断u和v是否连通
private static boolean isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
init(n);
for (int i = 0; i < n; i++) {
int s = scanner.nextInt();
int t = scanner.nextInt();
// 如果已经连通, 说明再加入当前边会形成环, 直接输出
if (isSame(s, t)) {
System.out.print(s + " ");
System.out.println(t);
} else {
join(t, s);
}
}
}
}







579

被折叠的 条评论
为什么被折叠?



