「这是我参与2022首次更文挑战的第39天,活动详情查看:2022首次更文挑战」
题目介绍
原题目请见上方链接
解题思路
该题目是在一棵正常的树上面加了一条边,我们先来看看在一棵树上加上一条有向边会出现什么情况
- 第一种情况是多余的边指向的节点本来已经有了一个父节点,添加了一条边之后这个节点有两个父节点,导致冲突
- 第二种情况是多余的边指向了根节点,导致树中的每个节点都存在一个父节点,从而形成了一个环
- 第三种情况是前面两种情况的结合体,多余的边指向一个节点之后,既形成了环又有一个节点同时存在两个父节点
在一棵完整的树上添加一条有向边无非上述三种情况,接下来说一下这三种情况怎么破局:
- 对于第一种情况,我们先按题目的给出的边的顺序进行节点间的连接(记录每个节点的父节点),当一条边指向的节点已经存在了父节点,那就说明这条边是冲突的边,直接返回这条边即可
- 对于第二种情况,我们先利用并查集的思想,将题目给出的每一条边的节点连通起来,当遍历到一条边,其两头的节点已经存在于同一个集合中,那就说明这条边是形成环的边,并且是最后一条边,因此我们直接返回这条边即可
- 对于第三种情况,需要将前面的两种情况结合起来。
- 首先我们遍历每一条边,如果这条边不是冲突的边,我们就将这条边两头的节点进行连通
- 如果是冲突的边,就将这条边的下标记录起来,然后不更新当前节点的父节点,继续判断后面的边,出现了形成环的边时,记录当前成环的边的下标
- 这时因为冲突的边没有连上,而又出现了成环的边,说明附加边就是图中红色的这条边,怎么找到这条边呢?(
成环的边的父节点的父节点 -> 成环的边的父节点
的这条边)
解题代码
var findRedundantDirectedConnection = function(edges) {
const unionSet = new UnionSet(edges.length + 1)
// 用于记录每个节点的父节点
const father = []
for (let i = 1; i <= edges.length; i++) {
father[i] = i
}
// 记录冲突的边的下标
let conflict = -1
// 记录成环的边的下标
let cycle = -1
for (let i = 0; i < edges.length; i++) {
const node1 = edges[i][0], node2 = edges[i][1]
if (father[node2] !== node2) {
// 如果出现冲突的边,只记录下标,不进行连接
conflict = i
} else {
// 如果不是冲突的边,先记录父节点,然后判断是否成环
father[node2] = node1
if (unionSet.get(node1) === unionSet.get(node2)) {
cycle = i
} else {
unionSet.merge(node1, node2)
}
}
}
if (conflict === -1) {
// 如果没有冲突的边,成环的边就是附加边
return edges[cycle]
} else {
if (cycle > -1) {
// 如果既有成环的边又有附加的边,那么就是 `成环的边的父节点的父节点 -> 成环的边的父节点` 这条边
return [father[edges[conflict][1]], edges[conflict][1]]
} else {
// 如果没有成环的边,那么冲突的边就是附加边
return edges[conflict]
}
}
};
// 并查集
class UnionSet{
constructor(n) {
this.fa = []
this.size = []
for (let i = 0; i < n; i++) {
this.fa[i] = i
this.size[i] = 1
}
}
get(v) {
if (this.fa[v] === v) return v
const root = this.get(this.fa[v])
this.fa[v] = root
return root
}
merge(a, b) {
const ra = this.get(a), rb = this.get(b)
if (ra === rb) return
if (this.size[ra] < this.size[rb]) {
this.fa[ra] = rb
this.size[rb] += this.size[ra]
} else {
this.fa[rb] = ra
this.size[ra] += this.size[rb]
}
}
}
复制代码