动态连通性:union-find算法(常规搜索、树状触点搜索、加权树搜索的算法分析)

时间:2021-12-29 14:36:11

算法

提出动态连通性问题:输入一列整数对,其中每个整数都表示一个某种类型的对象,一对整数对p和q可以被理解为p和q是相连的。

他们具有

自反性:p和p相连;

对称性:p和q相连,q和p相连;

传递性:p和q相连,q和r相连,那么p和r相连。

当程序从输入中读取了整数对p q时,如果已知的所有整数对都不能说明p 和q相连,那么程序应该忽略p和q这对整数继续处理输入中的下一对整数。

我们需要设计一个数据结构来保存程序已知的所有整数对的足够多的行系,并用他们来判断一堆新对象是否相连。


API

UF(int n) 以整数标识(0到n-1)初始化N个触点

void union(int p,int q)在p和q之间添加一条连接

int find(int p) p(0到n-1)所在的分量的标识符

boolean connected(int p,int q)若p和q存在同一连通分量重则返回true

int count() 记录连通分量的数量

定义一种数据结构表示已知连接,可以采用数组。

这是常规搜索的方法:

<span style="font-size:24px;">public int find(int p){
return id[p];

}
public void union(int p,int 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 -- ;
}</span>

常规搜索只能够用于问题规模比较小的情况。

我尝试输入10000个数,5000个连通分量,然后就卡住了.

也可能是算法有问题,但是常规搜索的算法时间复杂度是O(n`2),也就是每一次都要检索一次全数组。

第二种情况是把连通分量的根节点穿起来成一个树,实现代码如下:

private int find(int p){
while(p!=id[p]){
p = id[p];
}
return p;
}
public void union(int p,int q){
int pRoot = find(p);
int qRoot = fidn(q);
if(pRoot == qRoot){
return ;
}
id[pRoot] = qRoot;
count--;
}

这样影响搜索算法的时间复杂度的因素就只有树的高度了,树越高,那么算法执行的时间就越长,时间复杂度在O(n)~O(n`2)之间,取决于树的深度,当然最坏的情况并不比常规搜索要好多少。

虽然第二个树状触点搜索比第一个常规搜索要好,但是该算法还是可以改进的,接下来呼之欲出的毫无疑问就是加权树的搜索算法了,加权树以空间换取时间的代价,建立一个权值表,每一次归并都把小树插到大树的根节点上,这样树的深度就不会最坏到达n了。

对于N个触点,任意节点的深度最多为lgN。

均摊成本非常接近但没有到达1。

下面就是加权搜索树执行的全部代码:

package 动态连通_union_find算法;

import java.util.Random;

public class UF {
private int[] id;//分量id
private int count;//分量数量
private int[] sz;//各个根节点对应的分量大小
public UF(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;//赋值权重为1
}
}
public int count(){//返回分量数量
return count;
}
public boolean connected(int p,int q){//检测是否连接p 和q
return find(p) == find(q);
}
public int find(int p){//返回p下标的内容
while( p != id[p] ){
p = id[p];
}
return p;
}
public void unionFinal(int p,int q){//归并分量方法
int i = find(p);
int j = find(q);
if( i == j ){
return ;
}
//将小树的根节点连接到大树的根节点
if( sz[i] < sz[j] ){//若是j的权重高,i节点就等于j,j的权重加上i的权重
id[i] = j;
sz[j] += sz[i];
}else{
id[j] = i;//若是i的权重高,相反操作
sz[i] += sz[j];
}
count --;
}
public void print(int[] id){//输出每一个数字的结果
int length = id.length;
while( length > 0 ){
if( (id.length-length) % 30 == 0 ){
System.out.println();
}
System.out.print( id[id.length-length] + " " );
length -- ;
}
}
public static void main(String[] args) {//主函数
long time1 = System.currentTimeMillis();
int n = 1000000;//触点数
UF uf = new UF(n);
Random ran = new Random();
int line = 800000;//连线数
while( line > 0 ){
int p = ran.nextInt(n);
int q = ran.nextInt(n);//创建两个随机点,随机点数为0~n-1
if( uf.connected(p, q) ){//判断是否已经连通
continue;
}
uf.unionFinal(p, q);//归并分量
line -- ;
}
long time2 = System.currentTimeMillis();
System.out.println(time2 - time1);
}
}


加权树搜索算法处理N个触点和M条连接时最多访问数组cMlgN次,c为常数,这个结果跟quick-find算法需要至少访问数组MN次形成了鲜明的对比。





摘自《Algorithms》(《算法》(中文版)):

我们从union-find问题中可以看到,算法设计在解决实际问题时能够为程序的性能带来惊人的提高,这种潜力使它成为热门研究领域。还有什么其他类型的设计行为可能将成本降低为原来的数百万甚至数十亿分之一呢?




设计高效的算法是一种很有成就感的智力活动,同时也能够产生直接的经济效益。正如动态连接性问题所示,为解决一个简单的问题我们学习了许多算法,他们不但有用有趣,也精巧而引人入胜。随着计算机算法在科学和商业领域的应用范围越来越广,能够使用高效的算法来解决老问题并为新问题开发有效的解决方案也越来越重要了。