
<p align="center"><strong><a href="./qita/join.md">参与本项目</a>，贡献其他语言版本的代码，拥抱开源，让更多学习算法的小伙伴们受益！</strong></p>

# 108. 冗余连接 

[卡码网题目链接（ACM模式）](https://kamacoder.com/problempage.php?pid=1181)

题目描述

有一个图，它是一棵树，他是拥有 n 个节点（节点编号1到n）和 n - 1 条边的连通无环无向图（其实就是一个线形图），如图：

![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240905163122.png)

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

![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240905164721.png)

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

输入描述

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

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

输出描述

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

输入示例

```
3
1 2
2 3
1 3
```

输出示例

1 3

提示信息

![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240527110320.png)

图中的 1 2，2 3，1 3 等三条边在删除后都能使原图变为一棵合法的树。但是 1 3 由于是标准输出里最后出现的那条边，所以输出结果为 1 3

数据范围：

1 <= N <= 1000.

## 思路

这道题目也是并查集基础题目。

这里我依然降调一下，并查集可以解决什么问题：两个节点是否在一个集合，也可以将两个节点添加到一个集合中。

如果还不了解并查集，可以看这里：[并查集理论基础](./图论并查集理论基础.md)

我们再来看一下这道题目。

题目说是无向图，返回一条可以删去的边，使得结果图是一个有着N个节点的树（即：只有一个根节点）。

如果有多个答案，则返回二维数组中最后出现的边。

那么我们就可以从前向后遍历每一条边（因为优先让前面的边连上），边的两个节点如果不在同一个集合，就加入集合（即：同一个根节点）。


如图所示，节点A 和节点 B 不在同一个集合，那么就可以将两个 节点连在一起。

![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230604104720.png)

如果边的两个节点已经出现在同一个集合里，说明着边的两个节点已经连在一起了，再加入这条边一定就出现环了。

如图所示： 

![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230604104330.png)

已经判断 节点A  和 节点B 在在同一个集合（同一个根），如果将 节点A 和 节点B 连在一起就一定会出现环。 

这个思路清晰之后，代码就很好写了。

并查集C++代码如下：

```CPP
#include <iostream>
#include <vector>
using namespace std;
int n; // 节点数量
vector<int> father(1001, 0); // 按照节点大小范围定义数组

// 并查集初始化
void init() {
    for (int i = 0; i <= n; ++i) {
        father[i] = i;
    }
}
// 并查集里寻根的过程
int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]);
}
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
    u = find(u); // 寻找u的根
    v = find(v); // 寻找v的根
    if (u == v) return ; // 如果发现根相同，则说明在一个集合，不用两个节点相连直接返回
    father[v] = u;
}

int main() {
    int s, t;
    cin >> n;
    init();
    for (int i = 0; i < n; i++) {
        cin >> s >> t;
        if (isSame(s, t)) {
            cout << s << " " << t << endl;
            return 0;
        } else {
            join(s, t);
        }
    }
}
```

可以看出，主函数的代码很少，就判断一下边的两个节点在不在同一个集合就可以了。

## 拓展 

题目要求 “请删除标准输入中最后出现的那条边” ，不少录友疑惑，这代码分明是遇到在同一个根的两个节点立刻就返回了，怎么就求出 最后出现的那条边 了呢。 

有这种疑惑的录友是 认为发现一条冗余边后，后面还可能会有一条冗余边。 

其实并不会。

题目是在 树的基础上 添加一条边，所以冗余边仅仅是一条。 

到这一条可能靠前出现，可能靠后出现。 

例如，题目输入示例： 

输入示例

```
3
1 2
2 3
1 3
```

图： 

![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240527110320.png)

输出示例

1 3

当我们从前向后遍历，优先让前面的边连上，最后判断冗余边就是 1 3。 

如果我们从后向前便利，优先让后面的边连上，最后判断的冗余边就是 1 2。 

题目要求“请删除标准输入中最后出现的那条边”，所以 1 3 这条边才是我们要求的。 




## 其他语言版本

### Java 

### Python

```python
father = list()

def find(u):
    if u == father[u]:
        return u
    else:
        father[u] = find(father[u])
        return father[u]
        
def is_same(u, v):
    u = find(u)
    v = find(v)
    return u == v
    
def join(u, v):
    u = find(u)
    v = find(v)
    if u != v:
        father[u] = v
        
if __name__ == "__main__":
    # 輸入
    n = int(input())
    for i in range(n + 1):
        father.append(i)
    # 尋找冗余邊    
    result = None
    for i in range(n):
        s, t = map(int, input().split())
        if is_same(s, t):
            result = str(s) + ' ' + str(t)
        else:
            join(s, t)
        
    # 輸出
    print(result)
```

### 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 = []  // 并查集


// 并查集初始化
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
}


(async function () {
  // 读取第一行输入
  let line = await readline();
  N = Number(line);

  // 初始化并查集
  father = new Array(N)
  init()

  // 读取边信息, 加入并查集
  for (let i = 0; i < N; i++) {
    line = await readline()
    line = line.split(' ').map(Number)

    if (!isSame(line[0], line[1])) {
      join(line[0], line[1])
    }else{
      console.log(line[0], line[1]);
      break
    }
  }
})()
```



### TypeScript

### PhP

### Swift

### Scala

### C#

### Dart

### C

