Union-Find 算法的解析和应用

231 阅读4分钟

Union-Find 并查集算法

首先吐槽下这个翻译. 只看"并查集"完全 get 不到这个算法是干什么的. 没找到这个翻译的出处. 从字面意思来看个人认为翻译成合并查找更合适.

Union-Find 算法,也称为并查集(Disjoint Set),是一种用于解决集合合并和查询连通性的数据结构和算法。 把所有连通的节点合并到一个根节点上. 当需要判断2个节点是否相连时, 只需要判断他们的根节点是否相同即可

连通性

连通性是图论中的一个概念. 指的是在一个图或网络中,节点之间相连的特征, 即2个节点之间有边连接。

在图论中,连通性是图的基本特性之一。一个无向图称为连通图,如果图中的任意两个节点都是连通的,也就是说,对于图中的任意两个节点 u 和 v, 都存在一条从节点 u 到节点 v 的路径。

相反地,如果图不是连通的,则可以将其分解为若干个连通的子图,每个子图中的节点互相连通,但不与其他子图的节点连通。

连通性在计算机科学和网络理论中有广泛应用。例如,在社交网络分析中,连通性可以帮助我们识别社交群体或寻找影响力最大的节点。在网络路由中,连通性是确保数据包能够正确传递到目的地的重要属性。

连通性有以下特点:

  • 可传递性:如果节点 A 与节点 B 连通,并且节点 B 与节点 C 连通,则节点 A 与节点 C 也连通。
  • 自反性:每个节点与自身是连通的。
  • 对称性:如果节点 A 与节点 B 连通,则节点 B 与节点 A 也连通。
  • 等价关系:连通性满足等价关系的性质,即自反性、对称性和传递性。

Union-Find 算法

Union-Find 算法在解决一些图论问题、连通性问题和动态等价类问题等方面非常有用。它具有简单、高效且易于实现的特点,常被应用于各种算法和系统设计中。 该算法维护一个森林(或称为集合森林),其中每个元素都有一个指向其父节点的指针。初始状态下,每个元素都是独立的一个集合。

Union-Find 算法提供以下两个主要操作:

  • Find(查找):用于找到元素所属的根节点。该操作通过递归地沿着指向父节点的指针向上查找,直到找到根节点。 这可以用来确定两个元素是否属于同一个集合,只需比较它们的根节点是否相同。
  • Union(合并):用于将两个集合合并为一个集合。该操作将一个集合的根节点作为另一个集合的根节点的子节点,从而实现两个集合的合并。

算法优化: Union-Find 算法通常使用优化技巧,如路径压缩(Path Compression)和按秩合并(Union by Rank)来提高效率。

  • 路径压缩可以减少查找操作的时间复杂度,使得树的高度更加平衡;
  • 按秩合并则基于树的"秩"(即树的深度或节点数量)来决定合并时的策略,以减少树的高度增长。

实现方法

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n)) # 初始化时每个节点的 root 是自身
        self.rank = [0] * n # 每个节点的树的大小, 用于按秩优化. 
    
    def find(self, x):
        """
        查看节点 x 的根节点
        """
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # 路径压缩,将节点直接连接到根节点上
        return self.parent[x]
    
    def union(self, x, y):
        root_x = self.find(x)
        root_y = self.find(y)
        
        if root_x == root_y:
            return
        
        # 把秩小的连接到秩大的根节点下, 不会修改该根节点的秩
        if self.rank[root_x] < self.rank[root_y]:
            self.parent[root_x] = root_y
        elif self.rank[root_x] > self.rank[root_y]:
            self.parent[root_y] = root_x
        else:
            self.parent[root_y] = root_x
            self.rank[root_x] += 1
            

应用场景举例

判断被围绕的区域. 如下数组表示一个棋盘. 判断其中被 X 围绕的区域

示例:

arr = [
    ["X","O","X","O","X","O"],
    ["O","X","O","X","O","X"],
    ["X","O","X","O","X","O"],
    ["O","X","O","X","O","X"]
]

其中被X围绕的点O, 即点 [1, 2],[1, 4],[2, 1],[2, 3]

被包围的点即与边缘不连通的. 因此可以用 Union-Find 方法来判断连通性

步骤:

  1. 让边界上的点连到同一个根节点上
  2. 把上下相同的点连接
  3. 找到与边界的根节点不相连的 O

解法如下:

 def get_surrounded_pos(board: List[List[str]]) -> List[List[int]]:
        """
        填充被包围的 O
        """
        row_len = len(board)
        col_len = len(board[0])
        all_o_list = []
        out_root = row_len * col_len
        uf = UnionFind(row_len * col_len + 1)
        # 把最后一个节点作为根节点, 使边缘的元素都与该根节点相连
        for row in range(0, row_len):
            for col in range(0, col_len):
                char = board[row][col]
                char_idx = row * col_len + col
                # 边缘节点与 out_root 相连
                if col == 0 or col == col_len - 1 or row == 0 or row == row_len - 1:
                    uf.union(out_root, char_idx)
                # 上下左右的点如果相同的话则相连
                if row > 0 and board[row - 1][col] == char:
                    uf.union((row - 1) * col_len + col, char_idx)
                if row < row_len - 1 and board[row + 1][col] == char:
                    uf.union((row + 1) * col_len + col, char_idx)
                if col > 0 and board[row][col - 1] == char:
                    uf.union((col - 1) + row * col_len, char_idx)
                if col < col_len - 1 and board[row][col + 1] == char:
                    uf.union((col + 1) + row * col_len, char_idx)
                
                if char == 'O':
                    all_o_list.append([row, col])
        
        # 找到不与外界连通的元素
        return [item for item in all_o_list if not uf.is_connect(item[0] * col_len + item[1], out_root)]