问题描述
动态连通性问题:给定n个顶点,仅支持输入一对整数,一对整数p和q可以被理解为“p和q是相连的”。给定任何一对顶点,判断其是否连通。
Union-find数据模型
目标:为union和find操作设计一个高效的数据结构
先设计一份API来封装所需的基本操作:初始化、连接两个连通分量、判断包含顶点的连通分量、判断两个顶点是否在同一连通分量以及所有连通分量的个数
Union-find算法
1)Quick-find
数据结构:id[i]是顶点i的所属连通分量的标识
那么,
Find操作:找出顶点i的id,即id[i]
Connected操作:p和q是否有相同的id
Union操作:将p和q归入到一个连通分量中,将p所在的连通分量的所有顶点id改成id[q]
/**
* find操作只需要访问一次数组,而union操作访问数组次数在(N+3)~(2N+1)之间
* @author zhang
*
*/
public class QuickFindUF {
private int[] id; //代表所属的连通分量
private int count; //连通分量的总数量
public QuickFindUF(int n) {
count = n;
id = new int[n]; // 对数组id进行初始化
for(int i = 0; i < n; i++) {
id[i] = i; //初始时,每个顶点就是一个连通分量
}
}
public void union(int p ,int q) {
//在p和q之间添加一条连接,将p和q归入到一个连通分量中
int pID = find(p);
int qID = find(q);
//如果q和p同属一个连通分量,则不做任何操作
if(pID == qID) return;
//否则,将p所在的连通分量的所有顶点id改成qID
for(int i = 0; i < id.length; i++) {
if(id[i] == pID) id[i] = qID; //访问数组次数为O(n)
}
count--;
}
public int find(int p) {
return id[p];
}
public boolean connected(int p ,int q) {
return find(p) == find(q);
}
public int count() {
return count;
}
}
其各个操作的cost如下:
存在的问题:Union操作代价太高了!!!一共要进行n-1次union,那么时间复杂度为O(n^2),这显然代价太高了!
2)Quick-union
数据结构:与Quick-find不同,id[i]代表的是i的parent,那么顶点i的root就是id[id[id[...id[i]...]]]
那么,
Find操作:找到p所属连通分量的根节点,即id[id[id[...id[i]...]]]
Connected操作:p和q是否有相同的root
Union操作:找到p、q所在连通分量的根节点,将p的根节点的父节点改成q的根节点
public class QuickUnionUF {
private int[] id; //与QuickFindUF不同,id[i]代表的是i的parent
private int count; //连通分量的总数量
public QuickUnionUF(int n) {
count = n;
id = new int[n];
for(int i = 0; i < n; i++) {
id[i] = i; //初始时,每个顶点就是一个连通分量,它的父顶点都是自己
}
}
public void union(int p, int q) {
//即找到p、q所在连通分量的根节点,将p的根节点的父节点改成q的根节点
int i = find(p);
int j = find(q);
//若在同一连通分量上,不做操作
if(i == j) return;
id[i] = j;
count--;
}
public int find(int p) {
//找到p所属连通分量的根节点
while(p != id[p]) {
p = id[p];
}
return p;
}
public boolean connected(int p ,int q) {
return find(p) == find(q);
}
public int count() {
return count;
}
}
各个操作代价:
这种数据结构,可能会造成树很高,一条直链的情况,则Find/connected操作代价会很高,最坏情况为树的高度即N
/**
* 在QuickUnionUF中,union可能会出现长链的情况,现在记录每一棵树的大小,并总是将最小的树加到最大的
* 树上,以减少树的高度,这样find、union、connected操作复杂度为O(lgn)
* @author zhang
*
*/
public class WeightedQuickUnionUF {
private int[] id; //与QuickFindUF不同,id[i]代表的是i的parent
int[] weight; //记录每棵树的大小
private int count; //连通分量的总数量
public WeightedQuickUnionUF(int n) {
count = n;
id = new int[n];
weight = new int[n];
for(int i = 0; i < n; i++) {
id[i] = i; //初始时,每个顶点就是一个连通分量,它的父顶点都是自己
weight[i] = 1; //初始时,每棵树的大小为1
}
}
public void union(int p, int q) {
//即找到p、q所在连通分量的根节点,并将小的树的根节点加到大树的根节点上
int i = find(p);
int j = find(q);
//若在同一连通分量上,不做操作
if(i == j) return;
if(weight[i] < weight[j]) { id[i] = j; weight[j] += weight[i]; }//只需要更改root的权值即可,没必要对每个顶点做更改
else { id[j] = i; weight[i] += weight[j]; }
count--;
}
public int find(int p) {
//找到p所属连通分量的根节点
while(p != id[p]) {
p = id[p];
}
return p;
}
public boolean connected(int p ,int q) {
return find(p) == find(q);
}
public int count() {
return count;
}
}
加权quick-union算法构造的森林中的任意节点的深度最多为lgN,证明如下:其各个操作的代价:
4) 加权Quick-union的继续优化:路径压缩
修改find的代码:
这样,我们得到的结果几乎是扁平化的树,实际上也很难在对加权quick-union进行优化了,路径压缩的加权quick-union算法已经是最优的算法,但并非所有操作都能在常数时间内完成。其union和find非常非常接近但人没有达到1(均摊成本)
在最坏情况下,各个算法的对比: