文章目录
1. 图的连通性问题
(1)在地图上有若干城镇(点),已知所有有道路直接相连的城镇对。要解决整幅图的连通性问题。
(2)随意给你两个点,让你判断它们是否连通;或者问你整幅图一共有几个连通块,也就是被分成了几个互相独立的块。
这些问题都可以使用并查集来解决。
2. 并查集的原理简析
并查集(Union/Find)从名字可以看出,主要涉及两种基本操作:合并和查找。这说明,初始时并查集中的元素是不相交的,经过一系列的基本操作(Union),最终合并成一个大的集合。
而在某次合并之后,有一种合理的需求:某两个元素是否已经处在同一个集合中了?因此就需要Find操作。
并查集是一种 不相交集合 的数据结构,设有一个动态集合S={s1,s2,s3,…sn},每个集合通过一个代表来标识,代表 就是动态集合S 中的某个元素。
比如,若某个元素 x 是否在集合 s1 中(Find操作),返回集合 s1 的代表元素即可。这样,判断两个元素是否在同一个集合中也是很方便的,只要看find(x) 和 find(y) 是否返回同一个代表即可。
为什么是动态集合S呢?因为随着Union操作,动态集合S中的子集合个数越来越少。
数据结构的基本操作决定了它的应用范围,对并查集而言,一个简单的应用就是判断无向图的连通分量个数,或者判断无向图中任何两个顶点是否连通。
2.1 初始化集合S
假设我们已知有n个点,给出了多个直接相连的点对。现在要求这些点中形成了几个连通块,且最大的连通块中的节点数。
首先我们需要初始化一个集合S:
S
=
[
−
1
,
−
1
,
.
.
.
,
−
1
]
S = [-1, -1, ..., -1]
S=[−1,−1,...,−1]
最开始的时候,每个点都是独立的个体,用-1来表示他们都是根节点。
2.2 Union(并)
现在出现了一对直接相连的点。如(0, 1),表示0与1是直接相连的。那么在并查集中,很容易实现:
S
[
1
]
=
0
S[1] = 0
S[1]=0
此时的S就变成了:
S
=
[
−
1
,
0
,
−
1
,
−
1
,
−
1
]
S =[-1, 0, -1, -1, -1]
S=[−1,0,−1,−1,−1]
当访问1这个位置的时候,返回的结果是0,表示1链接在0后面,0是1的父节点。同理,如果这个时候再来一对点(1,2),那么
s
[
2
]
=
1
s[2]=1
s[2]=1之后,S变为:
S
=
[
−
1
,
0
,
1
,
−
1
,
−
1
]
S=[-1, 0, 1, -1, -1]
S=[−1,0,1,−1,−1]
根据上面的数组,我们可以判断节点2与1相连,1又与0相连。这就是简单的并的操作。用Python可以表示为:
def union(root1, root2):
s[root2] = root1
2.3 Find(查)
并查集中还有一个重要的功能,就是查找一个节点的父(根)节点。在我们定义状态数组S的时候,我们规定-1为根节点。这个时候我们只需要逐步向上查找,直到找到第一个为负数的节点时就找到了父节点。
如我们在上一小结中提到的:
0->1->2
S = [-1, 0, 1, -1, -1]
这个时候我们需要找到2的根节点,
于是
f
i
n
d
(
2
)
=
S
[
2
]
=
1
find(2)=S[2]=1
find(2)=S[2]=1,表示其父节点是1,但是
1
>
0
1>0
1>0,还不是根节点;
于是继续查找
f
i
n
d
(
1
)
=
S
[
1
]
=
0
find(1)=S[1]=0
find(1)=S[1]=0,表示1的父节点是0,但是
0
>
−
1
0 > -1
0>−1,还不是根节点;
于是继续有
f
i
n
d
(
0
)
=
S
[
0
]
=
−
1
find(0)=S[0]=-1
find(0)=S[0]=−1,好的,我们终于找到了第一个为负数的点,这个时候我们返回0,表示0是2的根节点。
上述的步骤我们可以通过递归来实现:
def find(x):
if s[x] < 0:
return x
else:
return find(s[x])
上面的代码就是简单的查找过程,但是在并查集中,我们有可能会生成一条很长的路径如:
0->1->2->3->4
这样对于find的效率会有影响,在数据结构–并查集的原理及实现这篇博客中提到了一种启发式的方法按秩合并:将秩小的子树的根指向秩大的子树的根。 我个人觉得比较麻烦,其实可以直接通过find的路径压缩来实现,具体步骤见2.4。
什么是路径压缩,刚才的一条长的路径0->1->2->3->4表明了0是节点1,2,3,4的根节点,那么如果能将结点1,2,3,4直接连接到0,查找的时候就更加方便了吗?其方法很简单:使find查找路径中的顶点(的父亲)都直接指向为树根:
def find(x):
if s[x] < 0:
return x
else:
s[x] = find(s[x]) # 让所有的顶点都指向了树根
return s[x]
# return find(s[x]) # 没有使用路径压缩
2.4 通过读入直接相连的点对来更新S,并统计每个联通块的节点数目
为了方便说明,先上代码:
maxValue = 0
def updateS(i, j):
index_i, index_j = find(i), find(j)
if index_i != index_j: # 两个根节点不一样
if s[index_i] > s[index_j]:
index_i, index_j = index_j, index_i
temp_j = s[index_j] # 在修改s之前,存下小的子树节点的个数
union(index_i, index_j) # 修改路径
s[index_i] += temp_j # 更新当前子树的节点个数
maxValue = max(maxValue, -s[index_i]) # 更新最大结点个数
更新流程:
- 先找到两个节点的根节点index_i, index_j;
- 判断两个根节点的节点数大小;
- 存下根结点数小的节点个数;
- 再将节点总数小的根节点的父节点修改为节点数多的根节点;
- 再更新节点总数多的根节点的节点个数;
- 更新最大节点总数maxValue。
举个例子 :比如当前的S为:
S
=
[
−
4
,
0
,
0
,
−
2
,
3
,
0
]
S = [-4, 0, 0, -2, 3, 0]
S=[−4,0,0,−2,3,0]:
从当前的S可以看出:
节点0,1,2,5是连通的,且根节点是0,节点总数是4(-S[0]);
节点3,4是连通的,且根节点是3,节点总数是2(-S[3])。
现在又有一个节点对(3, 5),我们发现节点3的根节点就是
f
i
n
d
(
3
)
=
3
find(3) = 3
find(3)=3,节点5的根节点是
f
i
n
d
(
5
)
=
0
find(5) = 0
find(5)=0。但是根节点0的节点总数是4,而根节点3的节点总数是2,于是我们只需要将根节点3接到根节点0的后面,修改
S
[
0
]
+
=
S
[
3
]
S[0] += S[3]
S[0]+=S[3],
S
[
3
]
=
0
S[3] = 0
S[3]=0,于是更新后的状态S为:
S
=
[
−
6
,
0
,
0
,
0
,
3
,
0
]
S = [-6, 0, 0, 0, 3, 0]
S=[−6,0,0,0,3,0]
原来与节点3相连接的节点现在都通过节点3与根节点0相连接了。
3. Python实现
class DisjSets(object):
def __init__(self, n):
self.s = [-1] * n
self.maxValue = 0
def union(self, root1, root2):
self.s[root2] = root1
def find(self, x):
if self.s[x] < 0:
return x
else:
self.s[x] = self.find(self.s[x]) # 使用了路径压缩,让查找路径上的所有顶点都指向了树根(代表节点)
return self.s[x]
# return self.find(self.s[x]) # 没有使用 路径压缩
def updateS(self, i, j):
index_i, index_j = self.find(i), self.find(j)
if index_i != index_j: # 两个根节点不一样
if self.s[index_i] > self.s[index_j]:
index_i, index_j = index_j, index_i
temp_j = self.s[index_j] # 在修改s之前,存下小的子树节点的个数
self.union(index_i, index_j) # 修改路径
self.s[index_i] += temp_j # 更新当前子树的节点个数
self.maxValue = max(self.maxValue, -self.s[index_i]) # 更新最大结点个数
测试
if __name__ == '__main__':
n = 6
A = [[0, 1],
[1, 2],
[0, 5],
[3, 4],
[4, 5]]
test = DisjSets(n)
for i in range(len(A)):
test.updateS(A[i][0], A[i][1])
print("S =", test.s)
print("maxValue =", test.maxValue)
S = [-6, 0, 0, 0, 3, 0]
maxValue = 6