Algorithm: Union Find (并查集查找)

Union Find是基于disjoint set的原理去判断一个图中元素之间的连通性或者说关联性的方法。


关于Disjoint Set的原理,可以参考这篇文章:https://blog.csdn.net/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的性能比第一种要好。




你可能感兴趣的:(Algorithm)