代码随想录算法训练营 Day48 | 图论 part06

108. 多余的边

题目描述

有一个图,它是一棵树,他是拥有 n 个节点(节点编号1到n)和 n - 1 条边的连通无环无向图,例如如图:

现在在这棵树上的基础上,添加一条边(依然是n个节点,但有n条边),使这个图变成了有环图,如图:

先请你找出冗余边,删除后,使该图可以重新变成一棵树。

输入描述

第一行包含一个整数 N,表示图的节点个数和边的个数。

后续 N 行,每行包含两个整数 s 和 t,表示图中 s 和 t 之间有一条边。

输出描述

输出一条可以删除的边。如果有多个答案,请删除标准输入中最后出现的那条边。

输入示例

复制代码
3
1 2
2 3
1 3

输出示例

复制代码
1 3
cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int n;
// father[i] 表示节点 i 的父节点
vector<int> father(1001, -1);
// 初始化:每个节点的父节点指向自己
void init() {
    for (int i = 1; i <= n; i++) {
        father[i] = i;
    }
}
// 查找根节点(路径压缩)
int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]);
}
// 合并两个节点
// 如果已经在同一集合,返回 false(说明出现环)
// 否则合并并返回 true
bool join(int u, int v) {
    u = find(u);
    v = find(v);
    // 已经连通,说明这条边会形成环
    if (u == v) return false;
    // 合并
    father[v] = u;
    return true;
}

int main() {
    cin >> n;
    init();
    // 输入 n 条边
    for (int i = 0; i < n; i++) {
        int s, t;
        cin >> s >> t;
        // 如果 join 失败,说明出现了环
        if (!join(s, t)) {
            cout << s << " " << t << endl;
            return 0;
        }
    }
    return 0;
}

总结

1. 核心思路

使用并查集判断图中是否出现环。

遍历每条边 (s, t)

  • 如果 st 已经连通,再加这条边就会形成环
  • 此时这条边就是"多余连接"
2. 关键点
  • join(s, t)
    • 返回 true:成功合并
    • 返回 false:说明已经在同一集合(出现环)
3. 本质理解

这是经典问题:

检测无向图中的环 / 找到第一条成环的边

4. 复杂度
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

109. 多余的边II

题目描述

有一种有向树,该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。有向树拥有 n 个节点和 n - 1 条边。如图:

现在有一个有向图,有向图是在有向树中的两个没有直接链接的节点中间添加一条有向边。如图:

输入一个有向图,该图由一个有着 n 个节点(节点编号 从 1 到 n),n 条边,请返回一条可以删除的边,使得删除该条边之后该有向图可以被当作一颗有向树。

输入描述

第一行输入一个整数 N,表示有向图中节点和边的个数。

后续 N 行,每行输入两个整数 s 和 t,代表这是 s 节点连接并指向 t 节点的单向边

输出描述

输出一条可以删除的边,若有多条边可以删除,请输出标准输入中最后出现的一条边。

输入示例

复制代码
3
1 2
1 3
2 3

输出示例

复制代码
2 3
cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int n;
vector<int> father(1001, -1);
// 初始化并查集
void init() {
    for (int i = 1; i <= n; i++) father[i] = i;
}
// 查找根节点(路径压缩)
int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]);
}
// 合并
// 如果已经在同一集合,返回 false(出现环)
bool join(int u, int v) {
    u = find(u);
    v = find(v);
    if (u == v) return false;
    father[v] = u;
    return true;
}
// 判断删除第 x 条边后,图是否无环
bool isMove(vector<vector<int>>& edges, int x) {
    init();

    for (int i = 0; i < n; i++) {
        if (i == x) continue; // 跳过这条边

        // 如果出现环,说明删除这条边不够
        if (!join(edges[i][0], edges[i][1])) return false;
    }
    return true;
}
// 没有入度为 2 的情况,直接找成环边
void justMove(vector<vector<int>>& edges) {
    init();

    for (int i = 0; i < n; i++) {
        // 第一次出现 join 失败的边,就是要删除的边
        if (!join(edges[i][0], edges[i][1])) {
            cout << edges[i][0] << " " << edges[i][1] << endl;
            return;
        }
    }
}

int main() {
    cin >> n;
    vector<vector<int>> edges;
    vector<int> indegree(n + 1, 0);
    // 输入边,同时统计入度
    for (int i = 0; i < n; i++) {
        int s, t;
        cin >> s >> t;
        edges.push_back({s, t});
        indegree[t]++;
    }
    vector<int> vec;
    // 找入度为 2 的边(倒序,保证后出现的优先)
    for (int i = n - 1; i >= 0; i--) {
        if (indegree[edges[i][1]] == 2) {
            vec.push_back(i);
        }
    }
    // 情况1:存在入度为2
    if (vec.size() > 0) {
        // 尝试删除其中一条
        if (isMove(edges, vec[0])) {
            cout << edges[vec[0]][0] << " " << edges[vec[0]][1] << endl;
        } else {
            cout << edges[vec[1]][0] << " " << edges[vec[1]][1] << endl;
        }
    }
    // 情况2:没有入度为2,说明纯环
    else {
        justMove(edges);
    }
    return 0;
}

总结

1. 题目本质

这是经典问题:

有向图中删除一条边,使其成为有根树

(LeetCode:冗余连接 II)

2. 核心分类

分两种情况:

① 有节点入度为 2

说明有两个父节点,需要删一条:

  • 尝试删除其中一条边
  • 用并查集判断是否成环
  • 能保证无环的那条就是答案

② 所有节点入度 ≤ 1

说明问题是"环":

  • 用并查集找第一条形成环的边
  • 直接删除
3. 并查集作用
  • 判断是否形成环
  • join失败 ⇒ 出现环
4. 实现关键点
  • 入度数组:找"入度为2"
  • 倒序遍历:保证删除的是最后出现的边
  • isMove():验证删除某条边是否可行
5. 复杂度
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
相关推荐
cheems95271 小时前
[算法手记] 动态规划 ,二维费用限制背包问题如何处理
算法·动态规划
Chase_______1 小时前
LeetCode 1343 题解:定长滑动窗口经典入门题,从暴力枚举到高效优化一文搞懂
算法·leetcode·职场和发展
样例过了就是过了1 小时前
LeetCode热题100 单词拆分
c++·算法·leetcode·动态规划·哈希算法
王老师青少年编程1 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【跳跃与过河问题】:跳跳!
c++·算法·贪心·csp·信奥赛·跳跃与过河问题·跳跳
MediaTea2 小时前
ML:决策树的基本原理与实现
人工智能·算法·决策树·机器学习·数据挖掘
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【跳跃与过河问题】:独木桥
c++·算法·贪心·csp·信奥赛·跳跃与过河问题·独木桥
忡黑梨2 小时前
eNSP_DHCP配置
c语言·网络·c++·python·算法·网络安全·智能路由器
陈壮实的搬砖日记2 小时前
白话生成式推荐二:MiniOneRec之RQ-VAE
算法
陈壮实的搬砖日记2 小时前
白话生成式推荐二:MiniOneRec之SFT
算法