DAY54

108 阅读6分钟

第十一章:图论part06

108.  冗余连接

并查集应用类题目,关键是如何把题意转化成并查集问题

www.programmercarl.com/kamacoder/0…

109.  冗余连接II

上面两道题目是不是感觉做出自信了,感觉并查集不过如此?

来这道题目 给大家适当一些打击, 难度上来了。

www.programmercarl.com/kamacoder/0…

处理有向图冗余边的过程主要分为三种情况,分别是:

  1. 情况 1:存在一个入度为 2 的节点
  2. 情况 2:图中存在环
  3. 情况 3:图中没有入度为 2 的节点,且没有环

情况 1:存在一个入度为 2 的节点

思路
  • 当一个节点的入度为 2 时,说明有两条边指向这个节点,这在有向树中是不允许的,因为树中每个节点只能有一条入度边。
  • 在这种情况下,有可能存在两条冗余边,至少需要删除其中一条边。
处理方式
  1. 找到入度为 2 的节点:遍历所有的边,记录入度为 2 的边的索引。
  2. 尝试删除这两条边中的一条
    • 调用 isTreeAfterRemoveEdge 函数,检查在删除某条边后,图是否仍然是树(无环,且每个节点只有一条入度边)。
    • 如果删除第一条边后图仍然是树,返回这条边作为冗余边;否则返回第二条边。

情况 2:图中存在环

思路
  • 如果没有入度为 2 的节点,图中可能存在环(即任意两个节点之间可能存在路径使得它们互相连接)。
  • 这种情况下,任何构成环的边都是冗余的,因为删除这条边不会影响图的连通性,但会去除环。
处理方式
  1. 使用 getRemoveEdge 函数:遍历所有边,使用并查集检查每一条边是否会导致环的形成。
  2. 当检测到某条边形成环时,返回这条边:这条边即为冗余边。

情况 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,意味着有两条边指向该节点。这种情况会导致图的结构不符合树的性质,因此需要判断哪条边是冗余的。具体来说:

  1. 入度为 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 个元素的情况则是为了确保算法的健壮性。