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):
- 如果
s和t已经连通,再加这条边就会形成环 - 此时这条边就是"多余连接"
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)



