并查集- 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;      }  

总结

对上述四中方法的实验对比,发现将加权方法是最快的,而且优化空间不大。在实际问题中,实现加权方法就足够了

0 1
原创粉丝点击