题目
在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。
输入一个有向图,该图由一个有着 n n n 个节点(节点值不重复,从 1 到 n n n)的树及一条附加的有向边构成。附加的边包含在 1 到 n n n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组 e d g e s edges edges 。 每个元素是一对 [ u i , v i ] [u_i, v_i] [ui,vi],用以表示 有向 图中连接顶点 u i u_i ui 和顶点 v i v_i vi 的边,其中 u i u_i ui 是 v i v_i vi 的一个父节点。
返回一条能删除的边,使得剩下的图是有 n n n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。
示例 1:
输入:edges = [[1,2],[1,3],[2,3]]
输出:[2,3]
示例 2:
输入:edges = [[1,2],[2,3],[3,4],[4,1],[1,5]]
输出:[4,1]
提示:
- n = = e d g e s . l e n g t h n == edges.length n==edges.length
- 3 < = n < = 1000 3 <= n <= 1000 3<=n<=1000
- e d g e s [ i ] . l e n g t h = = 2 edges[i].length == 2 edges[i].length==2
- 1 < = u i , v i < = n 1 <= u_i, v_i <= n 1<=ui,vi<=n
题解
并查集
思路
相比于 无环五向 的问题 684.冗余连接,本题是 有向 图。
且题目说明了每个节点都只有一个父节点,除了根节点没有父节点,因此在多加了一条附加的边之后,可能导致的情况有两种:
- 附加的边指向根节点,则包括根节点在内的每个节点都有一个父节点,此时图中一定有环路;
- 附加的边指向非根节点,则恰好有一个节点(即被附加的边指向的节点)有两个根节点,此时图中可能有环路也可能没有环路。
要找到附加的边,需要遍历图中的所有边构建出一棵树,在构建树的过程中寻找导致冲突(即导致一个节点有两个父节点)的边以及导致环路出现的边。
具体做法:
首先使用
p
a
r
e
n
t
parent
parent 数组记录每个节点的父节点,初始时
p
a
r
e
n
t
[
i
]
=
i
(
1
≤
i
≤
N
)
parent[i] = i\ (1 \le i \le N)
parent[i]=i (1≤i≤N),即每个节点的父节点是其自身。
另外创建并查集,初始时并查集中的每个节点都是一个连通分支,该连通分支的根节点就是该节点本身。
遍历每条边的过程中,维护导致冲突的边和导致环路出现的边,由于只有一条附加边,因此最多有一条导致冲突的边和一条导致环路出现的边。
当访问到边 [ u , v ] [u, v] [u,v] 时,进行如下操作:
- 如果此时已有 p a r e n t [ v ] ≠ v parent[v] \neq v parent[v]=v,说明 v v v 有两个父节点,将当前的边 [ u , v ] [u, v] [u,v] 记为导致冲突的边;
- 否则,令
p
a
r
e
n
t
[
v
]
=
u
parent[v] = u
parent[v]=u,然后在并查集中分别找到
u
u
u 和
v
v
v 的祖先(即各自的连通分支的根节点)。
- 如果祖先相同,说明这条边导致环路出现,将当前的边 [ u , v ] [u, v] [u,v] 记为导致环路出现的边;
- 如果祖先不同,则在并查集中将 u u u 和 v v v 进行合并。
根据上述操作,同一条边不可能同时被记为导致冲突的边和导致环路出现的边。如果访问到的边确实同时导致冲突和环路出现,则这条边被记为导致冲突的边。
在遍历图中的所有边之后,根据是否存在导致冲突的边和导致环路出现的边,得到附加的边。
如果没有导致冲突的边,说明附加的边一定导致环路出现,而且是在环路中的最后一条被访问到的边,因此附加的边即为导致环路出现的边。
如果有导致冲突的边,记这条边为 [ u , v ] [u,v] [u,v],则有两条边指向 v v v,另一条边为 [ p a r e n t [ v ] , v ] [parent[v],v] [parent[v],v],需要通过判断是否有导致环路的边决定哪条边是附加的边。
- 如果有导致环路的边,则附加的边不可能是 [ u , v ] [u,v] [u,v](因为 [ u , v ] [u,v] [u,v] 已经被记为导致冲突的边,不可能被记为导致环路出现的边),因此附加的边是 [ p a r e n t [ v ] , v ] [parent[v],v] [parent[v],v]。
- 如果没有导致环路的边,则附加的边是后被访问到的指向 v v v 的边,因此附加的边是 [ u , v ] [u,v] [u,v]。
代码
class Solution {
private:
int find(vector<int> &parent, int x) {
return parent[x] == x ? x : parent[x] = find(parent, parent[x]);
}
void merge(vector<int> &parent, int u, int v) {
parent[find(parent, u)] = find(parent, v);
}
vector<int> ancestor;
public:
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
vector<int> parent(edges.size() + 1);
for (int i = 1; i <= edges.size(); i++) {
parent[i] = i;
}
int conflict = -1;
int circle = -1;
for (int i = 0; i < edges.size(); i++) {
auto &edge = edges[i];
int node1 = edge[0], node2 = edge[1];
if (parent[node2] != node2) { //冲突,node2多个父节点,记录可能引起冲突的边数
conflict = i;
} else {
parent[node2] = node1;
if (find(parent, node1) == find(parent, node2)) { //成环了
circle = i;
} else {
merge(parent, node1, node2);
}
}
}
if (conflict < 0) { //没有导致冲突的边,则附加边只能是导致成环的边
return edges[circle];
} else {
//存在导致冲突的边
auto &conflictEdge = edges[conflict];
if (circle >= 0) //如果有导致环路的边
return vector<int>{parent[conflictEdge[1]], conflictEdge[1]};
else
return vector<int>{conflictEdge[0], conflictEdge[1]};
}
}
};
复杂度分析
- 时间复杂度: O ( N l o g N ) O(NlogN) O(NlogN),其中 N N N 是图中的节点个数。需要遍历图中的 N N N 条边,对于每条边,需要对两个节点查找祖先,如果两个节点的祖先不同则需要进行合并,需要进行 2 次查找和最多 1 次合并。一共需要进行 2 N 2N 2N 次查找和最多 N N N 次合并,因此总时间复杂度是 O ( 2 N l o g N ) = O ( N l o g N ) O(2NlogN)=O(NlogN) O(2NlogN)=O(NlogN)。这里的并查集使用了路径压缩,但是没有使用按秩合并,最坏情况下的时间复杂度是 O ( N l o g N ) O(NlogN) O(NlogN),平均情况下的时间复杂度依然是 O ( N α ( N ) ) O(Nα(N)) O(Nα(N)),其中 α \alpha α 为阿克曼函数的反函数, α ( N ) \alpha (N) α(N) 可以认为是一个很小的常数。
- 空间复杂度: O ( N ) O(N) O(N),其中 N N N 是图中的节点个数。使用数组 p a r e n t parent parent 记录每个节点的父节点,并查集使用数组记录每个节点的祖先。