并查集- Union-Find
来源:互联网 发布:mysql insert 编辑:程序博客网 时间:2024/05/22 14:06
并查集(Union-Find)主要是用于解决连通问题。给定一些数据对,判断这些集合中连通量有 多少,或者判断给定两个点是否是连通的。并查集主要用于判断是否连通,而不需要输出具体的路径。如果需要输出具体的路径,可以使用DFS之类的。
并查集的核心操作,主要就是find()以及union()。并查集的几个实现版本都是基于对这两个方法进行不断优化,从而演化得版本。本篇博客主要是简要介绍并查集的结构,以及并查集的优化思路(不同的实现版本)。更加细节的介绍,可以参考这篇博客,其实就是《算法》这本书里的内容。
并查集结构
首先我们明确一下需要解决的问题:指定两个节点p,q。判断p,q是否连通。
为了解决上面的问题,我们需要提供一些操作来完成这个。操作可能有
- find(int p): 查询节点属于的组
- connected(int p,int q): 判断两个节点是否属于同一个组
- union(int p,int q): 连接两个节点,使之属于同一个
- count(): 获取连通组的数目
因此并查集的结构为
public class UF { private int[] id; //分组标识 private int count; // 连通分量的个数 public UF(int N) { count = N; // N为元素个数 id = new int[N]; for(int i=0;i<N;i++){ id[i]=i; } } public int count(){ return count; } public int find(int p)(); // 查找P的所属组的标识 public void union(int p,int q)(); //连接两个点 public boolean connected(int p,int q){ //查看两个点是否连接。 return find(p) == find(q); }}
回到上面所提到的问题。我们需要判断两个点是否在一个连通分量里面,也就是两个点是否连通。那么我们就需要调用connected(int p,int q)方法。从代码中可以看到从connected方法调用的是find方法。find方法返回的是该点所属的分量标识。假如两个点的分量标识相同的话,那么就判定为这两个点属于同一个组,是连通的。
可以看到,最核心的操作就是find方法。其他的方法都需要使用到find方法。如connected方法,需要判断两个点是否连通,则需要调用find方法,找到该点所属的组的标识。如果需要调用union方法,首先需要判断两个节点是否连通。因此也需要调用find方法。所以并查集的优化的关键就是如何优化find方法。不同的优化方案就有不同的实现。这里我们对这几种方法进行简单讨论。
并查集的实现
Level 1 : quick-find
首先我们要知道在上面的并查集结构中,我们的int[] id数组。数组中保存的是该数字所对应的分量标识。因此在查找一个数所属哪个组,则直接读取id[p]就可以了。因此find(int p)的复杂度是O(1)。
单看find(int p),好像已经是最优的了。但是在维护id[]数组的时候,通过union(int p,int q)进行维护的。我们直接看union的实现代码:
//直接从书中copypublic void union(int p, int q) { // 获得p和q的组号 int pID = find(p); int qID = find(q); // 如果两个组号相等,直接返回 if (pID == qID) return; // 遍历一次,改变组号使他们属于一个组 for (int i = 0; i < id.length; i++) if (id[i] == pID) id[i] = qID; count--; }
可以看到对于id数组的维护,会遍历整个所有的数字。因此效率非常低。因此我们需要对union进行优化。
Level 2 : quick-union
因为各个节点之间是相互独立没有联系,因此我们现在需要的是将各个节点更好地组织起来,以达到可以快速查找、修改节点所属组的目的。这里选用的数据结构为树。
将id[]数组的意义变化一下。id[p]代表p点的父节点。这样从p点一层层遍历上去,总会到达根节点。当两个节点都走到了一个根节点,那么就说明两个点是连通的。
private int find(int p) { // 寻找p节点所在组的根节点,根节点具有性质id[root] = root while (p != id[p]) p = id[p]; return p; } public void union(int p, int q) { // Give p and q the same root. int pRoot = find(p); int qRoot = find(q); if (pRoot == qRoot) return; id[pRoot] = qRoot; // 将一颗树(即一个组)变成另外一课树(即一个组)的子树 count--; }
貌似现在比第一个快很多。但是既然是树的结构,我们就得考虑树的遍历问题。我们知道树的结构形状对于遍历的时间影响很大。我们希望构造的树越平衡越好,以达到对数级的访问。但是现在的树,最坏情况下会达到N的级别。因此我们的优化方向就是对于树的构造进行控制,使其尽量平衡,高度尽量低。
Level 3 : 加权 quick-union
对于树的构造,假如我们在将两个子树合并的时候,将低的子树接入高的那个子树,那么整个大子树的高度就会比较小,可能高度不会增加。因此我们只需要修改一下合并子树时的代码,进行一下高度判断。
public void union(int p, int q) { int i = find(p); int j = find(q); if (i == j) return; // 将小树作为大树的子树 if (sz[i] < sz[j]) { id[i] = j; sz[j] += sz[i]; } //区别 else { id[j] = i; sz[i] += sz[j]; } count--; }
这下构造的树比较平衡了,在平均情况下,加权方法的union和find,都是lgN的复杂度。但是还有没有可以优化的地方呢?有的。同样是对树的结构进行优化
Level 4 : 路径压缩的加权 quick-union
当我们判断一个节点是否在一个树中的时候,需要从当前节点回退回根进行判断。那假如我的节点都直接连接在根节点上的,那么就可以常数级别完成比较。
但是想法是很好的,在具体的实现过程中,会有大量的中间数据产生,因此其实也不是很好。但是我们仍然可以做一些优化。那就是让id[p]保存p点的爷爷节点。这样就比父的方法更高效了。
private int find(int p) { while (p != id[p]) { // 将p节点的父节点设置为它的爷爷节点 id[p] = id[id[p]]; p = id[p]; } return p; }
总结
对上述四中方法的实验对比,发现将加权方法是最快的,而且优化空间不大。在实际问题中,实现加权方法就足够了
- 并查集(Union-Find)
- Union Find 并查集
- 并查集Union--Find
- 并查集Union-Find
- union-find(并查集)
- 并查集- Union-Find
- union find(并查集)
- 并查集 (Union-Find Sets)
- 并查集 (Union-Find Sets)
- 并查集(Union-Find)算法介绍
- 并查集 (Union-Find Sets)
- 并查集(Union-Find)算法介绍
- 并查集Union-Find Sets
- 并查集算法(Union-Find)
- 并查集(Union-Find)算法介绍
- 并查集(Union-Find)算法介绍
- 并查集(Union-Find)算法介绍
- 并查集-(union-find sets)
- LeetCode Number Complement
- JAVA文件操作
- Eclipse上安装GIT插件EGit及使用
- jeesite快速开发平台(五)---内置组件的应用
- B
- 并查集- Union-Find
- c#自定义泛型列表
- React Native 热更新实现
- 2017互联网笔试题汇总
- 《ACM程序设计》书中题目--problem v
- 【程序1】组成互不相同且无重复数字的三位数
- Eclipse插件安装4种方法
- 大数据工程师(开发)面试系列(7)
- 2017Google Study Jams之L1Android Studio的安装与生日贺卡的实现