题目地址:
https://leetcode.com/problems/redundant-connection-ii/
给定一个无权简单有向图,顶点编号是 1 ∼ n 1\sim n 1∼n,去掉一条边之后能变成一棵有根树。问去掉哪条边。题目保证答案存在。如果存在多个解,则返回下标更大的那条边。
思路是并查集。可以模仿https://blog.csdn.net/qq_46105170/article/details/105919590里面的做法。但是这里有一点关键的不同,那一题中是无向图,所以去掉环里的哪条边都是可以的,但是本题,即使加上当前边就发现了环(这里的环不考虑边的方向),也不能判断是否应该去掉当前边(因为有可能去掉当前边之后,其作为无向图确实是树,但是作为有向图依然不是有根树)。我们先考虑一棵有根树加上一条边,有哪些情况:
1、是从其中一个顶点到另外一个非树根的顶点加上一条边,那么就会产生某个顶点有两个父亲。这种情况下,只需要将那个有两个父亲的顶点删去那个下标较大的成环的入边即可(这里的环也是指无向图的环);
2、是从其中一个顶点到树根加上一条边,此时每个顶点仍然只有一个父亲。这种情况下,图里一定有环(从树根先走到那个顶点,然后又回到树根),环的任意一条边删去都是可以的。
所以算法可以这样设计:
1、先统计一下每个顶点的父亲是谁,如果统计都某一时刻发现某个顶点有两个父亲,那么这个顶点的两条入边就是备选答案,我们先将其存起来(此时还不能判断这两个哪个是解,也有可能都是合法解。由于题目要求如果有多个解返回下标较大的那个,我们这里存的时候也按照下标先后次序排序),同时,将其中一条边断开(代码里直接标记一下这条边不可用即可)。这里断开的目的是为了区分两条边哪个是成环的边。在下面有解释;
2、接着开个并查集,将原图视为无向图,来模仿https://blog.csdn.net/qq_46105170/article/details/105919590里面的做法。依次union每条边的两个顶点(略过不可用的边)。一旦发现某次union的时候,两个顶点已经处于同一个集合了,那么就说明产生了环。我们先查一下备选解有多少个。如果之前没有存备选解,则说明没有顶点有两个父亲,那么此时成环的边就是当前边,直接返回当前边即可;如果有备选解,那么答案就是备选解里没标记断开的那个(说明没断开的那个成环了)。遍历完毕之后如果没return,则说明被断开的那条边是成环的,将其返回。
代码如下:
import java.util.Arrays;
public class Solution {
class UnionFind {
private int[] parent;
public UnionFind(int size) {
parent = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public boolean union(int x, int y) {
int px = find(x), py = find(y);
if (px == py) {
return false;
}
parent[px] = py;
return true;
}
}
public int[] findRedundantDirectedConnection(int[][] edges) {
int n = edges.length;
// 顶点编号是1 ~ n的,我们这里多开一个位置
int[] father = new int[n + 1];
Arrays.fill(father, -1);
int[][] res = null;
for (int[] edge : edges) {
int from = edge[0], to = edge[1];
// 如果某个顶点有两个父亲,则将这两条边存起来,并断开其中一条
if (father[to] != -1) {
res = new int[][]{{father[to], to}, {from, to}};
edge[0] = edge[1] = -1;
break;
}
father[to] = from;
}
UnionFind uf = new UnionFind(n + 1);
for (int[] edge : edges) {
int from = edge[0], to = edge[1];
// 略过被断开的边
if (from == -1 || to == -1) {
continue;
}
// 如果from和to已经在同一个集合里了,那么查一下备选答案,
// 如果备选答案为空,则说明要返回最后一个成环的边,即当前边;
// 否则备选答案里没断开的那条边是答案
if (!uf.union(from, to)) {
return res == null ? edge : res[0];
}
}
// 如果除了断开的那条边,所有边都不成环,那么断开的那条边成环了,返回之
return res[1];
}
}
时间复杂度 O ( n log ∗ n ) O(n\log ^* n) O(nlog∗n),空间 O ( n ) O(n) O(n)。