以图判树

Submission

运行时间: 18 ms

内存: 17.2 MB

class UnionFind:
    
    # 为了效率,我们不使用 makeset,而是
    # 在构造函数同时产生所有集合
    def __init__(self, n):
        self.parent = [node for node in range(n)]
        
    # 未进行任何优化的 find 方法。它会追踪到父连接
    # 直到找到 A 的根节点,并返回那个根节点。
    def find(self, A):
        while A != self.parent[A]:
            A = self.parent[A]
        return A
        
    # 未进行优化的 union 方法。 
    # 如果发生合并它返回 true,否则返回 false
    def union(self, A, B):
        # 找到 A 和 B 的根
        root_A = self.find(A)
        root_B = self.find(B)
        # 检查 A 和 B 是否已经在同一个集合中
        if root_A == root_B:
            return False
        # 合并包含 A 和 B 的集合
        self.parent[root_A] = root_B
        return True

class Solution:
    def validTree(self, n: int, edges: List[List[int]]) -> bool:
        # 情况 1: 图必须包含 n - 1 条边。
        if len(edges) != n - 1: return False
        
        # 情况 2: 图必须包含单个连通分量
        # 创建一个包含 n 个节点的新并查集对象。
        unionFind = UnionFind(n)
        # 添加每一条边。检查是否发生合并,
        # 因为如果它没有,就一定有环
        for A, B in edges:
            if not unionFind.union(A, B):
                return False
        # 如果我们走到这一步,就没有环了!
        return True
        

Explain

该题解使用了并查集来判断给定的边集是否构成一棵树。并查集是一种树形数据结构,用于处理不相交集合的合并及查询所属集合等操作。通过并查集,我们可以高效地判断图中是否存在环以及连通分量的个数。 具体思路如下: 1. 首先,树中边的数量必须等于节点数减一,否则一定不是树。 2. 接着,初始化一个大小为 n 的并查集,每个节点各自为一个集合。 3. 遍历所有的边,对于每条边 (A, B),找到 A 和 B 所在集合的根节点。 - 如果 A 和 B 在同一个集合中,说明出现了环,不是树,返回 false。 - 如果 A 和 B 不在同一个集合中,则将它们所在的集合合并为一个集合。 4. 遍历完所有的边后,如果没有出现环,说明是一棵树,返回 true。

时间复杂度: O(n)

空间复杂度: O(n)

class UnionFind:
    
    # 为了效率,我们不使用 makeset,而是
    # 在构造函数同时产生所有集合
    def __init__(self, n):
        # 初始化父节点列表,每个节点的父节点初始为自己
        self.parent = [node for node in range(n)]
        
    # 未进行任何优化的 find 方法。它会追踪到父连接
    # 直到找到 A 的根节点,并返回那个根节点。
    def find(self, A):
        while A != self.parent[A]:
            A = self.parent[A]
        return A
        
    # 未进行优化的 union 方法。 
    # 如果发生合并它返回 true,否则返回 false
    def union(self, A, B):
        # 找到 A 和 B 的根节点
        root_A = self.find(A)
        root_B = self.find(B)
        # 检查 A 和 B 是否已经在同一个集合中
        if root_A == root_B:
            return False
        # 合并包含 A 和 B 的集合,将 A 的根节点的父节点设置为 B 的根节点
        self.parent[root_A] = root_B
        return True

class Solution:
    def validTree(self, n: int, edges: List[List[int]]) -> bool:
        # 情况 1: 图必须包含 n - 1 条边。
        if len(edges) != n - 1: return False
        
        # 情况 2: 图必须包含单个连通分量
        # 创建一个包含 n 个节点的新并查集对象。
        unionFind = UnionFind(n)
        # 添加每一条边。检查是否发生合并,
        # 因为如果它没有,就一定有环
        for A, B in edges:
            if not unionFind.union(A, B):
                return False
        # 如果我们走到这一步,就没有环了!
        return True
        

Explore

在本题的上下文中,选择未优化的 find 和 union 方法是因为问题规模通常较小,且操作的复杂度可以接受。使用未优化的方法简化了代码的实现,使得代码更易于理解和维护。尽管路径压缩或按秩合并可以显著提高并查集操作的效率,特别是在大规模数据或多次查询和合并操作的场景中,但在本题中,由于边的数量与节点数相近且操作次数有限,这些优化可能不会带来显著的性能提升。

在本题的上下文中,代码先检查边的数量是否等于节点数减一。如果条件满足,随后使用并查集检查是否存在环。此方法在处理连通图时是有效的,因为一个没有环的连通图正是一棵树。然而,如果图是非连通的,即使没有环,边的数量与节点数减一的条件也会导致算法返回 false。因此,这种方法只适用于连通图的情况,而题目假设或要求图应当是连通的(即一棵树)。

在判断一个结构是否为一棵树时,除了检查边的数量等于节点数减一这一必要条件外,还需要确保图是连通的且没有环。这两个条件可以通过并查集来验证:1) 使用并查集处理所有边,如果任何两个节点已经在同一集合中再次合并,表明存在环;2) 在处理完所有边后,如果所有节点都属于同一连通分量,则表明图是连通的。只有同时满足这些条件,结构才是一棵树。

这种做法主要是基于树的一个基本特性:在一个包含 n 个节点的树中,必然恰有 n-1 条边。这个条件是树的必要条件之一,但不是充分条件,因为单单边的数量等于节点数减一并不能保证没有环或图是连通的。因此,在这之后,解法还需要检查图是否连通且无环来确保结构真的是一棵树。如果边的数量不符,可以直接判断不是树,这种做法有效地排除了所有边数过多或过少的非树结构情况。