并查集算法:
并查集算法(union-find)是一种用于快速判断两节点是否连通的算法.
1.连通性:
并查集算法中所处理的连通性具有如下性质:
- 自反性:节点p与其自身是连通的
- 对称性:如果p与q是连通的,则q与p也是连通的.
- 传递性:如果节点p是节点q是连通的,节点q与节点r是连通的,则节点p与节点r是连通的.
2.连通性判断的问题形式:
在连通性判断问题中,通常首先给定多个连通的节点对(i,j)
,然后要你判断给定的两节点r,s是否是连通的.
3. 基于并查集的连通性问题判断:
由于连通性的传递性,我们需要基于给定的连通节点对(i,j)
,构建起各个连通分量.例如,给定(a, b)
, (b,c)
连通点对,则当前(a,b,c)
三个节点位于同一连通分量内,同一连通分量内的节点间是互相连通的.只要我们构建了连通分量,则判断节点
r
r
r,
s
s
s是否是连通的,就转变为判断节点
r
r
r,
s
s
s是否位于同一连通分量内.而并查集算法核心即是这两个步骤.1.构建连通分量;2.返回给定节点所在的连通分量编号.
4.并查集算法:
我们假定N个节点使用整数0~N-1表示.基于此,我们可以使用整数数组实现并查集算法.如果节点是其他类型对象,则可以使用键值对数据结构实现同样的并查集算法.
此处整理三种并查集算法.
在并查集算法中,每一连通分量都可以使用当前连通分量中的一个节点值来代表,称为当前连通分量的根节点.例如假设当前连通分量中包括节点{2, 3, 11}
,我们可以使用任意一个节点来表示当前连通分量的根节点.假设2
为该连通分量的根节点,则我们说2
所在的连通分量根节点为2
,3
所在的连通分量根节点为2
,如果当前两个节点所在连通分量根节点相同,则当前两个节点位于同一连通分量中,这两个节点是连通的.
并查集算法API:
并查集算法包括三个方法:
void union(int p, int q)
:如果节点p,q是连通的,则我们使用union
方法将两个节点相连接,注意到,当p,q连通后,p所在的连通分量与q所在的连通分量也连接为一个更大的连通分量.
int find(int p)
:find
方法返回节点p所在连通分量的根节点
boolean connected(int p, int q)
:connected
方法判断节点p与q是否连通,该方法实现非常简单:return find(p) == find(q)
.
4.1 quick-find算法:
我们可以使用整数数组id[]
保存节点所在连通分量的根节点,id[i]
为第i
个节点所在连通分量的根节点.在初始情况下,每一个节点相独立,因此id[i] = i
.基于此,并查集算法实现如下:
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;
}
算法时间复杂度分析:
不能发现,quick-find算法中,find方法的时间复杂度为O(1),union方法的时间复杂度为O(N);
4.2 quick-union算法:
为了提高union方法的效率,我们涉及quick-union算法.在该算法中,id[]
数组含义不同于quick-find算法.我们使用id[i]
来存在节点i的父节点,如果当前节点i为所在连通分量的根节点,则其无父节点,id[i] = i
.基于这种表示方法,一个连通分量可以被理解为一棵多叉树.例如,若某连通分量中包括节点{2, 3, 5, 7, 12}
,此时id[2] = 3, id[3] = 5, id[5] = 5, id[7] = 5, id[12] = 5
.则当前连通分量可以用如下图结构表示:
假设另一连通分量为:
其对应的id[]
数组为id[4] = 4, id[6] = 4, id[8] = 6, id[9] = 4, id[11] = 9
.
则只需要将其中一个连通分量的根节点添加到另一连通分量根节点上即刻实现连通分量合并.我们只需要更改id[4] = 5
即可完成.
实际上,我们可以发现quick-find方法相当于一棵树高为2的多叉树,其除过根节点外只有一层叶节点.对于连通分量{4,6,8,9,11}
,其根节点为4,则使用quick-find方法的表示,其树结构如下:
且quick-find方法要求合并后的两颗树仍然为高度为2的树,因此两颗树的合并过程较为麻烦,需要将其中一棵树的所有节点均连接到另一棵树的根节点.
通过对以上关于树形的了解,我们不难发现,find
方法相当于从当前节点出发向上寻找根节点,在quick-find方法中,由于树高为1,因此我们可以在O(1)时间复杂度实现find
方法.而对于quick-union方法,find
方法最坏情况下的时间复杂度即为该树的高度.
public int find(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;
}
算法时间复杂度分析:
该方法中,find方法所需时间与当前节点到树根部路径长度成正比,union方法需要调用两次find方法查找连通分量根节点,因此其时间复杂度与find方法时间复杂度相同.
4.3 加权quick-union方法
在分析quick-union方法时,我们指出,find方法时间复杂度与树的高度有关.最坏情况下,其时间复杂度可能与当前连通分量中的节点个数成正比(每个节点只有一个子节点).那我们应该如何尽可能地降低树的高度呢?分析算法不难发现,我们唯一可能作出的改进在于union
方法中两颗子树的连接,在quick-union方法中,我们随意的将一棵树连接到另一棵树上.为了尽可能降低数的高度,我们应在每一步连接时,将高度较低的树连接到高度较高的树上.后面推导我们会证明,在每一步我们只需要比较两棵树的节点个数,将节点数少的树合并到节点树多的树上即可.
假设有如下两个连通分量,我们使用树形表示:
如果我们将左边的连通分量连接到右边的连通分量中,则合并后连通分量树形为:
由于我们将更高的树连接到较低的树上,此时树的高度进一步增加1.
如果我们将较低树连接到较高树,此时连通分量树形如下:
此时树高并未增加.
加权quick-union算法实现如下:
public class WeightQuickUnionUF{
private int[]id; //纪录当前节点所在连通分量的根节点
private int[] sz;//纪录sz[]对应树形的节点个数.
private int count; //纪录当前连通分量个数
public WeightQuickUnionUF(int N){
id = new int[N];
count = 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;
}
public int count(){
return count;
}
public int find(int p){
while(p != id[p])
p = id[p];
return p;
}
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--; //合并后,连通分量减1
}
}
加权quick-union方法中,对于节点为N的连通分量,find方法的时间复杂度为O(lgN);即当前连通分量所在树形结构其树高不超过lg2N;
证明:我们使用归纳法证明该命题.
对于大小为k的树,其树高最多为lg2k;
- 当k=1时,树高为1 = lg2; 命题成立
- 假设大小为i的树,其树高最多为lg2i,当我们将大小为i与另一棵大小为j的树进行合并时,假设 i < j i < j i<j,则将i合并到j, 合并后树节点个数为 i + j = k i+j = k i+j=k,合并后大小为i的树节点的高度+1, 1 + l g 2 i = l g ( 2 i + 2 i ) < = l g ( 2 ( i + j ) ) = l g ( 2 k ) 1+lg2i = lg(2i + 2i) <= lg(2(i+j)) = lg(2k) 1+lg2i=lg(2i+2i)<=lg(2(i+j))=lg(2k)而大小为j的树中节点高度未变,因此合并后节点数为k的树中各节点高度不超过 l g 2 k lg2k lg2k;