zoukankan      html  css  js  c++  java
  • 并查集

    本文参考了【算法】并查集(Disjoint Set)并查集详解并查集

    并查集原理

    并查集是一种用于处理不相交集合之间合并问题的数据结构,例如求连通子图、判断是否存在环、求最小生成树等。

    以判断图中是否有环为例,下图是一个无向图。

    graph LR; A---B A---C A---D C---D C---E

    首先把每一个顶点都看作是一个集合,逐一地考察每一条边。

    • 考察A---B,则把 A 和 B 放入同一个集合,有{A, B}, {C}, {D}, {E}
    • 考察A---C,则把 A 和 C 放入同一个集合,有{A, B, C}, {D}, {E}
    • 考察A---D,则把 A 和 D 放入同一个集合,有{A, B, C, D}, {E}
    • 考察C---D,发现 C 和 D 已经在同一个集合里了,这就说明图中存在环。

    由这个例子可以看出,并查集的主要操作就是判断两个元素是否在同一个集合中,若不在同一个集合中,则将他们合并。实际上,并查集用一个”代表“来表示一个集合,对于给定的任意一个元素,都可以在常数级别的时间复杂度内找到这个元素的”代表“,即所在的集合。

    可见,并查集主要包含两种操作:

    1. find(x):找到元素 x 所在集合的代表;
    2. unionSet(x, y): 若 x 和 y 不在同一个集合,则将其合并。

    实现(Python)

    并查集用树来表示集合,树中的每一个结点就代表集合中的一个元素,树的根结点就是这个集合的”代表“。要注意的是,树中的父子关系并不能代表元素之间的关系。而树是用数组来表示的,索引 i 处存储的是该元素的父结点的索引,而树根结点的父节点就是它本身。首先初始化一个并查集。

    def init(n: int) -> List[int]:  # 假设有 n 个元素,即开始时每个元素自成一派
        return [i for i in range(n)]
    

    接下来写find操作。只要每次沿着父节点向上,最终就一定能够找到根结点,即对应集合的”代表“。

    def find(x: int, path: List[int]) -> int:
        root = x
        while path[root] != root:
            root = path[root]
        return root
    

    最后是合并操作。需要判断两个结点的根结点是否相同,如果不同,则将他们合并。

    def unionSet(x: int, y: int, path: List[int]) -> bool:
        x_root = find(x, path)
        y_root = find(y, path)
        if x_root == y_root:  # 未发生合并,返回False
            return False
        else:  # 需要合并,返回True
            path[y_root] = x_root
                return True
    

    这时候会发现一个问题,就是之前说的find操作是在常数时间内的,如果构造树的时候构造出一个一字长蛇阵,那么这个操作显然不是常数级别的。因此需要用到路径压缩。

    路径压缩

    每次查找时,将结点的父节点改为对应集合的根结点。这样每条经过查找的路径高度都会降为 1。但是由于只在查找时进行路径压缩,因此树的结构仍然有可能是奇形怪状的。

    只要修改find操作的代码就行了。

    def find(x: int, path: List[int]) -> int:
        if x != path[x]:
            path[x] = find(path[x], path)
    
        return path[x]
    

    按秩合并

    另开一个跟path等长的数组,用来存树的高度。每次合并操作时,总是将较矮树的根结点指向较高树的根结点。

    def init(n: int) -> List[int]:  # 假设有 n 个元素,即开始时每个元素自成一派
        path = [i for i in range(n)]
        rank = [0 for i in range(n)]
        return path, rank
    

    同时,需要修改合并操作的代码。当两棵树的高度一样时,我们把 y 的根结点指向 x 的根结点。

    def unionSet(x: int, y: int, path: List[int]) -> bool:
        x_root = find(x, path)
        y_root = find(y, path)
        if x_root == y_root:  # 未发生合并,返回False
            return False
        else:  # 需要合并,返回True
            if rank[x_root] > rank[y_root]:
                path[y_root] = x_root
            elif rank[x_root] < rank[y_root]:
    	    path[x_root] = y_root
            else:
    	    path[y_root] = x_root
    	    rank[x_root] += 1
        return True
    

    例题

    LeetCode 547. 朋友圈
    班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。

    给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果(M_{ij}=1),表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

    思路:
    只要直接套用以上的并查集模板即可,最后统计其中树根的个数即可。由上面的假设可知,树根结点满足path[i]=i。感觉这种不那么复杂的题也不需要按秩合并或者路径压缩了,直接来还快一些。

    class Solution:
        def findCircleNum(self, M: List[List[int]]) -> int:
            def find(x: int, path: List[int]) -> int:
                root = x
                while path[root] != root:
                    root = path[root]
                return root
            
            def unionSet(x: int, y: int, path: List[int], rank: List[int]) -> bool:
                x_root = find(x, path)
                y_root = find(y, path)
                if x_root == y_root:  # 未发生合并,返回False
                    return False
                else:  # 需要合并,返回True
                    if rank[x_root] > rank[y_root]:
                        path[y_root] = x_root
                    elif rank[x_root] < rank[y_root]:
                        path[x_root] = y_root
                    else:
                        path[y_root] = x_root
                        rank[x_root] += 1
                return True
            
            N = len(M)
            path = [i for i in range(N)]
            rank = [0] * N
            for i in range(N):
                for j in range(i, N):
                    if M[i][j] == 1 and i != j:
                        unionSet(i, j, path, rank)
            
            cnt = 0
            for i in range(N):
                if path[i] == i:
                    cnt += 1
            
            return cnt
    
  • 相关阅读:
    可搜索的下拉框
    Vue 父组件调用子组件的方法
    vue中异步函数async和await的用法
    用配置文件的方法发送axios请求
    vue中 localStorage的使用方法(详解)
    下拉框 组件的使用
    遇到不懂的记录
    做测试平台可能会用到的东西
    下拉框 v-for循环拿值的方法
    ant 自定义遮罩
  • 原文地址:https://www.cnblogs.com/beeblog72/p/13154309.html
Copyright © 2011-2022 走看看