提示:问题从无向图的冗余边检测变成了有向图的检测。
文章目录
一、题目描述
问题背景
在树结构中,节点的连接是无环且有层级的。然而在某些特殊情况下,往树中额外添加一条边后,可能会打破这种层次结构,甚至导致图中出现环。在本题中,给定的图是通过在一棵有根树中添加一条有向边构成的,要求找出这条导致问题的边,并将其删除,使得剩余部分依然是一棵有根树。
有根树的定义:
只有一个根节点,根节点没有父节点,所有其他节点都有且只有一个父节点。
树中的每个节点只能有一个父节点(除了根节点)。
题目要求
给定一个有向图,该图由n 个节点和n 条边组成,其中n 个节点构成了一个有根树,而附加的第n 条边可能会导致两个问题:
1.某个节点有了两个父节点。
2.图中形成了一个环。
我们的目标是找出并删除一条边,使得删除后图恢复成一个有根树。如果有多个答案,返回给定二维数组中最后出现的那条边。
在这个问题中,我们输入的是一个有向图,边以二维数组的形式给出。每条边用 [u, v] 表示,节点u 是节点v 的父节点,形成一条有向边。要求删除一条冗余边,使得剩下的图成为一个有n 个节点的有根树。
示例
输入: edges = [[1,2], [1,3], [2,3]]
输出: [2,3]
输入: edges = [[1,2], [2,3], [3,4], [4,1], [1,5]]
输出: [4,1]
二、解题思路
1.要解决的问题
本题有两个可能的错误情况:
1.双父节点问题:某个节点v 有两个不同的父节点。
2.环问题:添加的边导致有向图中形成了环。
我们可以分两步来解决这个问题:
步骤1:检查是否有节点有两个父节点。我们遍历所有的边,记录每个节点的父节点。如果发现某个节点有两个父节点,则标记这两条边为候选边。
步骤2:使用并查集检测环。我们从头到尾遍历每条边,并用并查集来检测是否形成了环。如果遇到环,可能要删除的边就是导致环形成的那条边。
2.并查集(Union-Find)介绍
并查集是一种常用的数据结构,适用于动态连通性问题。它支持以下两种操作:
查找(Find):查找某个元素所属的集合的代表元素。
合并(Union):将两个不同的集合合并为一个集合。
为了提高并查集的效率,可以使用两种优化方法:
路径压缩:在查找操作时,将树的节点直接连接到根节点,从而减少树的高度。
按秩合并:在合并时,总是将较小的树挂到较大的树上,从而尽量保持树的高度较小。
3.详细解题步骤
3.1 初始化并查集
首先,我们初始化并查集结构,用一个 parent 数组来记录每个节点的父节点。初始时,每个节点的父节点指向它自己。我们还用一个 rank 数组来记录树的高度。
3.2 遍历边集,处理双父节点问题
在遍历边集的过程中,如果发现某个节点已经有一个父节点,再次遇到该节点时,说明这个节点有两个父节点。我们记录这两条边,称为候选边,后续将对它们进行处理。
3.3 并查集检测环
如果没有双父节点的情况,我们可以用并查集来检测图中是否形成了环。在处理每条边时,尝试将两个节点合并到同一个集合中。如果两个节点已经属于同一个集合,则说明这条边导致了环的出现。
3.4 最终确定删除的边
如果出现双父节点,我们检查是否在删除其中一条边后,图仍然包含环。如果没有环,则删除该边。如果仍有环,则删除另一条候选边。
如果没有双父节点,直接返回导致环的那条边
四、代码实现
class Solution(object):
def findRedundantDirectedConnection(self, edges):
"""
:type edges: List[List[int]]
:rtype: List[int]
"""
n = len(edges)
parent = list(range(n + 1)) # 用于并查集初始化,每个节点的父节点是自己
root_candidate = [None, None] # 用于记录出现双父节点的两条候选边
parent_map = {} # 记录每个节点的父节点
# 并查集的查找函数,带路径压缩
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
# 合并操作
def union(x, y):
rootX = find(x)
rootY = find(y)
if rootX != rootY:
parent[rootX] = rootY
return True
return False
# 遍历所有边,找出是否有双父节点的情况
for u, v in edges:
if v in parent_map: # 发现有两个父节点
root_candidate[0] = [parent_map[v], v] # 第一次记录父节点的边
root_candidate[1] = [u, v] # 第二次记录的边
else:
parent_map[v] = u # 记录父节点
# 用于判断是否存在环
parent = list(range(n + 1)) # 重置并查集
for u, v in edges:
# 跳过第二条候选边,如果有两个父节点的情况
if root_candidate[1] and [u, v] == root_candidate[1]:
continue
if not union(u, v):
if root_candidate[0]: # 如果有双父节点情况,返回第一条边
return root_candidate[0]
return [u, v] # 否则返回当前边(环的那条边)
return root_candidate[1] # 如果不存在环,返回第二条候选边
# 示例测试
solution = Solution()
print(solution.findRedundantDirectedConnection([[1,2],[1,3],[2,3]])) # 输出: [2,3]
print(solution.findRedundantDirectedConnection([[1,2],[2,3],[3,4],[4,1],[1,5]])) # 输出: [4,1]
代码详解
1.parent数组:
用来存储并查集中每个节点的父节点,用于合并和查找操作。
2.parent_map字典:
用于记录每个节点的父节点,帮助我们检测是否存在双父节点的情况。
3.find函数(路径压缩):
通过递归查找节点的根,并且在查找过程中进行路径压缩,将节点直接连接到根节点以加速后续查询操作。
4.union函数:
合并两个集合,如果两个节点不属于同一个集合,则将其合并。
5.双父节点的处理:
如果发现某个节点有两个父节点,我们将这两条边作为候选。后续通过并查集判断哪一条边可以移除。
6.检测环:
如果不存在双父节点的情况,我们用并查集直接检测是否形成了环。
五、总结
在这个算法中,每条边的查找和合并操作都经过优化,时间复杂度为 O(n),其中n 是节点的数量。
时间复杂度:O(n),因为每个节点的查找和合并操作的时间复杂度是近似常数的。
空间复杂度:O(n),用于存储并查集的 parent 数组以及 parent_map。
本题结合了并查集和有向图的知识。通过识别双父节点以及检测图中环的形成,我们能够有效地找到需要删除的那条边。