动态联通性问题--union-find算法

连通性问题

在给定的一张节点网络(也就是图)中,判断两个节点之是否可达的问题就是连通性问题。java

场景:判断两个用户之间是否存在间接社交关系;判断两台计算机之间是否创建链接等。算法

定义数据结构

使用最基本的数组做为该算法的数据结构。数组下标 i 表明当前节点编号,id[i]的值表示与该节点连通的某一个节点。每一个节点id[i]的值初始化为 i。数组

定义输入

输入是一系列整数对,一对整数(p, q)表明p和q是相连的。网络

例如:输入(3, 4)、(1,3)、(2, 5),那么3-四、1-三、1-四、2-5是连通的。数据结构

定义union-find算法API

public class UF{优化

      void union(int p,int q)                   //在p和q之间创建链接ui

      int find(int p)                                 //p所在的份量的标识符spa

      boolean connected(int p,int q)     //p和q同在一个份量中则为truecode

}对象

初始状态,每一个节点都是一个份量。两点之间创建链接后,union()方法会将两个份量合并。一个份量中各触点都相互链接。find()方法返回给定触点所在连通份量的标识符。

connected()方法即return find(p)==find(q);  因此关键是实现find()方法和union方法。

1  quick-find算法

quick-find算法保证在同一连通份量中全部触点id[]中的值必须相同。这种实现状况下:

  • union()必须遍历数组,将一个连通份量中的id[]值变为另外一个连通份量的id[]值
  • find方法只需return id[p]
  • connected()方法只需判断find[p]==find[q]便可

换一种思路,其实quick-find算法的核心是将每个连通份量以背包的形式存放。

算法实现:

//union()方法用于合并两个连通份量
public void qf_union(int p,int q) {
	int pID = qk_find(p);
	int qID = qk_find(q);
	if(pID == qID) return;
    //由于不知道p所在的连通份量的全部节点,须要全扫描节点数组
	for(int i = 0;i<id.length;i++)
		if(id[i] == pID) id[i] = qID;
}

//find()方法实现简单,直接返回数组值
public int qf_find(int p) { return id[p]; }

//connected()方法返回是否相等
public boolean connected(int p, int q){ return find(p) == find(q); }

算法分析:

该算法的特色是union慢,find快。在quick-find算法中,每次find()调用访问一次数组,常数级别O(1)。归并两个份量的union()操做访问数组次数时间复杂度O(N)。

2  quick-union算法

quick-union算法中每一个触点所对应的id[]元素都是另外一个触点的名称(也多是本身,若是是本身的画说明是根节点),触点之间循环这种关系直到到达根触点。当且仅当两个触点开始这个过程打到同一个根触点说明它们存在于一个连通份量中。这种实现状况下:

  • find()方法就是沿着这条路径找到根节点
  • union()方法只需将一个根节点连接到另外一个上面就可实现合并份量
  • connected()方法只需判断find[p]==find[q]便可

换一种思路,quick-union算法的核心是将连通份量以多叉树的形式存放。

算法实现:

//find()方法须要沿着多叉树向上找到本身的根节点
public int qu_find(int p) {
	while(p!=id[p]) p=id[p];
	return p;
}

//union()方法只须要将一个根节点链接到另外一个根节点便可
public void qu_union(int p,int q) {
	int pRoot = qu_find(p);
	int qRoot = qu_find(q);
	if(pRoot == qRoot) return;
	id[pRoot] = qRoot;
}

//connected()方法简单比较find(p)和find(q)
public boolean connected(int p, int q){ return find(p) == find(q); }

算法分析:

该算法的特色是union快,find慢。find()方法访问数组次数是1+触点所在树的高度*2,即时间复杂度是O(logN);union()方法和connected()方法时间复杂度是常数级别O(1)。

3  加权quick-union算法:

对quick-union算法的改进,保证小的树连接在大树上。即给每个连通份量添加权重,在须要将两个连通份量合并时,将权重小的连通份量链接到大的连通份量上。

算法实现:

在类中新建一个数组保存各根节点的权重。注意该数组中只有根结点对象的下标中的数据有效。而后修改union方法以下:

public void union(int p, int q) {
    int rootP = find(p);
    int rootQ = find(q);
    if (rootP == rootQ) return;
    //将权重小的连通份量链接到权重大的上面
    if (size[rootP] < size[rootQ]) {
        parent[rootP] = rootQ;
        size[rootQ] += size[rootP];
    }else {
        parent[rootQ] = rootP;
        size[rootP] += size[rootQ];
    }
}

算法分析:

加权quick-union算法能够有效地下降生成的连通份量的树的高度,从而提升算法执行效率。固然这是一种用空间换时间的方法,由于使用了辅助数组保存节点权重,因此它的额外空间复杂度为O(N)。

4  路径压缩的加权quick-union算法:

加权quick-union算法在大部分整数对都是直接链接的状况下,生成的树依旧会比较高。因此能够进一步优化:每次计算某个节点的根结点时,将沿路检查的结点也指向根结点。尽量的展平树,这样将大大减小find()方法遍历的结点数目。

算法实现:

//union()方法
public void union(int p, int q) {
    int rootP = find(p);
    int rootQ = find(q);
    if (rootP == rootQ) return;
    if (rank[rootP] < rank[rootQ]) parent[rootP] = rootQ;
    else if (rank[rootP] > rank[rootQ]) parent[rootQ] = rootP;
    else {
        parent[rootQ] = rootP;
        rank[rootP]++;
    }
}

//find()方法
public int find(int p) {
    while (p != parent[p]) {
        parent[p] = parent[parent[p]];    //路径压缩减半
        p = parent[p];
    }
    return p;
}

//connected()方法
public boolean connected(int p, int q) { return find(p) == find(q); }

算法分析:

路径压缩后基本上连通份量树的高度为2, 因此find()方法的时间复杂度接近O(1),union()方法的时间复杂度接近O(1)。

附:union-find算法和图的可达性问题

图的可达性问题通常采用深度优先遍历的思想来实现。理论上,深度优先算法解决图的可达性比union-find快,由于它可以保证所需时间是线性的。

但实际上,union-find算法更快,由于它不须要完整的构造并表示一张图。更重要的是union-find算法是一种动态算法,咱们在任什么时候候都能用接近常数的时间检查两个顶点是否连通,甚至在添加一条边的时候,但深度优先算法必须对图进行预处理。

相关文章
相关标签/搜索