并查集及其典型应用

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])  # 更新最大结点个数

更新流程:

  1. 先找到两个节点的根节点index_i, index_j;
  2. 判断两个根节点的节点数大小;
  3. 存下根结点数小的节点个数;
  4. 再将节点总数小的根节点的父节点修改为节点数多的根节点;
  5. 再更新节点总数多的根节点的节点个数;
  6. 更新最大节点总数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

参考资料

图中连通块的个数:并查集

数据结构–并查集的原理及实现

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
并查集(Disjoint Set)是一种数据结构,用于解决集合的合并和查找问题。在Python可以使用类来实现并查集。引用展示了一个简单的并查集类的代码实现,其包括了初始化集合、查找集合、合并集合和判断两个元素是否在同一个集合的方法。另外,引用和展示了对并查集代码的优化,包括路径压缩和按秩合并等技巧,以提高并查集的效率。 在Python使用并查集可以解决一些实际问题,如求解岛屿个数、朋友圈等。通过将问题转化为集合的合并和查找操作,可以使用并查集来高效地解决这些问题。 所以,如果你需要在Python实现并查集,可以参考以上的代码实现和优化方法。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [python 数据结构与算法——并查集](https://blog.csdn.net/itnerd/article/details/103916115)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [并查集Python版](https://blog.csdn.net/XZ2585458279/article/details/127274576)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值