算法-保证图可完全遍历-并查集

1、题目介绍

Alice 和 Bob 共有一个无向图,其中包含 n 个节点和 3  种类型的边:

类型 1:只能由 Alice 遍历。
类型 2:只能由 Bob 遍历。
类型 3:Alice 和 Bob 都可以遍历。
给你一个数组 edges ,其中 edges[i] = [typei, ui, vi] 表示节点 ui 和 vi 之间存在类型为 typei 的双向边。请你在保证图仍能够被 Alice和 Bob 完全遍历的前提下,找出可以删除的最大边数。如果从任何节点开始,Alice 和 Bob 都可以到达所有其他节点,则认为图是可以完全遍历的。

返回可以删除的最大边数,如果 Alice 和 Bob 无法完全遍历图,则返回 -1
image-20210127170828361
输入:n = 4, edges = [[3,1,2],[3,2,3],[1,1,3],[1,2,4],[1,1,2],[2,3,4]]
输出:2
解释:如果删除 [1,1,2][1,1,3] 这两条边,Alice 和 Bob 仍然可以完全遍历这个图。再删除任何其他的边都无法保证图可以完全遍历。所以可以删除的最大边数是 2

2、题目分析

  • 考虑使用贪心并查集解决本问题,将删除边的操作转换为添加边

  • 1、贪心:

    • 公共边/类型3可以让Alian和Bob都遍历,首先考虑添加公共边:如果两个节点已经在一个连通分量,那么添加公共边无意义,这条公共边被舍弃;如果不在,那么添加公共边并更新连通分量
    • Alian/Bob独占边:插入公共边之后开始插入独占边,如果对Alian/Bob某一方两个端点已连通的时候,就要舍弃这条边;否则就添加且更新连通分支
    • 最后输出/返回舍弃的边的数量
  • 2、并查集:先了解一下什么是并查集,并查集的作用是合并相关联的节点并检查两个节点是否先关联,特别适合对图的连通分支的操作。

    看到并查集的初始化代码之后,可以把并查集想象成是n个互不相干的根节点:如果要union合并两个节点,就把一个节点变成另外一个的子节点,这样两个节点就属于一个连通分支了;如果要检查两个节点是否连通,比较根节点是否一致即可;

    class UnionFind{
        //节点数组
        private int[] parent;
        //数组大小
        private int size;
        //记录剩余的连通分支数
        private int count;
        
        //带参的构造函数
        public UnionFind(int n){
            this.size = n;
            this.count = size;
            this.parent = new int[size];
            for(int i = 0;i < size;i++){
                parent[i] = i;
            }
        }
        
        //查找函数,找到根节点
        public int find(int x){
            //递归查找,比while循环要稍快
            if(x != parent[x]){
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }
        
        //合并函数,将一个节点和另外一个连接
        public void union(int x,int y){
            //x的根值
            int x_root = find(x);
            //y的根值
            int y_root = find(y);
            //x和y有相同的根,证明在一棵树/连通分支上
            if(x_root = y_root)
                return;
            //不在一个连通分支上则连通两个节点
            parent[x_root] = y_root;
            //有合并操作的时候,连通分支少一个
            count--;
        }
        
        //判断函数,判断两个节点是否在一个连通分支上
        public boolean isConnected(int x,int y){
            return find(x) == find(y);
        }
        
        //返回连通分支数,count使用private修饰符保护
        public int getCount(){
            return count;
        }
    }
    
  • 操作记录,要设置一个变量记录最后剩余的连通分支数量,每次有合并的时候连通分支数目减一

3、代码分析

class Solution {
    public int maxNumEdgesToRemove(int n, int[][] edges) {
        //为每个人初始化一个并查集对象
        UnionFind un_Alian = new UnionFind(n);
        UnionFind un_Bob= new UnionFind(n);
        int res = 0;
        
        for(int[] edge : edges){
            //1、先找出所有公共边
            if(edge[0] == 3){
                //当前两个节点已经连通,删去一条边,加入公共边,因为两个都设置,所有判断一条边即可
                if(!un_Alian.union(edge[1],edge[2])/* || un_Bob.isConnected(edge[1],edge[2])*/){
                    res++;
                }
                //不连通的时候,此时union()已经将Alian相连,只需要连接Bob
                else{
                    un_Bob.union(edge[1],edge[2]);
                }
            }
        }
        //独占边情况
        for(int[] edge : edges){
            //2、Alice独占的边
            if(edge[0] == 1){
                if(!un_Alian.union(edge[1],edge[2])){
                    ++res;
                }  
            }
            //3、Bob独占的边
            else if(edge[0] == 2){
                if(!un_Bob.union(edge[1],edge[2])){
                    ++res;
                }
            }
        }
        //不连通的时候
        if(un_Alian.getCount() != 1 || un_Bob.getCount() != 1){
            return -1;
        }
        return res;
    }
}

//并查集模板
class UnionFind{
    private int[] parent;
    private int size;
    private int count;

    public UnionFind(int n){
        this.size = n;
        this.count = n;
        parent = new int[size + 1];
        //节点编号从1开始,也可以不动模板改动本题的函数
        for(int i = 1;i <= size;++i){
            parent[i] = i;
        }
    }

    public int find(int x){//超时的原因在这里
        if(x != parent[x]){
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }
    public boolean isConnected(int x,int y){
        return find(x) == find(y);
    }
    //加一个判断功能,返回值是boolean型
    public boolean union(int x,int y){
         int x_root = find(x);
         int y_root = find(y);
         if(x_root == y_root) return false;
         parent[x_root] = y_root;
         count--;
         return true;
    }

    public int getCount(){
        return count;
    }
}

4、复杂度分析

  • 时间复杂度:O(m * a(n)),m是数组的长度,a()是阿克曼函数的反函数
  • 空间复杂度:O(n),new两个并查集需要2n的额外空间
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值