Union Find是基于disjoint set的原理去判断一个图中元素之间的连通性或者说关联性的方法。
关于Disjoint Set的原理,可以参考这篇文章:https://blog.youkuaiyun.com/firehotest/article/details/53503624
其中提到了,生成最小生成树的kruskal 算法是基于disjoint set的基础上的。disjoint set的基本操作就是union find。
什么是最小生成树?给你一个有权全通图,找到边集的最小权重子集,使得所有的顶点全连通。kruskal算法内容:
给所有的边按照权重从小到大排序。按排序后的顺序提取每条边,如果边的两头的顶点不是联通的,(find不是一个disjoint set)的,把边加入答案集并且union两个顶点。遍历完所有的边之后,得到的就是答案了。
这篇文章,主要讲讲,union find的过程中,比较容易出错的几个点。
1、一般而言,对于二维数组的union find,parent最好是降维的。因为能够提高find的效率,也增强了程序的可读性。
2、union的时候,是把rootA和rootB对应的parent重设。不是点a和点b的parent。
3、path compression是一个很好的提高效率的方法。下面通过find的模板来说明:
int find(int id) {
while (id != parent[id]) {
parent[id] = parent[parent[id]];
id = parent[id];
}
return id;
}
其中,parent[id] = parent[parent[id]]做的就是path compression。如果去掉这一句,也是可以正常运行得到结果的。这里有几种情况:
id本身自己是root,那么不进入循环。
parent[id]本身就是一个root了,下一次parent[id]还是一样是一个值,没有compression的效果。
parent[id]不是root,那么下次parent[id]直接指向parent[id]的parent,可以省略掉parent[id]这一步,有compression的效果。
另外一种compression path的写法(推荐这种)是:
int find(int id) {
if (id != parent[id]) {
parent[id] = find(parent[id]);
}
return parent[id];
}
个人觉得,这种通过递归的写法更为彻底的compression了。因为只有最后到root的时候才一层一层的返回。
以friend circle作为例子,讲述union find的写法。
写法1:
class Solution {
private class UfHelper {
private int d;
private int[] parent;
private int[] rank;
private int count;
public UfHelper(int[][] in) {
int d = in.length;
parent = new int[d];
rank = new int[d];
count = d;
for (int i = 0; i < d; ++i) {
parent[i] = i;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public void union(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
--count;
if (rank[x] < rank[y]) {
parent[rootx] = rooty;
} else if (rank[x] > rank[y]) {
parent[rooty] = rootx;
} else {
parent[rootx] = rooty;
rank[rooty] += 1;
}
}
}
public int getCount() {
return count;
}
}
public int findCircleNum(int[][] M) {
if (M == null || M.length == 0 || M[0] == null || M[0].length == 0) {
return 0;
}
UfHelper helper = new UfHelper(M);
int d = M.length;
for (int i = 0; i < d; ++i) {
for (int j = i + 1; j < d; ++j) {
if (M[i][j] == 1) {
helper.union(i, j);
}
}
}
return helper.getCount();
}
}
写法2:
class Solution {
private int[] parent;
private int[] rank;
private int n;
private int find(int id) {
if (id != parent[id]) {
parent[id] = find(parent[id]);
}
return parent[id];
}
public int findCircleNum(int[][] M) {
if (M == null || M.length == 0) {
return 0;
}
n = M.length;
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; ++i) {
parent[i] = i;
}
int ans = n;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (M[i][j] == 1) {
int rooti = find(i);
int rootj = find(j);
if (rooti != rootj) {
// union
--ans;
if (rank[rooti] < rank[rootj]) {
parent[rooti] = rootj;
} else if (rank[rooti] > rank[rootj]) {
parent[rootj] = rooti;
} else {
parent[rooti] = rootj;
++rank[rootj];
}
}
}
}
}
return ans;
}
}
写法1把parent和rank都包装起来了,封装性能比价好,但是遇到number of island II一种慢慢加联通店的题目就显得十分不方便。另外经过测试之后,使用第二种path compression的性能比第一种要好。