并查集(动态连通性问题)
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。
可以将这个过程形象当做江湖门派。
每一个弟子都归属与一个门派,每一个门派都有一个掌门。掌门相同的弟子属于同一个门派
每一个人都单独是一个门派,自己也是掌门。如果门派要合并,那么就将掌门改为合并后的掌门。
在这个过程中,主要就是查找是否是同一门派和合并两个门派的操作
int[] id; //每个弟子的掌门
int count; //门派数量
//判断两个弟子是否是同一门派,即掌门是否相同
boolean connected(int p,int q){
return find(p) == find(q);
}
//查找掌门
int find(int p)
//合并门派
void union(int p,int q)
实现
quick-find算法
将p门派合并到q门派,将所有属于p门派的弟子的掌门改为q门派的掌门。
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;
}
//将p门派合并到q门派
for (int i=0; i<id.length; i++) {
if (id[i] == pID) {
id[i] = qID;
}
count--;
}
}
该算法的find()操作速度很快,因为只需要访问id数组一次。但是对于每一对输入union()都需要扫描整个id[]数组,对于大量的数据显然无法处理。
quick-union算法
这个算法重点是提高union()方法的速度,在上面的算法中,我们每一次合并都要将该门派的所有弟子的掌门都更改为另一个门派的掌门,这样肯定要耗费时间.
我们可以这样处理:让被合并的门派成为另一个门派的一个分部,既然掌门都属于另一个门派了,那么其下的弟子肯定也就合并到了另一个门派了,无需遍历整个id[]数组。
public 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 = find(q);
if (pRoot == qRoot) {
return;
}
id[pRoot] = qRoot;
count--;
}
该算法优化了union()方法,只需要一次操作便能合并。
但是该方法可能会出现一个问题,那就是会加深树的高度,所有的门派都在树的一端,例如10个门派树高为10,100个门派树高为100,对于大量数据,如此高的树对于find()方法的耗时也非常大。
加权quick-union算法
为了解决上面存在的为题,我们只需要在上面的基础简单修改一下便可。
在合并过程中,我们记录树的大小,即每一个门派的弟子数目,小门派要合并到大门派,这样可以明显降低树的高度。
private int[] sz; //记录每个门派的大小
public int find(int p){
while (p != id[p]) {
p = id[p];
}
return p;
}
public void union(int p,int q){
int i = find(p);
int j = find(q);
if (i == j) {
return;
}
//将小门派合并到大门派
if (sz[i] < sz[j]) {
//i合并到j
id[i] = j;
sz[j] += sz[i];
} else{
id[j] = i;
sz[i] += sz[j];
}
count--;
}
路径压缩的加权quick-union算法
该算法是最优算法,在加权quick-union算法的基础上,我们可以尽可能减少树的高度来减少find()所消耗的时间。最好是直接能够直接指向掌门,在找一个弟子的掌门的时候,我们可以直接将其指向掌门。如下:
private int find(int i){
while(i != id[i]){
id[i] = id[id[i]]; //将当前的父亲节点变为爷爷节点, 父亲节点为根节点则为其自身
i = id[i];
}
return i;
}