冗余连接 II

标签: 深度优先搜索 广度优先搜索 并查集

难度: Hard

在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。

输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1n)的树及一条附加的有向边构成。附加的边包含在 1n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。

结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 uivi 的一个父节点。

返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。

 

示例 1:

输入:edges = [[1,2],[1,3],[2,3]]
输出:[2,3]

示例 2:

输入:edges = [[1,2],[2,3],[3,4],[4,1],[1,5]]
输出:[4,1]

 

提示:

  • n == edges.length
  • 3 <= n <= 1000
  • edges[i].length == 2
  • 1 <= ui, vi <= n

Submission

运行时间: 28 ms

内存: 16.4 MB

class UnionFindSet:
    def __init__(self, n):
        self.father = {}
        for i in range(n):
            self.father[i] = i

    def add(self, x):
        if x not in self.father:
            self.father[x] = x

    def find(self, x):

        root = x

        while self.father[root] != root:
            root = self.father[root]

        while self.father[x] != root:
            original_father = self.father[x]
            self.father[x] = root
            x = original_father
        
        return root

    def union(self, x, y):
        root_x = self.find(x)
        root_y = self.find(y)

        if root_x == root_y:
            return False
            
        self.father[root_y] = root_x
        return True

class Solution:
    def findRedundantDirectedConnection(self, edges: List[List[int]]) -> List[int]:
        length = len(edges)

        # 步骤 1:预处理入度数组(记录指向某个结点的边的条数)
        inDegree = [0] * (length + 1)
        for edge in edges:
            inDegree[edge[1]] += 1

        # 步骤 2:先尝试删除构成入度为 2 的边,看看是否形成环
        for i in range(length-1, -1, -1):
            if inDegree[edges[i][1]] == 2:
                # 如果不构成环,这条边就是要去掉的那条边
                if not self.judgeCircle(edges, length, i):
                    return edges[i]
        
        # 步骤 3:再尝试删除构成入度为 1 的边,看看是否形成环
        for i in range(length-1, -1, -1):
            if inDegree[edges[i][1]] == 1:
                if not self.judgeCircle(edges, length, i):
                    return edges[i]
    
    # len: 结点总数(从 1 开始,因此初始化的时候 + 1)
    # removeEdgeIndex 删除的边的下标
    # 构成环,返回 true
    def judgeCircle(self, edges: List[List[int]], length: int, removeEdgeIdge) -> bool:
        ufs = UnionFindSet(length + 1)
        for i in range(length):
            if i == removeEdgeIdge:
                continue
            
            if not ufs.union(edges[i][0], edges[i][1]):
                return True
        
        return False

Explain

该题解使用并查集来解决问题。首先预处理边的入度数组,记录每个节点被指向的次数。然后按照特定顺序尝试删除边,判断删除后的图是否会形成环。如果删除构成入度为2的边不会形成环,说明这条边是导致冗余的边;如果删除构成入度为1的边不会形成环,说明这条边是导致冗余的边。在并查集中,如果合并两个节点时发现它们已经在同一个集合内,说明形成了环。

时间复杂度: O(n)

空间复杂度: O(n)

class UnionFindSet:
    def __init__(self, n):
        self.father = {}
        for i in range(n):
            self.father[i] = i
    
    def add(self, x):
        if x not in self.father:
            self.father[x] = x
    
    def find(self, x):
        # 查找x的根节点
        root = x
        while self.father[root] != root:
            root = self.father[root]
        
        # 路径压缩
        while self.father[x] != root:
            original_father = self.father[x]
            self.father[x] = root
            x = original_father
        
        return root
    
    def union(self, x, y):
        # 合并x和y所在的集合
        root_x = self.find(x)
        root_y = self.find(y)
        
        if root_x == root_y:
            # 若x和y已经在同一集合,返回False
            return False
        
        self.father[root_y] = root_x
        return True

class Solution:
    def findRedundantDirectedConnection(self, edges: List[List[int]]) -> List[int]:
        length = len(edges)
        
        # 步骤 1:预处理入度数组(记录指向某个结点的边的条数)
        inDegree = [0] * (length + 1)
        for edge in edges:
            inDegree[edge[1]] += 1
        
        # 步骤 2:先尝试删除构成入度为 2 的边,看看是否形成环
        for i in range(length-1, -1, -1):
            if inDegree[edges[i][1]] == 2:
                # 如果不构成环,这条边就是要去掉的那条边
                if not self.judgeCircle(edges, length, i):
                    return edges[i]
        
        # 步骤 3:再尝试删除构成入度为 1 的边,看看是否形成环
        for i in range(length-1, -1, -1):
            if inDegree[edges[i][1]] == 1:
                if not self.judgeCircle(edges, length, i):
                    return edges[i]
    
    # removeEdgeIndex 删除的边的下标
    # 构成环,返回 true
    def judgeCircle(self, edges: List[List[int]], length: int, removeEdgeIdge) -> bool:
        # 初始化并查集
        ufs = UnionFindSet(length + 1)
        for i in range(length):
            if i == removeEdgeIdge:
                # 跳过要删除的边
                continue
            
            if not ufs.union(edges[i][0], edges[i][1]):
                # 合并失败,说明形成环,返回True
                return True
        
        # 无环,返回False
        return False

Explore

路径压缩技术是并查集优化中的一个重要手段。在查找元素根节点的过程中,路径压缩将元素到根节点路径上的所有节点直接连接到根节点。这样做的目的是减少后续查找同一路径上节点的深度,从而减少递归或迭代的层数,加快查找速度。经过多次操作之后,结构趋于扁平化,使得任何节点的根节点都可以近乎直接访问,显著降低了查找时间复杂度。

在处理入度为2的节点时,题解采用了尝试删除每一条边的策略来判断哪一条边应该被删除。具体步骤是:首先从后向前遍历所有边,寻找到入度为2的节点相关的两条边。然后,尝试分别删除这两条边,每次删除后都使用并查集检查剩余的图是否会形成环。如果删除某条边后图不形成环,则这条边就是冗余的,应该被删除。这一策略利用了图理论中的性质——如果删除某条边后仍然能保持图的连通性且无环,则该边可能是冗余的。

在并查集中,每个节点开始时都是自己的根节点。当尝试合并两个节点时,将它们的根节点设为同一个,以表示它们属于同一个连通分量。如果在合并操作中发现两个节点已经有相同的根节点,这意味着这两个节点已经在同一个连通分量中,再次尝试将它们合并就会形成一个环。因此,如果合并操作返回False,表明在尝试合并两个已经连通的节点,从而发现了一个环。这是检测图中环的有效方法。

在实现上,题解通过一个简单的策略来确保删除的边不被考虑:在遍历并尝试合并其他边时,跳过指定的删除边。具体来说,在执行并查集的合并操作前,检查当前正在处理的边是否是要删除的边的索引。如果是,则跳过这条边,不进行任何操作;如果不是,则正常执行合并操作。这样可以确保在构建并查集的过程中,被标记为删除的边不会影响并查集的结构,从而准确地检验删除这条边后图是否还会形成环。