leetcode-master/problems/kamacoder/0109.冗余连接II.md

384 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center"><strong><a href="./qita/join.md">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!</strong></p>
# 109. 冗余连接II
[卡码网题目链接ACM模式](https://kamacoder.com/problempage.php?pid=1182)
题目描述
有向树指满足以下条件的有向图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。有向树拥有 n 个节点和 n - 1 条边。
输入一个有向图,该图由一个有着 n 个节点(节点编号 从 1 到 nn 条边,请返回一条可以删除的边,使得删除该条边之后该有向图可以被当作一颗有向树。
输入描述
第一行输入一个整数 N表示有向图中节点和边的个数。
后续 N 行,每行输入两个整数 s 和 t代表 s 节点有一条连接 t 节点的单向边
输出描述
输出一条可以删除的边,若有多条边可以删除,请输出标准输入中最后出现的一条边。
输入示例
```
3
1 2
1 3
2 3
```
输出示例
2 3
提示信息
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240527112633.png)
在删除 2 3 后有向图可以变为一棵合法的有向树,所以输出 2 3
数据范围:
1 <= N <= 1000.
## 思路
本题与 [108.冗余连接](./0108.冗余连接.md) 类似,但本题是一个有向图,有向图相对要复杂一些。
本题的本质是 :有一个有向图,是由一颗有向树 + 一条有向边组成的 (所以此时这个图就不能称之为有向树),现在让我们找到那条边 把这条边删了,让这个图恢复为有向树。
还有“**若有多条边可以删除,请输出标准输入中最后出现的一条边**”,这说明在两条边都可以删除的情况下,要删顺序靠后的边!
我们来想一下 有向树的性质如果是有向树的话只有根节点入度为0其他节点入度都为1因为该树除了根节点之外的每一个节点都有且只有一个父节点而根节点没有父节点
所以情况一如果我们找到入度为2的点那么删一条指向该节点的边就行了。
如图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240527115807.png)
找到了节点3 的入度为2删 1 -> 3 或者 2 -> 3 。选择删顺序靠后便可。
但 入度为2 还有一种情况,情况二,只能删特定的一条边,如图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240527151456.png)
节点3 的入度为 2但在删除边的时候只能删 这条边节点1 -> 节点3如果删这条边节点4 -> 节点3那么删后本图也不是有向树了因为找不到根节点
综上如果发现入度为2的节点我们需要判断 删除哪一条边,删除后本图能成为有向树。如果是删哪个都可以,优先删顺序靠后的边。
情况三: 如果没有入度为2的点说明 图中有环了(注意是有向环)。
如图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240527120531.png)
对于情况三,删掉构成环的边就可以了。
## 写代码
把每条边记录下来,并统计节点入度:
```cpp
int s, t;
vector<vector<int>> edges;
cin >> n;
vector<int> inDegree(n + 1, 0); // 记录节点入度
for (int i = 0; i < n; i++) {
cin >> s >> t;
inDegree[t]++;
edges.push_back({s, t});
}
```
前两种入度为2的情况一定是删除指向入度为2的节点的两条边其中的一条如果删了一条判断这个图是一个树那么这条边就是答案。
同时注意要从后向前遍历,因为如果两条边删哪一条都可以成为树,就删最后那一条。
代码如下:
```cpp
vector<int> vec; // 记录入度为2的边如果有的话就两条边
// 找入度为2的节点所对应的边注意要倒序因为优先删除最后出现的一条边
for (int i = n - 1; i >= 0; i--) {
if (inDegree[edges[i][1]] == 2) {
vec.push_back(i);
}
}
if (vec.size() > 0) {
// 放在vec里的边已经按照倒叙放的所以这里就优先删vec[0]这条边
if (isTreeAfterRemoveEdge(edges, vec[0])) {
cout << edges[vec[0]][0] << " " << edges[vec[0]][1];
} else {
cout << edges[vec[1]][0] << " " << edges[vec[1]][1];
}
return 0;
}
```
再来看情况三明确没有入度为2的情况那么一定有向环找到构成环的边就是要删除的边。
可以定义一个函数,代码如下:
```cpp
// 在有向图里找到删除的那条边,使其变成树
void getRemoveEdge(const vector<vector<int>>& edges)
```
大家应该知道了,我们要解决本题要实现两个最为关键的函数:
* `isTreeAfterRemoveEdge()` 判断删一个边之后是不是有向树
* `getRemoveEdge()` 确定图中一定有了有向环,那么要找到需要删除的那条边
此时就用到**并查集**了。
如果还不了解并查集,可以看这里:[并查集理论基础](https://programmercarl.com/kamacoder/图论并查集理论基础.html)
`isTreeAfterRemoveEdge()` 判断删一个边之后是不是有向树: 将所有边的两端节点分别加入并查集,遇到要 要删除的边则跳过,如果遇到即将加入并查集的边的两端节点 本来就在并查集了,说明构成了环。
如果顺利将所有边的两端节点(除了要删除的边)加入了并查集,则说明 删除该条边 还是一个有向树
`getRemoveEdge()`确定图中一定有了有向环,那么要找到需要删除的那条边: 将所有边的两端节点分别加入并查集,如果遇到即将加入并查集的边的两端节点 本来就在并查集了,说明构成了环。
本题C++代码如下:(详细注释了)
```cpp
#include <iostream>
#include <vector>
using namespace std;
int n;
vector<int> father (1001, 0);
// 并查集初始化
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]);
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return ;
father[v] = u;
}
// 判断 u 和 v是否找到同一个根
bool same(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 在有向图里找到删除的那条边,使其变成树
void getRemoveEdge(const vector<vector<int>>& edges) {
init(); // 初始化并查集
for (int i = 0; i < n; i++) { // 遍历所有的边
if (same(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边
cout << edges[i][0] << " " << edges[i][1];
return;
} else {
join(edges[i][0], edges[i][1]);
}
}
}
// 删一条边之后判断是不是树
bool isTreeAfterRemoveEdge(const vector<vector<int>>& edges, int deleteEdge) {
init(); // 初始化并查集
for (int 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;
}
int main() {
int s, t;
vector<vector<int>> edges;
cin >> n;
vector<int> inDegree(n + 1, 0); // 记录节点入度
for (int i = 0; i < n; i++) {
cin >> s >> t;
inDegree[t]++;
edges.push_back({s, t});
}
vector<int> vec; // 记录入度为2的边如果有的话就两条边
// 找入度为2的节点所对应的边注意要倒序因为优先删除最后出现的一条边
for (int i = n - 1; i >= 0; i--) {
if (inDegree[edges[i][1]] == 2) {
vec.push_back(i);
}
}
// 情况一、情况二
if (vec.size() > 0) {
// 放在vec里的边已经按照倒叙放的所以这里就优先删vec[0]这条边
if (isTreeAfterRemoveEdge(edges, vec[0])) {
cout << edges[vec[0]][0] << " " << edges[vec[0]][1];
} else {
cout << edges[vec[1]][0] << " " << edges[vec[1]][1];
}
return 0;
}
// 处理情况三
// 明确没有入度为2的情况那么一定有有向环找到构成环的边返回就可以了
getRemoveEdge(edges);
}
```
## 其他语言版本
### Java
### Python
### Go
### Rust
### Javascript
```javascript
const r1 = require('readline').createInterface({ input: process.stdin });
// 创建readline接口
let iter = r1[Symbol.asyncIterator]();
// 创建异步迭代器
const readline = async () => (await iter.next()).value;
let N // 节点数和边数
let father = [] // 并查集
let edges = [] // 边集
let inDegree = [] // 入度
// 并查集初始化
const init = () => {
for (let i = 1; i <= N; i++) father[i] = i;
}
// 并查集里寻根的过程
const find = (u) => {
return u == father[u] ? u : father[u] = find(father[u])
}
// 将v->u 这条边加入并查集
const join = (u, v) => {
u = find(u)
v = find(v)
if (u == v) return // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u
}
// 判断 u 和 v是否找到同一个根
const isSame = (u, v) => {
u = find(u)
v = find(v)
return u == v
}
// 判断删除一条边后是不是树
const isTreeAfterRemoveEdge = (edges, edge) => {
// 初始化并查集
init()
for (let i = 0; i < N; i++) {
if (i == edge) continue
if (isSame(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树
return false
}
join(edges[i][0], edges[i][1])
}
return true
}
// 在有向图里找到删除的那条边, 使其成为树
const getRemoveEdge = (edges) => {
// 初始化并查集
init()
for (let i = 0; i < N; i++) {
if (isSame(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边
console.log(edges[i][0], edges[i][1]);
return
} else {
join(edges[i][0], edges[i][1])
}
}
}
(async function () {
// 读取第一行输入
let line = await readline();
N = Number(line);
// 读取边信息, 统计入度
for (let i = 0; i < N; i++) {
line = await readline()
line = line.split(' ').map(Number)
edges.push(line)
inDegree[line[1]] = (inDegree[line[1]] || 0) + 1
}
// 找到入度为2的节点
let vec = [] // 记录入度为2的边如果有的话就两条边
// 找入度为2的节点所对应的边注意要倒序因为优先删除最后出现的一条边
for (let i = N - 1; i >= 0; i--) {
if (inDegree[edges[i][1]] == 2) {
vec.push(i)
}
}
// 情况一、情况二
if (vec.length > 0) {
// 放在vec里的边已经按照倒叙放的所以这里就优先删vec[0]这条边
if (isTreeAfterRemoveEdge(edges, vec[0])) {
console.log(edges[vec[0]][0], edges[vec[0]][1]);
} else {
console.log(edges[vec[1]][0], edges[vec[1]][1]);
}
return 0
}
// 情况三
// 明确没有入度为2的情况那么一定有有向环找到构成环的边返回就可以了
getRemoveEdge(edges)
})()
```
### TypeScript
### PhP
### Swift
### Scala
### C#
### Dart
### C