第十一章:图论part06
108. 冗余连接
并查集应用类题目,关键是如何把题意转化成并查集问题
www.programmercarl.com/kamacoder/0…
109. 冗余连接II
上面两道题目是不是感觉做出自信了,感觉并查集不过如此?
来这道题目 给大家适当一些打击, 难度上来了。
www.programmercarl.com/kamacoder/0…
处理有向图冗余边的过程主要分为三种情况,分别是:
- 情况 1:存在一个入度为 2 的节点
- 情况 2:图中存在环
- 情况 3:图中没有入度为 2 的节点,且没有环
情况 1:存在一个入度为 2 的节点
思路
- 当一个节点的入度为 2 时,说明有两条边指向这个节点,这在有向树中是不允许的,因为树中每个节点只能有一条入度边。
- 在这种情况下,有可能存在两条冗余边,至少需要删除其中一条边。
处理方式
- 找到入度为 2 的节点:遍历所有的边,记录入度为 2 的边的索引。
- 尝试删除这两条边中的一条:
- 调用
isTreeAfterRemoveEdge函数,检查在删除某条边后,图是否仍然是树(无环,且每个节点只有一条入度边)。 - 如果删除第一条边后图仍然是树,返回这条边作为冗余边;否则返回第二条边。
- 调用
情况 2:图中存在环
思路
- 如果没有入度为 2 的节点,图中可能存在环(即任意两个节点之间可能存在路径使得它们互相连接)。
- 这种情况下,任何构成环的边都是冗余的,因为删除这条边不会影响图的连通性,但会去除环。
处理方式
- 使用
getRemoveEdge函数:遍历所有边,使用并查集检查每一条边是否会导致环的形成。 - 当检测到某条边形成环时,返回这条边:这条边即为冗余边。
情况 3:图中没有入度为 2 的节点,且没有环
思路
- 在这种情况下,图应该是一个有效的树结构(或者森林),且所有节点的入度均为 1。
- 如果出现了环,那么会在
isTreeAfterRemoveEdge中进行检测并删除。
处理方式
- 实际上,算法在这一步并不需要显式处理这一情况,因为如果没有入度为 2 的节点,且在
getRemoveEdge的过程中未发现环,算法会自动返回 null 或者不返回任何边。
总结
- 情况 1 主要处理入度为 2 的节点,通过删除冗余边来恢复树的结构。
- 情况 2 处理图中存在的环,通过返回构成环的边来解决冗余问题。
- 情况 3 直接依赖于前面两个情况的逻辑,不需要单独处理。
/**
* @param {number[][]} edges - 边的数组,表示有向图的每条边
* @return {number[]} - 表示要删除的冗余边
*/
var findRedundantDirectedConnection = function (edges) {
const n = edges.length; // 节点数量
const N = n + 1; // 为了方便处理节点索引,增加 1
let father = Array(N).fill(0); // 初始化并查集
const inDegree = Array(N).fill(0); // 记录节点的入度
// 初始化并查集
function init() {
for (let i = 1; i < N; i++) {
father[i] = i; // 每个节点的父节点初始化为自身
}
}
// 查找函数,带路径压缩
function find(u) {
if (father[u] !== u) {
father[u] = find(father[u]); // 路径压缩
}
return father[u];
}
// 合并函数
function join(u, v) {
u = find(u);
v = find(v);
if (u !== v) {
father[v] = u; // 将 v 合并到 u 所在集合
}
}
// 判断两个节点是否在同一集合
function same(u, v) {
return find(u) === find(v);
}
// 在有向图中找到需要删除的冗余边
function getRemoveEdge() {
init(); // 初始化并查集
for (let i = 0; i < n; i++) {
if (same(edges[i][0], edges[i][1])) { // 如果构成环,这条边即为冗余边
return edges[i];
}
join(edges[i][0], edges[i][1]);
}
return [];
}
// 删除指定边后判断图是否是一棵树
function isTreeAfterRemoveEdge(deleteEdge) {
init(); // 初始化并查集
for (let i = 0; i < n; i++) {
if (i === deleteEdge) continue; // 跳过要删除的边
if (same(edges[i][0], edges[i][1])) { // 若形成环,则图不是树
return false;
}
join(edges[i][0], edges[i][1]);
}
return true;
}
// 统计每个节点的入度
for (let i = 0; i < n; i++) {
inDegree[edges[i][1]]++;
}
const vec = []; // 存储入度为2的边的索引
// 找到所有入度为2的节点对应的边,倒序优先选择最后出现的
for (let i = n - 1; i >= 0; i--) {
if (inDegree[edges[i][1]] === 2) {
vec.push(i);
}
}
// 处理图中有入度为2的情况
if (vec.length > 0) {
if (isTreeAfterRemoveEdge(vec[0])) {
return edges[vec[0]];
} else {
return edges[vec[1]];
}
}
// 若没有入度为2的情况,则处理有向环的情况
return getRemoveEdge();
};
// 示例调用
const edges = [
[1, 2],
[1, 3],
[2, 3]
];
console.log(findRedundantDirectedConnection(edges)); // 示例输出
在处理有向图中冗余边的算法中,vec 用于存储入度为 2 的边的索引,通常情况下,这个数组可以包含 0 或 1 个元素,或者在特定情况下包含 2 个元素。以下是对这种情况的解释:
理论背景
在有向图中,若一个节点的入度为 2,意味着有两条边指向该节点。这种情况会导致图的结构不符合树的性质,因此需要判断哪条边是冗余的。具体来说:
- 入度为 2 的节点:会导致存在两条边指向同一个节点,至少需要删除其中一条边。
- 两个边:如果有一个节点的入度为 2,可能会存在两个边同时指向这个节点。这在某些情况下是允许的,例如图的结构不止一个指向这个节点的边(即可能有多个边)。
处理逻辑
vec可能包含 0 或 2 个元素:- 0个元素:表示图中没有节点的入度为 2,可能表示是一个树或者无环结构。
- 1个元素:表示图中仅有一个入度为 2 的边,即该节点指向了多条边,但只需处理其中一条。
- 2个元素:表示存在两个边的节点入度为 2 的情况(可以存在于复杂图结构中)。在这种情况下,必须选择删除其中的一条。
代码逻辑
在代码中:
const vec = []; // 存储入度为2的边的索引
// 找到所有入度为2的节点对应的边,倒序优先选择最后出现的
for (let i = n - 1; i >= 0; i--) {
if (inDegree[edges[i][1]] === 2) {
vec.push(i);
}
}
这个循环会倒序遍历所有边,查找所有入度为 2 的边的索引。最终,vec 可能会包含 0、1 或 2 个元素,具体取决于图的结构。
结论
虽然在许多情况下,vec 可能只包含一个元素,但它的设计是为了处理图中可能出现的所有边的情况。对于实际应用来说,你的理解是正确的,通常情况下我们主要关注 1 个元素的场景,而 0 和 2 个元素的情况则是为了确保算法的健壮性。