在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。
输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。
返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。
示例 1:
1 -----> 3
| ^
| /
| /
| /
2
输入:edges = [[1,2],[1,3],[2,3]]
输出:[2,3]
示例 2:
2 ---> 3
^ |
| \/
1 <--- 4
|
\/
5
输入:edges = [[1,2],[2,3],[3,4],[4,1],[1,5]]
输出:[4,1]
解题思路
根据树的定义,每个节点都有一个父节点,根节点没有父节点。树的变等于节点树减1。
当向树中插入一条附加边 u -> v 会出现如下2中情况:
- 有环: 在树中节点构成环路
- 有冲突: 一个节点的入度为2,也就是有2个父亲节点 因为有环和有冲突可能是并存的,那么又可以分为如下3中情况:
- 有环,无冲突 如例2
- 有环,有冲突
2 ---> 3
^ |
| \/
1 <--- 4
^
|
5
- 无环,有冲突 如例1
如何判断有环或有冲突,我们设置一个parent数组来记录每个节点,当访问到边
[u, v]
- 如果此时已有 panrent[v] !== v, 说明v有两个父节点,将当前边标记为冲突边
- 否则,令 panrent[v] = u, 然后在并查集中分别找到 uu 和 vv 的祖先(即各自的连通分支中的根节点),如果祖先相同,说明这条边导致环路出现,将当前的边标记为导致环路出现的边,如果祖先不同,则在并查集中将 u 和 v 进行合并。 解决方法
- 有环,无冲突的情况,我们只需要把构成环路的那条边删除即可
- 有环,有冲突的情况,会有2条指向同一个节点的边,即
[u,v]和[parent[v],v], 因为[u,v]是导致冲突的边,所以附加边只能是[parent[v],v] - 无环,有冲突的情况,只有一条附加边删除即可
代码
class UnionFind {
constructor(n) {
this.fa = new Array(n)
for (let i = 0; i <= n; i++) {
this.fa[i] = i;
}
}
find(x) {
if (this.fa[x] == x) return x;
let root = this.find(this.fa[x])
this.fa[x] = root
return root
}
unit(a, b) {
this.fa[this.find(a)] = this.find(b)
}
}
var findRedundantDirectedConnection = function(edges) {
let nodeCount = edges.length
let uf = new UnionFind(nodeCount+1)
let parent = []
for (let i = 1; i < (nodeCount + 1); i++) {
parent[i] = i
}
let conflit = -1
let cycle = -1
for (i in edges) {
let edge = edges[i]
let node1 = edge[0], node2 = edge[1]
if (parent[node2] != node2) { // 有冲突边
conflit = i
} else {
parent[node2] = node1
if (uf.find(node1) == uf.find(node2)) { // 有环路
cycle = i;
} else {
uf.unit(node1, node2)
}
}
}
if (conflit < 0) {
return edges[cycle]
} else {
let conflitEdge = edges[conflit]
if (cycle >= 0) {
return [parent[conflitEdge[1]], conflitEdge[1]]
} else {
return conflitEdge
}
}
};