union-find算法研究
union-find 是对图的一些算法的讨论。在我们的讨论中,我们使用整数标识符来作为图的一个触点。
术语
动态连通性
如果 q 和 p 是相连的,那么:
自反性:p 和 p 也是相连的
对称性:p 和 q 也是相连的
传递性:如果 p 和 q 是相连的,而 q 和 r 是相连的,那么 p 和 r 是相连的。
数学集合
将对象称为触点,将整数对称为连接,将等价类称为连通分量或是简称分量。
union-find算法的API
public class UF
返回类型 | 方法名 | 描述 |
---|---|---|
UF(int N) | 以整数标识(0到N-1)初始化N个触点 | |
void | union(int p, int q) | 在 p 和 q 之前添加一条连接 |
int | fint(int p) | p 所在的分量的标识符 |
boolean | connected(int p, int q) | 如果 p 和 q 存在于同一个分量中则返回 true |
int | count() | 分量的个数 |
union-find算法实现
一开始我们有 N 个分量,每个触点都构成了一个只含有自己的分量,因此我们将 id[i] 的值初始化为 i,其中 i 在 0 到 N-1之间。对于触点 i 我们将 find() 方法用来判定它所在的分量所需的信息保存在 id[i] 中。connected() 方法的实现只用一条语句 find(p)==find(q),它返回一个布尔值,我们所有的方法的实现都会用到connected()方法
我们维护了两个实例变量,一个是连通分量的个数,一个是数组 id[ ]。
package com.li.union_find;
/*并查集undion-find的简单实现*/
public class Union_find {
private int[] id; //分量id(以触点作为索引)
private int count; //分量数量
/*初始化分量id数组*/
public Union_find(int N) {
count=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) {
//详细实现
}
public boolean connected(int p, int q) {
return find(p)==find(q);
}
public void union(int p, int q) {
//详细实现
}
}
quick-find算法
一种方法是保证当且仅当 id[p] 等于 id[q],p和q是连通的.即同一连通分量中的所有触点 id[ ]的值全部相同,这意味着connected()只需判断id[p]和id[q]的值是否相等即可.
调用union()将p和q归并到相同的分量中,如果 id[p] == id[q],则不需要进行任何改变,否则遍历整个数组id,将所有和 id[p] 相等的元素变成 id[q],这样我们就将 p 所在的整个分量加入了 q 所在的连通分量中,当然也可以将所有和id[q]相等的元素变成id[p]。
public int find(int p) {
return id[p];
}
/*将p和q归并到相同的分量中*/
public void union(int p, int q) {
//如果已经在同一分量中,不采取行动
if(id[p]==id[q])
return;
int pID=find(p);
int qID=find(q);
for(int i=0; i<id.length; i++) {
if(id[i]==pID)
id[i]=qID;
}
count--;
}
quick-union算法分析
find() 操作的速度显然很快,因为它只需要访问一次数组。但是此算法的 union() 操作一般无法处理大型的问题,因为对于每一对输入 union() 都需要扫描整个 id[] 数组。
quick-union算法
这次我们讨论的重心就是提高 union() 方法的速度,可以说,它和 quick-find 算法时互补的。我们知道,链表的连接以及添加是很快的,我们可以基于这种结构来重新构造相同的数据结构。
如下:每个触点所对应的 id[] 元素都是同一个分量中的另一个触点的名称(也可能是它自己)——我们将这种联系称为链接。在实现 find() 方法时,我们从给定的触点开始,由它的链接得到另一个触点,再由这个触点的链接到达第三个触点,如此继续跟着链接直到到达一个跟触点,即链接指向自己的触点。
package com.li.union_find;
public class Quick_union {
private int[] id; //分量id(以触点作为索引)
private int count; //分量数量
/*初始化分量id数组*/
public Quick_union(int N) {
count=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) {
//找出分量的名称
while(p!=id[p])
p=id[p];
return p;
}
public boolean connected(int p, int q) {
return find(p)==find(q);
}
/*将p和q归并到相同的分量中*/
public void union(int p, int q) {
int pRoot=find(p);
int qRoot=find(q);
if(qRoot==pRoot)
return;
id[pRoot]=qRoot;
count--;
}
}
森林的表示
我们如果用节点(带标签的圆圈)表示触点,用从一个节点到另一个节点的箭头表示链接,由此得到数据结构的图像表示使我们理解算法的操作变得相对容易。我们得到的结构是树——从技术上讲,id[ ] 数组用父链接的形式表示了一片森林。
定义
一棵树的大小是它的节点的数量。树中的一个节点的深度是它到根节点的路径上的链接数。数的高度是它的所有节点中的最大深度。
命题
quick-union 算法中的 find() 方法访问数组的次数为 1 加上给定触点所对应的节点中的深度的两倍。union() 和 connected()访问数组的次数为两次 find() 操作(如果 union() 中给定的两个触点分别存在于不同的树中则还需要加 1)。
加权 quick-union算法
与其在 union() 中随意将一棵树连接到另一棵树,我们现在回记录一棵树的大小并总是将较小的树连接到较大的树上。这项改动需要添加一个数组和一些代码来记录树中的节点数,它能够大大改进算法的效率,我们将它称为加权 quick-union 算法。
package com.li.union_find;
public class WeightedQuickUnion {
private int[] id; //分量id(以触点作为索引)
private int count; //分量数量
private int[] sz; //各个根节点所对应的分量的大小
public WeightedQuickUnion(int N) {
count=N;
id=new int[N];
for(int i=0; i<N; i++)
id[i]=i;
sz=new int[N];
for(int i=0; i<N; i++)
sz[i]=1;
}
public int count() {
return count;
}
public int find(int p) {
//找出分量的名称
while(p!=id[p])
p=id[p];
return p;
}
public boolean connected(int p, int q) {
return find(p)==find(q);
}
public void union(int p, int q) {
int pRoot=find(p);
int qRoot=find(q);
if(pRoot==qRoot)
return;
/*将小树的根节点连接到大树的根节点*/
if(sz[pRoot]<sz[qRoot]) {
id[pRoot]=qRoot;
sz[qRoot]+=sz[pRoot];
}
else {
id[qRoot]=pRoot;
sz[pRoot]+=sz[qRoot];
}
count--;
}
}
算法分析
把小树连接到较大的树上,保证了树的平均深度较小,从而在进行 find() 操作时运行时间更少。
命题:对于 N 个触点,加权 quick-union 算法构造的森林中的任意节点的深度最多为 lgN l g N 。
证明:归纳法证明一个更强的命题,即森林中大小为 K 的树的高度最多为 lgK l g K 。
推论:对应加权 quick-union 算法和 N 个触点,在最坏的情况下,find()、connected() 和 union() 的成本的增长数量级为 lgN l g N 。
各种 union-find 算法的性能特点
在最坏的情况下
最优算法
理想情况下,我们希望每个节点都直接链接到它的根节点上,但我们又不希望像 union-find 算法那样通过修改大量链接做到这一点,这时可以通过在检查节点的同时把它直接链接到根节点上面去。
要实现路径压缩,只需要为find()添加一个循环,将在路径上遇到的节点全部链接到根节点。
路径压缩的加权quick-union算法是最优的算法,但并非所有操作都能在常数时间内完成。
package com.li.union_find;
import com.li.stdlib.StdIn;
import com.li.stdlib.StdOut;
/*使用路径压缩的加权 quick-union 算法*/
public class WeightedQuickUnionPathCompressionUF {
private int[] parent; // parent[i] = parent of i
private int[] size; // size[i] = number of sites in tree rooted at i
private int count; // number of components
public WeightedQuickUnionPathCompressionUF(int N) {
count = N;
parent = new int[N];
size = new int[N];
for (int i = 0; i < N; i++) {
parent[i] = i;
size[i] = 1;
}
}
public int count() {
return count;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
public int find(int p) {
validate(p);
int root = p;
/* find the root */
while (root != parent[root])
root = parent[root];
/* path compression */
while (p != root) {
int newp = parent[p];
parent[p] = root;
p = newp;
}
return root;
}
// validate that p is a valid index
private void validate(int p) {
int N = parent.length;
if (p < 0 || p >= N) {
throw new IllegalArgumentException("index " + p + " is not between 0 and " + (N - 1));
}
}
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ)
return;
// make smaller root point to larger one
if (size[rootP] < size[rootQ]) {
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
} else {
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
}
count--;
}
public static void main(String[] args) {
int n = 10;
WeightedQuickUnionPathCompressionUF uf = new WeightedQuickUnionPathCompressionUF(n);
while (!StdIn.isEmpty()) {
int p = StdIn.readInt();
int q = StdIn.readInt();
if (uf.connected(p, q))
continue;
uf.union(p, q);
StdOut.println(p + " " + q);
}
StdOut.println(uf.count() + " components");
}
}