89. Redundant Connection
View on NeetCode
View on LeetCode
Problem
In this problem, a tree is an undirected graph that is connected and has no cycles.
You are given a graph that started as a tree with n nodes labeled from 1 to n, with one additional edge added. The added edge has two different vertices chosen from 1 to n, and was not an edge that already existed. The graph is represented as an array edges of length n where edges[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the graph.
Return an edge that can be removed so that the resulting graph is a tree of n nodes. If there are multiple answers, return the answer that occurs last in the input.
Example 1:
Input: edges = [[1,2],[1,3],[2,3]]
Output: [2,3]
Example 2:
Input: edges = [[1,2],[2,3],[3,4],[1,4],[1,5]]
Output: [1,4]
Constraints:
n == edges.length3 <= n <= 1000edges[i].length == 21 <= ai < bi <= edges.lengthai != bi- There are no repeated edges.
- The given graph is connected.
Solution
Approach 1: Union-Find (Disjoint Set Union)
The key insight is that the redundant edge connects two nodes that are already connected. We use Union-Find to track connected components.
Implementation
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
parent = list(range(n + 1)) # parent[i] = parent of node i
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # Path compression
return parent[x]
def union(x, y):
root_x = find(x)
root_y = find(y)
if root_x == root_y:
return False # Already in same set, edge is redundant
parent[root_x] = root_y
return True
for u, v in edges:
if not union(u, v):
return [u, v]
Approach 2: DFS Cycle Detection
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
graph = [[] for _ in range(n + 1)]
def has_cycle(u, v):
"""Check if there's already a path from u to v"""
visited = set()
def dfs(node, target):
if node == target:
return True
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
if dfs(neighbor, target):
return True
return False
return dfs(u, v)
for u, v in edges:
if has_cycle(u, v):
return [u, v]
# Add edge to graph
graph[u].append(v)
graph[v].append(u)
Complexity Analysis
Union-Find Approach:
- Time Complexity: O(n * α(n)), where α is the inverse Ackermann function (practically constant). With path compression, nearly O(n).
- Space Complexity: O(n) for the parent array.
DFS Approach:
- Time Complexity: O(n²), as we may perform DFS for each edge, and DFS is O(n).
- Space Complexity: O(n) for the graph and visited set.
Key Insights
-
Tree Property: A tree with n nodes has exactly n-1 edges. The nth edge creates a cycle.
-
Union-Find Efficiency: Union-Find is ideal for this problem as it efficiently tracks connected components and detects when we’re connecting already-connected nodes.
-
Path Compression: The
findfunction uses path compression to optimize future lookups. -
Last Occurrence: By processing edges in order and returning the first redundant edge found, we automatically return the last one in the input.
-
Alternative Detection: DFS can detect cycles by checking if a path already exists between nodes before adding an edge.
-
Undirected Graph: Since the graph is undirected, we add edges in both directions in the DFS approach.