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 。
输入: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的额外空间