Leetcode刷题Python之685.冗余连接II

提示:问题从无向图的冗余边检测变成了有向图的检测。


一、题目描述

问题背景

在树结构中,节点的连接是无环且有层级的。然而在某些特殊情况下,往树中额外添加一条边后,可能会打破这种层次结构,甚至导致图中出现环。在本题中,给定的图是通过在一棵有根树中添加一条有向边构成的,要求找出这条导致问题的边,并将其删除,使得剩余部分依然是一棵有根树。

有根树的定义:
只有一个根节点,根节点没有父节点,所有其他节点都有且只有一个父节点。
树中的每个节点只能有一个父节点(除了根节点)。

题目要求

给定一个有向图,该图由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。

本题结合了并查集和有向图的知识。通过识别双父节点以及检测图中环的形成,我们能够有效地找到需要删除的那条边。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值