LeetCode 684中等并查集
冗余连接 图解题解
这道题到底在问什么
有一棵 n 个节点(编号 1..n)的树,本该有 n-1 条边;现在多给了一条、一共 n 条边,使图里出现一个环。请返回这条多余的边(若有多解,返回输入中最后出现的那条)。
- 输入
- edges=[[1,2],[1,3],[2,4],[2,5],[3,6],[3,7],[4,8],[5,9],[6,10],[7,11],[5,2]]
- 输出
- [5,2]
最优解:一步一步想明白
- 3记住这一句,下面每条边都在套它。
- 4起手:11 个节点、11 条边。一棵 11 节点的树只该有 11-1=10 条边,这里多了一条——多出来的那条会让图里出现一个环,它就是要找的冗余边。下面逐条加边、揪出成环的那条。
- 5看边 [1,2]:1 的根是 1,2 的根是 2,根不同 → 这条边接通了两个原本分开的块,安全合并、不成环。
- 6合并:让根 2 指向根 1,两块并成一块(同色)。连通块数 -1 → 10。
- 7看边 [1,3]:1 的根是 1,3 的根是 3,根不同 → 这条边接通了两个原本分开的块,安全合并、不成环。
- 8合并:让根 3 指向根 1,两块并成一块(同色)。连通块数 -1 → 9。
- 9看边 [2,4]:2 的根是 1,4 的根是 4,根不同 → 这条边接通了两个原本分开的块,安全合并、不成环。
- 10合并:让根 4 指向根 1,两块并成一块(同色)。连通块数 -1 → 8。
- 11看边 [2,5]:2 的根是 1,5 的根是 5,根不同 → 这条边接通了两个原本分开的块,安全合并、不成环。
- 12合并:让根 5 指向根 1,两块并成一块(同色)。连通块数 -1 → 7。
- 13看边 [3,6]:3 的根是 1,6 的根是 6,根不同 → 这条边接通了两个原本分开的块,安全合并、不成环。
- 14合并:让根 6 指向根 1,两块并成一块(同色)。连通块数 -1 → 6。
- 15看边 [3,7]:3 的根是 1,7 的根是 7,根不同 → 这条边接通了两个原本分开的块,安全合并、不成环。
- 16合并:让根 7 指向根 1,两块并成一块(同色)。连通块数 -1 → 5。
- 17看边 [4,8]:4 的根是 1,8 的根是 8,根不同 → 这条边接通了两个原本分开的块,安全合并、不成环。
- 18合并:让根 8 指向根 1,两块并成一块(同色)。连通块数 -1 → 4。
- 19看边 [5,9]:5 的根是 1,9 的根是 9,根不同 → 这条边接通了两个原本分开的块,安全合并、不成环。
- 20合并:让根 9 指向根 1,两块并成一块(同色)。连通块数 -1 → 3。
- 21看边 [6,10]:6 的根是 1,10 的根是 10,根不同 → 这条边接通了两个原本分开的块,安全合并、不成环。
- 22合并:让根 10 指向根 1,两块并成一块(同色)。连通块数 -1 → 2。
- 23看边 [7,11]:7 的根是 1,11 的根是 11,根不同 → 这条边接通了两个原本分开的块,安全合并、不成环。
- 24合并:让根 11 指向根 1,两块并成一块(同色)。连通块数 -1 → 1。
- 25看边 [5,2]:查根发现 5、2 的根都是 1,它俩在之前的边里早就连通了——再连这条边就把已连通的两点圈成了环。
- 26两端已同根 → 加上这条边就成环!这条 [5,2] 就是多余的冗余边,正是答案。
- 27结论:逐条加边时,第一条「两端已经连通」的边 [5,2] 就是冗余边——删掉它,剩下的 10 条边正好连成一棵无环的树(全连成一块、同色)。
⚠️ 容易写错的地方
✗ 错:节点从 1 编号开错
✓ 对:parent 开 n+1、下标 1..n
题目节点是 1..n,开 n 会越界
✗ 错:同根还继续合并
✓ 对:同根即成环、立刻返回这条边
它就是要找的冗余边
✗ 错:不按输入顺序遍历
✓ 对:严格从前往后逐条加边、遇环即返回
多解时题目要返回输入中最后出现的边,顺序扫描遇到的成环边正是它
完整代码(Java / Python / C++)
Java
class Solution {
int[] parent;
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
parent = new int[n + 1];
for (int i = 1; i <= n; i++) parent[i] = i;
for (int[] e : edges) {
int ra = find(e[0]), rb = find(e[1]);
if (ra == rb) return e; // 两端已同根 -> 成环 -> 冗余边
parent[rb] = ra;
}
return new int[0];
}
int find(int x) {
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
return x;
}
}Python
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
parent = list(range(n + 1))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
for a, b in edges:
ra, rb = find(a), find(b)
if ra == rb:
return [a, b] # 两端已同根 -> 成环 -> 冗余边
parent[rb] = ra
return []C++
class Solution {
public:
vector<int> parent;
int find(int x) {
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
return x;
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
int n = edges.size();
parent.resize(n + 1);
for (int i = 1; i <= n; i++) parent[i] = i;
for (auto& e : edges) {
int ra = find(e[0]), rb = find(e[1]);
if (ra == rb) return e; // 两端已同根 -> 成环 -> 冗余边
parent[rb] = ra;
}
return {};
}
};复杂度
时间
O(n·α)
n 条边,每次 find 近乎常数(路径压缩)
空间
O(n)
一个 parent 数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 冗余连接 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么遇到第一条成环的边就能直接返回?+
n 节点的树加一条边只会形成恰好一个环;顺序扫边时,第一条使两端已同根的边,正是闭合这个环的那条多余边,所以可以立即返回。
不用并查集还能怎么做?+
DFS/BFS:对每条边试着加入并判断是否在已构建图里产生环(或最后做环检测)。但并查集天然按边流增量维护连通性,find 近乎 O(1),最简洁高效。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 冗余连接 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。