冗余连接
问题描述
在本问题中,树指的是一个连通且无环的无向图。
给定一个由 N 个节点(节点值从 1 到 N)组成的树,以及一条额外的边。结果图是连通的,但有且仅有一个环。
返回一条可以删除的边,使得结果图重新成为一棵树。如果有多个答案,则返回最后出现的那条边。
注意:边的表示形式为 [u, v],其中 u < v。
示例:
输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
解释: 给定的无向图如下:
1
/ \
2 - 3
删除边 [2,3] 后,图变成一棵树。
输入: [[1,2], [2,3], [3,1], [1,4]]
输出: [3,1]
解释: 给定的无向图如下:
1 - 4
/ \
2 - 3
删除边 [3,1] 后,图变成一棵树。
算法思路
并查集:
- 核心思想:树有 N 个节点和 N-1 条边,添加一条额外边后形成 N 条边和一个环
- 检测环:
- 遍历每条边,尝试将其两个端点连接
- 如果两个端点已经连通,说明添加这条边会形成环
- 这条边就是冗余边
操作:
find(x):查找 x 的根节点(带路径压缩优化)union(x, y):合并 x 和 y 所在的集合- 如果
find(x) == find(y),说明 x 和 y 已经连通
代码实现
方法一:并查集
class Solution {
/**
* 找到冗余连接,使得删除后图重新成为树
* 使用并查集检测环
*
* @param edges 无向图的边列表
* @return 冗余的边(最后出现的形成环的边)
*/
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length; // 节点数为边数(因为有冗余边)
// 初始化并查集,节点编号从1到n
UnionFind uf = new UnionFind(n + 1); // 索引0不使用
// 按顺序处理每条边
for (int[] edge : edges) {
int u = edge[0];
int v = edge[1];
// 如果两个节点已经连通,这条边就是冗余的
if (uf.find(u) == uf.find(v)) {
return edge;
}
// 否则合并两个节点
uf.union(u, v);
}
// 理论上不会执行到这里
return new int[0];
}
/**
* 并查集实现类
*/
static class UnionFind {
private int[] parent;
private int[] rank; // 用于按秩合并优化
public UnionFind(int size) {
parent = new int[size];
rank = new int[size];
// 初始化:每个节点的父节点是自己
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 0;
}
}
/**
* 查找x的根节点(带路径压缩)
*/
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
/**
* 合并x和y所在的集合(按秩合并)
*/
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
// 按秩合并:将较小秩的树连接到较大秩的树
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
}
}
方法二:简化并查集(无按秩合并)
class Solution {
/**
* 简化并查集,只使用路径压缩
*/
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
int[] parent = new int[n + 1];
// 初始化:每个节点的父节点是自己
for (int i = 1; i <= n; i++) {
parent[i] = i;
}
for (int[] edge : edges) {
int u = edge[0];
int v = edge[1];
int rootU = find(parent, u);
int rootV = find(parent, v);
if (rootU == rootV) {
return edge; // 找到冗余边
}
// 合并两个集合
parent[rootU] = rootV;
}
return new int[0];
}
/**
* 带路径压缩的查找操作
*/
private int find(int[] parent, int x) {
if (parent[x] != x) {
parent[x] = find(parent, parent[x]);
}
return parent[x];
}
}
方法三:DFS检测环
import java.util.*;
class Solution {
/**
* 使用DFS检测环的方法
* 时间复杂度较高,逻辑直观
*/
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
List<List<Integer>> graph = new ArrayList<>();
// 初始化邻接表
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<>());
}
// 逐条添加边,每次添加后检测是否形成环
for (int[] edge : edges) {
int u = edge[0];
int v = edge[1];
// 在添加当前边之前,检查u和v是否已经连通
if (hasPath(graph, u, v, new boolean[n + 1])) {
return edge; // 如果已经连通,这条边会形成环
}
// 添加当前边到图中
graph.get(u).add(v);
graph.get(v).add(u);
}
return new int[0];
}
/**
* 使用DFS检查从start到end是否存在路径
*/
private boolean hasPath(List<List<Integer>> graph, int start, int end, boolean[] visited) {
if (start == end) {
return true;
}
visited[start] = true;
for (int neighbor : graph.get(start)) {
if (!visited[neighbor] && hasPath(graph, neighbor, end, visited)) {
return true;
}
}
return false;
}
}
算法分析
- 时间复杂度:
- 并查集:O(N × α(N))
- DFS:O(N²),每条边都要做一次DFS
- 空间复杂度:
- 并查集:O(N),存储父节点数组
- DFS:O(N),存储邻接表和访问数组
算法过程
输入:edges = [[1,2], [1,3], [2,3]]
并查集:
- 初始化:
parent = [0,1,2,3] - 处理边[1,2]:
find(1)=1,find(2)=2→ 不同集合union(1,2)→parent = [0,1,1,3]
- 处理边[1,3]:
find(1)=1,find(3)=3→ 不同集合union(1,3)→parent = [0,1,1,1]
- 处理边[2,3]:
find(2)=1,find(3)=1→ 相同集合!- 返回[2,3]
路径压缩:
- 初始:1→2, 1→3
- 查询find(2)时:2→1(直接指向根)
- 查询find(3)时:3→1(直接指向根)
测试用例
public class TestFindRedundantConnection {
public static void printArray(int[] arr) {
System.out.print("[");
for (int i = 0; i < arr.length; i++) {
if (i > 0) System.out.print(", ");
System.out.print(arr[i]);
}
System.out.println("]");
}
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例1
int[][] edges1 = {{1,2}, {1,3}, {2,3}};
int[] result1 = solution.findRedundantConnection(edges1);
System.out.print("Test 1: ");
printArray(result1); // [2, 3]
// 测试用例2:标准示例2
int[][] edges2 = {{1,2}, {2,3}, {3,1}, {1,4}};
int[] result2 = solution.findRedundantConnection(edges2);
System.out.print("Test 2: ");
printArray(result2); // [3, 1]
// 测试用例3:更大的图
int[][] edges3 = {{1,2}, {1,3}, {1,4}, {2,5}, {2,6}, {3,7}, {3,8}, {4,9}, {4,10}, {5,6}};
int[] result3 = solution.findRedundantConnection(edges3);
System.out.print("Test 3: ");
printArray(result3); // [5, 6]
// 测试用例4:线性链 + 回边
int[][] edges4 = {{1,2}, {2,3}, {3,4}, {4,5}, {5,1}};
int[] result4 = solution.findRedundantConnection(edges4);
System.out.print("Test 4: ");
printArray(result4); // [5, 1]
// 测试用例5:星型结构
int[][] edges5 = {{1,2}, {1,3}, {1,4}, {1,5}, {2,3}};
int[] result5 = solution.findRedundantConnection(edges5);
System.out.print("Test 5: ");
printArray(result5); // [2, 3]
// 测试用例6:最小情况
int[][] edges6 = {{1,2}, {1,2}};
int[] result6 = solution.findRedundantConnection(edges6);
System.out.print("Test 6: ");
printArray(result6); // [1, 2]
// 测试用例7:复杂环
int[][] edges7 = {{1,3}, {3,4}, {4,2}, {2,1}, {1,5}};
int[] result7 = solution.findRedundantConnection(edges7);
System.out.print("Test 7: ");
printArray(result7); // [2, 1]
}
}
关键点
-
并查集:
- 高效检测两个节点是否已经连通
- 动态维护连通分量
- 路径压缩和按秩合并优化性能
-
为什么按输入顺序处理就能得到正确答案?:
- 要求返回"最后出现"的冗余边
- 输入数组已经按边的出现顺序排列
- 第一个形成环的边就是答案(因为之前的边都没形成环)
-
树:
- N个节点的树有N-1条边
- N个节点N条边必然有且仅有一个环
- 删除环中的任意一条边都能恢复树的结构
-
优化:
- 路径压缩:在find操作中将路径上的所有节点直接指向根
- 按秩合并:将较小的树合并到较大的树,保持树的平衡
常见问题
- 为什么不用检测所有可能的冗余边?
- 保证只有一个环,所以第一个检测到的冗余边就是答案
991

被折叠的 条评论
为什么被折叠?



