题目描述

✅ 685. 冗余连接 II

题意分析

有根有向树中,根的入度为零,其余节点各有一个父节点,且都能从根到达。输入是在这样一棵树上增加一条有向边得到的,要求删除一条边恢复有根树;若多条边都能删除,返回输入下标最大的那条。

新边只会让它的终点多一个父节点,所以至多有一个节点入度变成二。若出现双父节点,必须删掉它的两条入边之一;若没有双父节点,则需要从新增的环上删边。先处理入度,才能正确使用并查集判断剩余结构。

解法:识别双父节点,再跳边判环

核心思路

[!blue]

incoming[v] 保存节点 v 第一条入边的下标,初始为 -1,以免与合法的下标零混淆。再次遇到指向同一节点的边时,把先、后两条入边下标记为 first、second。它们是仅有的删除候选,因为删除别处的边无法消除这个节点的双父关系。

有双父节点时,优先尝试跳过较晚的 second,再用并查集检查剩下的边。parent、size 维护的是无向连通块,不是原有向图中的父子关系;一条边的两端已经同属一个连通块时,再加入它就会产生无向环。

跳过 second 后,每个节点入度至多为一,并且恰好剩下 n-1 条边。若没有无向环,这些边必定连接全部 n 个节点;入度之和又是 n-1,因此恰好一个节点入度为零。沿父节点回溯不会遇到环,只能到达这个唯一根,所以剩下的图确实是一棵有根有向树。此时删除 second 合法,而且它比另一候选更靠后。

若跳过 second 后仍检测到环,删除它不能恢复树。候选只有 first、second,题目又保证一定能删一条边恢复原树,因此必须返回 first,不应返回刚检测到的那条闭环边。

没有双父节点时,不跳过任何边。原树加一条边后,忽略方向的图恰有一个环;此时所有节点入度都是一,删掉任意环边都能恢复唯一根和树结构。按输入顺序合并时,环上最后出现的边才会让两端已经连通,因此首次合并失败的当前边,正是题目要求的最靠后可删边。

代码末尾只在“跳过 second 后始终无环”时才会执行 return edges[second]。没有双父的情况必定在扫描中检测到环并提前返回,不会拿 -1 当作最终下标。

解题步骤

  1. 扫描所有边,记录各节点的第一条入边,定位可能的 first、second。
  2. 初始化并查集,按原输入顺序处理边;若存在 second,暂时跳过它。
  3. 两端已经连通时,存在双父就返回 first,否则返回当前边。
  4. 没有检测到环时返回 second;合并不同连通块时按大小连接,并在查找中压缩路径。

代码实现

class Solution {
    public int[] findRedundantDirectedConnection(int[][] edges) {
        int n = edges.length;
        int first = -1;
        int second = -1;
        int[] incoming = new int[n + 1];

        Arrays.fill(incoming, -1);

        for (int i = 0; i < n; i++) {
            int child = edges[i][1];

            if (incoming[child] >= 0) {
                first = incoming[child];
                second = i;
            } else {
                incoming[child] = i;
            }
        }

        int[] parent = new int[n + 1];
        int[] size = new int[n + 1];

        for (int i = 1; i <= n; i++) {
            parent[i] = i;
            size[i] = 1;
        }

        for (int i = 0; i < n; i++) {
            if (i == second) {
                continue;
            }

            int a = find(parent, edges[i][0]);
            int b = find(parent, edges[i][1]);

            if (a == b) {
                return edges[first >= 0 ? first : i];
            }

            if (size[a] < size[b]) {
                int t = a;

                a = b;
                b = t;
            }

            parent[b] = a;
            size[a] += size[b];
        }

        return edges[second];
    }

    private int find(int[] parent, int x) {
        while (parent[x] != x) {
            parent[x] = parent[parent[x]];
            x = parent[x];
        }

        return x;
    }
}
func findRedundantDirectedConnection(edges [][]int) []int {
    n := len(edges)
    first, second := -1, -1
    incoming := make([]int, n+1)
    for i := range incoming {
        incoming[i] = -1
    }
    for i, edge := range edges {
        child := edge[1]
        if incoming[child] >= 0 {
            first, second = incoming[child], i
        } else {
            incoming[child] = i
        }
    }
    parent, size := make([]int, n+1), make([]int, n+1)
    for i := 1; i <= n; i++ {
        parent[i] = i
        size[i] = 1
    }
    find := func(x int) int {
        for parent[x] != x {
            parent[x] = parent[parent[x]]
            x = parent[x]
        }
        return x
    }
    for i, edge := range edges {
        if i == second {
            continue
        }
        a, b := find(edge[0]), find(edge[1])
        if a == b {
            if first >= 0 {
                return edges[first]
            }
            return edge
        }
        if size[a] < size[b] {
            a, b = b, a
        }
        parent[b] = a
        size[a] += size[b]
    }
    return edges[second]
}

复杂度分析

  • 时间复杂度:$O(n\alpha(n))$。第一遍扫描入边为 $O(n)$,第二遍使用按大小合并和路径压缩的并查集处理每条边,$\alpha$ 为反阿克曼函数。
  • 空间复杂度:$O(n)$,用于入边记录及并查集数组。输入边数组不被修改。

关键点总结

[!green]

  • “有根树加一条边”的保证,使异常仅有双父和环这几种组合,不能把本方法直接当作任意有向图的判树算法。
  • 并查集只检查无向环;结合已修正的入度条件和 n-1 条边,才足以证明剩余图是有根树。
  • 有双父时先尝试删除较晚入边,无双父时选择唯一环上最晚出现的边,两者分别满足靠后优先规则。

易错点总结

[!yellow]

  • 直接对全部边套无向判环,可能选到无法消除双父关系的边。
  • 跳过 second 后仍有环时,要删 first;不能继续返回 second 或当前闭环边。
  • incoming 存的是原边下标,初值必须区别于零;并查集的 parent 也不能用来替代这个入边记录。
  • 选择较晚候选后仍要验证剩余结构,不能仅凭下标靠后就直接删掉它。

相似题目

题目 难度 关联与区别
684. 冗余连接 中等 从无向冗余边扩展到有向树,必须先识别并处理入度为 2 的节点。
261. 以图判树 中等 删去候选边后的目标都是一棵树;本题还要求有向父子关系及靠后边规则。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/81061530
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!