Union-Find 合并查找
动态连接性
判断连接性的关键
-
等价关系模型:如果有(a,b),(b,c),那么也会有(a,c)。其中()表示有连接。
-
连通分量:最大的可连通对象集合,有两个特点:1)连通分量内部任意两个对象都是相连通的;2)连通分量内部的对象不与外部对象相连通。
利用连通分量,可以方便地实现并查集的两个操作:查询请求
和合并命令
-
查找:检查两个对象是否在相同的连通分量中
-
合并:将两个对象的分享替换成其并集
快速查找
快速查找是基于贪心策略的一种算法,贪心策略是指在问题求解的时候,只找出当前最优解。
基于此,设计一种数据结构来存储实验对象。
-
长度为N的整型数组
-
如果p和q有相同的id,则表示他们有连接
因此,查找和合并操作就变为:
-
查找:检查p和q是否具有相同的id,若相同则代表连通。如图id[1]=id[2],表示1和2相连通
-
合并:在合并p和q对象时,将值为所有等于id[p]的值重新赋值为id[q]。如图合并0和1,需要将id[0]=id[5]=id[6]=0的值全部赋值为id[1]=1的值
具体算法
public class QuickFind {
private int[] id;
/**
* 构造函数,初始化数据结构的属性
*
* @param N
* 数组额长度
*/
public QuickFind(int N) {
// 初始化数据结构的值
id = new int[N];
for (int i = 0; i < N; i++) {
id[i] = i;
}
}
/**
* 查找操作,判断连通性
*
* @param p
* @param q
* @return
*/
public boolean connected(int p, int q) {
return id[p] == id[q];
}
/**
* 合并操作
*
* @param p
* @param q
*/
public void union(int p, int q) {
int pid = id[p];
int qid = id[q];
for (int i = 0; i < id.length; i++) {
if (pid == id[i]) {
id[i] = qid;
}
}
}
/* setter和getter */
public int[] getId() {
return id;
}
public void setId(int[] id) {
this.id = id;
}
}
时间复杂度分析
算法 | 初始化 | 合并(包含查找) | 查找 |
---|---|---|---|
快速查找 | N | N | 1 |
快速合并
快速合并是基于懒策略的一种算法,懒策略是指在问题求解时尽量避免计算,直到不得不进行计算。
基于此,设计另外一种该数据结构来存储实验对象。
-
长度为N的整型数组
-
id[i]是i的父亲,从而构造成树的结构
-
如果i=id[i],则表示id[i]是树根
因此,合并和查找操作就变为:
-
查找:检查p和q是否具有相同的根,如有则代表连通。
-
合并:在合并p和q对象时,将p的根的id(父亲)设成q的根。
具体算法
public class QuickUnion {
public class QU {
private int[] id;
public QU(int N) {
// 初始化属性值
id = new int[N];
for (int i = 0; i < N; i++) {
id[i] = i;
}
}
/**
* 查找指定值的根
*
* @param i
* 待查找根的值
* @return
*/
public int root(int i) {
// 当id[i]=i时就是根
while (id[i] != i)
i = id[i];
return i;
}
/**
* 查找连通性
*
* @param p
* @param q
* @return
*/
public boolean connected(int p, int q) {
return root(p) == root(q);
}
/**
* 合并操作,将p的根的id(父亲)设成q的根
*
* @param p
* @param q
*/
public void union(int p, int q) {
int proot = root(p);
int qroot = root(q);
id[proot] = qroot;
}
/** getter和setter */
public int[] getId() {
return id;
}
public void setId(int[] id) {
this.id = id;
}
}
时间复杂度分析
-
查找:取决于对象p和q的深度(而树的深度有可能为N,线性级别)
-
合并:常数级别,包含查找时就是查找的时间复杂度
算法 | 初始化 | 合并(包含查找) | 查找 |
---|---|---|---|
快速查找 | N | N | 1 |
快速合并 | N | N | N |
带权的快速合并
上面所描述的快速合并中,存在两个缺点是:一是查找时间消耗过大,二是树的深度容易过大
针对上述缺点,引入带权的快速合并
算法。该算法的特点:
-
改进快速合并,避免生成过高的树
-
追踪每个树的大小(对象的个数)
-
在合并的时候,通过“将小树连接到大树的根”来达到平衡树高的效果
数据结构与“快速合并”相同,合并和查找操作为:
-
查找:检查p和q是否具有相同的根,如有则代表连通(与快速合并相同)。
-
合并:在合并p和q对象时,将小树的根的id设为大树的根(小树接入大树)。故需要额外维护数据size array来保存树的大小。
代码:修改union部分
/**
* 合并操作,将小树的根的id(父亲)设成大树的根
*
* @param p
* @param q
*/
public void union(int p, int q) {
int proot = root(p);
int qroot = root(q);
if(size[p]>size[q]){
id[qroot] = proot;
size[proot]+=size[qroot];
}else{
id[proot] = qroot;
size[qroot]+=size[proot];
}
}
时间复杂度分析
-
查找:取决于对象p和q的深度(而树的深度最多为lgN)
-
合并:常数级别,包含查找时就是查找的时间复杂度
算法 | 初始化 | 合并(包含查找) | 查找 |
---|---|---|---|
快速查找 | N | N | 1 |
快速合并 | N | N | N |
带权快速合并 | N | lgN | lgN |
带压缩路径的快速合并
在做合并和查询找之前,将树的路径进行压缩,从而保持树的扁平化。
代码实现
public int root(int i) {
// 当id[i]=i时就是根
while (id[i] != i){
//将i的根指向其爷爷辈
id[i]=id[id[i]];
i = id[i];
}
return i;
}
时间对复杂度比
对于在N个对象中有M个并查集的情况
算法 | 时间 |
---|---|
快速查找 | MN |
快速合并 | MN |
带权快速合并 | N+MlogN |
带压缩路径快速合并 | N+MlogN |