文章参考自博客:https://www.cnblogs.com/SeaSky0606/p/4752941.html
1、前言
为了便于引入算法,下面我们假设一个场景:
假设现在有A,B两人素不相识,但A通过熟人甲,甲通过熟人乙,乙通过熟人丙,丙通过熟人丁,而丁又刚好与B是熟人。就这样,A通过一层一层的人际关系最后认识了B。
基于以上介绍的“关系网”,现在给出一道思考题:13亿中国人当中一共有几个“关系网”呢?
如果用图论的知识来说,就是图中有多少个连通子图呢?
2、Union-Find初探
是的,想到1,300,000,000这个数字,或许此刻你大脑已经懵了。那好,我们就先从小数据分析:
从上图中,其实很好理解。初始每个人都是单独的一个“点”,用科学语言,我们把它描述为“连通分量”。随着一个一个关系的确立,即点与点之间的连接,每连接一次,总连通分量数即减1(理解算法的关键点之一)。最后的“关系网”几乎可以很轻易地数出来。所以,只要你把所有国人两两之间的联系给出,然后不断连线,连线,...,最后再统计一下不就完事儿了麽~
问题是:怎么存储点的信息?点与点怎么连,怎么判断该不该连?
因此,我们需要维护2个变量,其中一个变量count表示实时的连通分量数,另一个变量可以用来存储具体每一个点所属的连通分量。因为不需要存储复杂的信息。这里我们选常用的数组 id[N] 存储即可。然后,我们需要2个函数find(int x)和union(int p,int q)。前者返回点“x”所属于的连通分量,后者将p,q两点进行连接。注意,所谓的连接,其实可以简单的将p的连通分量值赋予q或者将q的连通分量值赋予p,即:
id[p]=q 或者id[q]=p。
有了上面的分析,我们就可以牛刀小试了。且看Java代码实现第一版。
package UnionFind;
import java.util.Scanner;
public class BasicUnionFInd {
int count; //连通分量数
int[] id; //每个数所属的连通分量
public BasicUnionFInd(int N){
count = N;
id= new int[N];
for(int i=0;i<N;i++){
id[i] = I;
}
}
//返回连通分量数
public int getCount(){
return this.count;
}
//查找x所属的连通分量
public int find(int x){
return id[x];
}
//将两个点进行合并
public void union(int p,int q){
int pID = find(p);
int qID = find(q);
for(int i=0;i<id.length;i++){
if(find(i)==pID){
id[i] = qID;
}
}
count--;
}
//判断两个点是否连通
public boolean connected(int p,int q){
return find(p) == find(q);
}
public static void main(String[] args){
Scanner input = new Scanner(System.in);
System.out.println("please input the count of total numbers");
int N = input.nextInt();
BasicUnionFInd buf = new BasicUnionFInd(N);
System.out.println("please input pairs,input exit to end");
while(input.hasNext()){
String pair = input.next();
if(pair.equals("exit")) break;
int p = Integer.parseInt(pair.split("-")[0]);
int q = Integer.parseInt(pair.split("-")[1]);
if(buf.connected(p,q)) continue;
buf.union(p,q);
}
System.out.println("总的连通分量数是:" + buf.getCount());
}
}
代码的输入输出为:
可以看到,find()操作的时间复杂度为:O(l),Union的时间复杂度为:O(N)。因为算法可以非常高效地实现find(),所以我们也把它称为“quick-find”算法。
3.Union-find进阶
仔细一想,我们上面再进行union()连接操作时,实际上就是一个进行暴力“标记”的过程,即把所有连通分量id跟点q相同的点找出来,然后全部换成p的id。算法本身没有错,但是这样的代价太高了,得想办法优化~
因此,这里引入了一个抽象的“树”结构,即初始时每个点都是一棵独立的树,所有的点构成了一个大森林。每一次连接,实际上就是两棵树的合并。通过,不断的合并,合并,再合并最后长成了一棵棵的大树。
package UnionFind;
import java.util.Scanner;
public class UnionFind_v1 {
int count; //连通分量数
int[] id; //每个数所属的连通分量
public UnionFind_v1(int N){
count = N;
id= new int[N];
for(int i=0;i<N;i++){
id[i] = I;
}
}
//返回连通分量数
public int getCount(){
return this.count;
}
//查找x所属的连通分量
public int find(int x){
while(x!=id[x]) x=id[x];
return x;
}
//将两个点进行合并
public void union(int p,int q){
int pID = find(p);
int qID = find(q);
if(pID==qID) return;
id[qID] = pID;
count--;
}
//判断两个点是否连通
public boolean connected(int p,int q){
return find(p) == find(q);
}
public static void main(String[] args){
Scanner input = new Scanner(System.in);
System.out.println("please input the count of total numbers");
int N = input.nextInt();
BasicUnionFInd buf = new BasicUnionFInd(N);
System.out.println("please input pairs,input exit to end");
while(input.hasNext()){
String pair = input.next();
if(pair.equals("exit")) break;
int p = Integer.parseInt(pair.split("-")[0]);
int q = Integer.parseInt(pair.split("-")[1]);
if(buf.connected(p,q)) continue;
buf.union(p,q);
}
System.out.println("总的连通分量数是:" + buf.getCount());
}
}
代码的输入输出为:
利用树本身良好的连通性,我们算法仅需要O(l)时间代价进行union()操作,但此时find()操作的时间代价有所增加。结合本算法对quick-find()的优化,我们把它称为“quick-union”算法。
4.Union-Find再进阶
表面上,上述引入“树”结构的算法时间复杂度由原来的O(N)改进为O(lgN)。但是,不要忽略了这样一种极端情况,即每连接一个点之后,树在不断往下生长,最后长成一棵“秃树”(没有任何树枝)。
为了不让我们前面做的工作白费,必须得采取某些措施避免这种恶劣的情况给我们算法带来的巨大代价。所以...
是的,或许你已经想到了,就是在两棵树进行连接之前做一个判断。每一次都优先选择将小树合并到大树下面,这样子树的高度不变,能避免树一直往下增长了!下图中,数据增加了“6-2”的一条连接,得知以“2”为根节点的树比“6”的树大,对比(f)和(g)两种连接方式,我们最优选择应该是(g),即把小树并到大树下。
基于此,我们还得引入一个变量对以每个结点为根节点的树的大小进行维护,具体我们以sz[i]表示i结点代表的树(或子树)的结点数作为它的大小,初始sz[i]=1。因为现在的每一个结点都有了权重,所以我们也把这种树结构称为“加权树”,本算法称为“weightedUnionFind”。
package UnionFind;
import java.util.Scanner;
public class WeightedUnionFind {
int count; //连通分量数
int[] id; //每个数所属的连通分量
int[] sz;
public WeightedUnionFind(int N){
count = N;
id= new int[N];
sz = new int[N];
for(int i=0;i<N;i++){
id[i] = I;
sz[i] = 1;
}
}
//返回连通分量数
public int getCount(){
return this.count;
}
//查找x所属的连通分量
public int find(int x){
while(x!=id[x]) x=id[x];
return x;
}
//将两个点进行合并
public void union(int p,int q){
int pID = find(p);
int qID = find(q);
if(pID==qID) return;
if(sz[p] < sz[q]){
id[pID] = qID;
sz[q] += sz[p];
}
else{
id[qID] = pID;
sz[p] += sz[q];
}
count--;
}
//判断两个点是否连通
public boolean connected(int p,int q){
return find(p) == find(q);
}
public static void main(String[] args){
Scanner input = new Scanner(System.in);
System.out.println("please input the count of total numbers");
int N = input.nextInt();
BasicUnionFInd buf = new BasicUnionFInd(N);
System.out.println("please input pairs,input exit to end");
while(input.hasNext()){
String pair = input.next();
if(pair.equals("exit")) break;
int p = Integer.parseInt(pair.split("-")[0]);
int q = Integer.parseInt(pair.split("-")[1]);
if(buf.connected(p,q)) continue;
buf.union(p,q);
}
System.out.println("总的连通分量数是:" + buf.getCount());
}
}
代码的输入输出如下:
5总结
下面我们来比较一下算法的性能:
个人认为WeightedUF的union时间复杂度是O(1),原博客应该是写错了。。