【并查集】685. 冗余连接II

在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。
输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。
返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。

解题思路:分类讨论 + 并查集

这道题乍一看,和684. 冗余连接非常像!只不过从无向变成了有向。
但是这个难度提升了一个喜马拉雅山,主要是要想好一共有几种情况,针对每种情况去解决。

1、分析

这道题分类讨论非常关键!
这道题分类讨论非常关键!
这道题分类讨论非常关键!
(重要的事情说三遍,不然你会马上晕的就不知道天南地北了)

首先我们要明确一个观念,题目说了,人家本来就是一颗有向树,只不过现在在这个基础上新增了一条边,变成了错误的一棵树。而我们只要删除一条错误的边,就可以纠正了!【也就是说,即使有多种删除方案,但只要删除一条,而且是边集中最右边的那条】
——那么如何找到这条错误的边呢?

● 我们先从有向树的定义入手:
(1) 每个孩子仅有一个父节点;
(2) 有且仅有一个根节点。

      
⭐ 对应的,这条错误的边(我们认为只有一条)一定导致以下异常情况之一
● 情况一:有一个节点有两个父节点;
——问题:为什么不是3个,4个或者更多呢?
 因为如果是3个,我们就要删除2条边,4个就要删除3条边了!这不符合题目要求。
——问题:那为什么不是0个呢?
 一棵标准的有向树会有一个根节点,它的父节点是0不是很正常吗!!
● 情况二:(每个节点仅有一个父节点)没有根节点【形成有向环】。
——问题:会不会有多个根节点呢?
 不会!因为如果存在多个根节点,就算不删除边的情况下,树都无法连通。(更何况我们还要删除一条边)

2、解题思路:

● 处理情况一:假设有节点node,他的两个父节点分别是parent1、parent2。那么必然删除边:[parent1, node]或者[parent2, node]。
 我们可以尝试去删除第二条出现在边集的边,假设是[parent2, node]
 (1) 如果删除后,剩余边集可以构成一棵有向树。那么我们直接删除[parent2, node]即可
 因为就算删除第一条边也可以构成有向树,但是因为我们要返回的是靠右的边。
 (2) 如果删除后,剩余边集不可以构成一棵有向树。那么我们只能直接删除[parent1, node]

⭕ 那——怎么判断是否可以构成有向树?
利用并查集。定义connect[i]为节点 i 的父节点所在集合,初始每个节点的connect[i]是自己。
① 连通。如果并查集中所有节点的父节点集合是同一个,说明是连通的;
② 没有环。是的!你没有看错,【异常情况一有可能会出现环!】
如下图所示,如果此时删除[3, 2],那么会剩下环。

异常情况一:既有两个父节点,也有环

判断是否有环:就很简单了,根据并查集,【如果父节点的connect集合和子节点的connect集合一样,就说明如果加入这条边会导致有向环!】,此时不能构成有向树。

以上两个条件均满足,就说明可以构成树!如果有一个不满足就不能构成树。那么要先判断哪个呢?

如果我们直接先进行步骤①,会导致什么结果?
此时节点1、2、4的父节点集合是什么?——无限循环,根本找不到它的父节点集合!\color{red}{所以这里要先处理有环的情况,再处理连通的情况。}

● 处理情况二:这里就很简单了,和情况一的第②步处理一样,只要出现环,那么这条边一定要删除。

⭐ 另外,先判断情况一,在情况一不满足的情况下,再去处理情况二!
——为什么?你也看到了,情况二是通过环来处理的,但是情况一也可能会有环。
那我如果现在有环,是哪个情况呢?
如果是情况一,是删除两个父节点连接边其中之一呢?还是删除环那条呢?情况就太复杂了。
如果先处理情况一,判断确实没有任何一个点的父节点超过两个。那么我们就能肯定是情况二,返回出现环的边。

3、代码实现
class Solution {
    private int[] connect;
    public int[] findRedundantDirectedConnection(int[][] edges) {
        // 分析题目:已经是一个有向树的情况下,新增一条边,导致有向树不满足定义。
        //          因此,只要删除一条有问题的边,剩下的一定是一颗有向树。
        
        // 看两种不符合条件的情况
        // 1. 某一个节点存在两个父节点
        // 2. 不是情况1时,说明每个节点仅有一个父节点,此时一定存在有向环(没有根)

        // 初始化并查集
        connect = new int[edges.length + 1];
        for(int i=0; i<connect.length; i++){
            connect[i] = i;
        }
        
        int[] indegree = new int[edges.length+1];
        int i = 0;
        for(; i<edges.length; i++){
            indegree[edges[i][1]]++;
            if(indegree[edges[i][1]] > 1) break;
        }

        // 1. 第一种情况:某个节点有两个父节点(为什么现要判断第一种情况呢,因为第一种情况也会出现环这种情况,没办法解耦)
        if(i < edges.length){
            // 一定是删除其中一个父节点连接到该节点的边

            // 如果删除第二条出现的边
            // (1) 能构成树,说明应该删除第二条出现的边;
            // (2) 不能构成树,说明应该删除第一条边
            int j = 0;
            for(; j<edges.length; j++){
                if(edges[j][1] == edges[i][1]) break;
            }
            if(isTree(edges, i)) return new int[]{edges[i][0], edges[i][1]};
            else return new int[]{edges[j][0], edges[j][1]};
        }else{
            // 说明此时没有根,存在有向环
            i = 0;
            for(; i<edges.length; i++){
                if(!merge(edges[i][0], edges[i][1])) break;
            }
            return new int[]{edges[i][0], edges[i][1]};
        }

    }
    public int find(int x){
        while(x != connect[x]){
            x = connect[x];
        }
        return x;
    }
    // 父节点是x, 孩子节点是y,有x→y
    public boolean merge(int x, int y){
        int x_root = find(x);
        int y_root = find(y);
        if(x_root == y_root) return false;
        else{
            connect[y_root] = x_root; // 有向,必须是这个合并顺序
            return true;
        }
    }
    // 如果删除这条边后,并查集满足:是全连通 + 没有环,就说明是树
    public boolean isTree(int[][] edges, int removeIdx){
        // 环检查
        for(int i=0; i<edges.length; i++){
            if(i == removeIdx) continue;
            if(!merge(edges[i][0], edges[i][1])) return false;
        }
        // 是全连通图
        int root = -1;
        for(int i=1; i<connect.length; i++){
            int tmp = find(i);
            if(root == -1) root = tmp;
            else if(tmp != root) return false;
        }
        return true;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容