1 什么是union-find算法
1.1 定义
考虑以下问题:
1. java中一个对象引用另外一个对象,而另外一个对象又引用另外一个新的对象,我们如何判断两个对象是否已经存在引用关系?
2. 网络通讯的电缆结构中,一个连接点连接另外一个连接点,而另外一个连接点又连接一个新的连接点,我们如何判断两个连接点之间是否存在连接关系呢?
3. 快递物流路线中,一个物流点开通了通向另外一个物流点的路线,而另外一个物流点又开通了和另外一个新的物流点的路线,我们如何判断两个物流点是否已经开通了路线呢?
以上几个问题其实是一个问题:动态连通性问题,而union-find
算法就是为了解决动态连通性问题而产生的。
1.2 概念
连接: 动态连通性中的每条线性连接就是一个连接
触点:连接上的每个节点称为触点
2 接口
为了简化union-find
算法的研究,我们把union-find
算法总结为以下几个功能:
package com.niuniu.studyalgorithm.unionfind;
/**
* @author 002991
*/
public interface UnionFind {
/**
* 以整数标识初始化N个触点
* @param n
*/
void uf(int n);
/**
* 连接q和q
* @param p
* @param q
*/
void union(int p, int q);
/**
* 查找p所在的连通标识符
* @param p
* @return
*/
int find(int p);
/**
* 判断p和q是否是连通的
* @param p
* @param q
* @return
*/
boolean connected(int p, int q);
/**
* 统计连通的数量
* @return
*/
int count();
}
上面接口中,我们用一个整数代表一个触点,用一个数组来盛放我们所有的触点。下面是接口的通用实现:
package com.niuniu.studyalgorithm.unionfind;
/**
* @author 002991
*/
public abstract class UnionFindImpl implements UnionFind{
protected int[] id;
protected int count;
@Override
public void uf(int n) {
this.count = n;
id = new int[n];
for(int i = 0; i < n;i++){
id[i] = i;
}
}
@Override
public abstract void union(int p, int q);
@Override
public abstract int find(int p);
@Override
public boolean connected(int p, int q) {
return find(p) == find(q);
}
@Override
public int count() {
return count;
}
}
3 quick-find实现
3.1 核心思路
数组中的每个元素的下标为触点,每个元素的值为触点值,触点值相等的的触点在一个连接上所以:
1. 要判断两个触点是否在一个连接上,只需要判断两个触点对应的触点值是否相等即可
2. 要把不在一个连接上的触点连接起来,只需要让两个触点的触点值相等即可。
3.2 代码实现
package com.niuniu.studyalgorithm.unionfind;
/**
* @author 002991
*/
public class QuickFindUnionFindImpl extends UnionFindImpl{
@Override
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 --;
}
@Override
public int find(int p) {
return id[p];
}
}
3.3 优缺点分析
find操作很快,但是union操作每次都要遍历整个数组,效率低下
4 quick-union实现
4.1 核心思路
采用树的结构,数组的每个元素下标为触点,每个元素的值为另外一个触点,最终有一个触点对应的值为他自己,这个触点就是根触点,所以:
1. 判断两个触点是不是再一个连接上,只需要判断两个触点的根触点是不是一样即可
2. 连接两个不在一个连接上的触点只需要把一个触点的根触点指向另外一个触点的根触点即可
4.2 代码实现
package com.niuniu.studyalgorithm.unionfind;
/**
* @author 002991
*/
public class QuickUnionUnionFindImpl extends UnionFindImpl{
@Override
public void union(int p, int q) {
int pid = find(p);
int qid = find(q);
if(pid == qid){
return;
}
id[pid] = qid;
count--;
}
@Override
public int find(int p) {
while (p != id[p]){
p = id[p];
}
return p;
}
}
4.3 优缺点分析
union操作很快,但是find操作效率在极端的情况(一个线型的树)效率很慢
5 加权的quick-union实现
5.1 核心思路
quick-union
的实现实际上是对树的操作,而树的层级越低,效率就越高,当一个层级很高的数连接到一个层级很低的树上的时候,就会导致效率很低。所以我们再增加一个数组记录每个连接的触点数,我们连接的时候只把触点少的树连接到触点多的树上就能提高效率了。
5.2 代码实现
package com.niuniu.studyalgorithm.unionfind;
/**
* @author 002991
*/
public class WeightQuickUnionUnionFindImpl extends QuickUnionUnionFindImpl{
/**
* 每个连接的触点数量
*/
private int[] sz;
@Override
public void uf(int n) {
super.uf(n);
sz = new int[n];
for(int i = 0; i < n;i++){
sz[i] = i;
}
}
@Override
public void union(int p, int q) {
int pid = find(p);
int qid = find(q);
if(pid == qid){
return;
}
//判断后再连接,把小树连接到大树上面
if(sz[pid] < sz[qid]){
id[pid] = qid;
sz[qid] += sz[pid];
}else{
id[qid] = pid;
sz[pid] += sz[qid];
}
count--;
}
}
5.3 优缺点总结
加权的quick-union算法是以上三种算法中最理想的一种。