LeetCode 685 - Redundant Connection II
The problem asks us to identify a redundant edge in a directed graph that originated as a rooted tree. A rooted tree has a single root with all other nodes having exactly one parent.
Difficulty: 🔴 Hard
Topics: Depth-First Search, Breadth-First Search, Union-Find, Graph Theory
Solution
Problem Understanding
The problem asks us to identify a redundant edge in a directed graph that originated as a rooted tree. A rooted tree has a single root with all other nodes having exactly one parent. The graph given is initially a tree with n nodes labeled 1 to n, with exactly one extra directed edge added. This extra edge may create either a cycle, a node with two parents, or both.
The input edges is a list of pairs [ui, vi], where ui is the parent of vi. We need to return one edge to remove such that the resulting graph becomes a valid rooted tree again. If multiple edges can be removed, we must return the one that appears last in the input.
Constraints inform us that 3 <= n <= 1000, so algorithms with time complexity O(n²) may work but are not ideal for large n. Each node label is unique and positive, and no self-loops exist. The problem guarantees exactly one extra edge, which simplifies the cases we need to consider.
Key edge cases include:
- A node having two parents. We must choose which edge to remove to restore the tree property.
- A cycle in the graph. Removing the edge that closes the cycle restores the acyclic property.
- A combination of both: a node has two parents, and one of the edges creates a cycle.
Approaches
Brute Force Approach
A naive approach would iterate through each edge and temporarily remove it, then check if the resulting graph is a rooted tree. To check if the graph is a rooted tree, we can use a DFS or BFS to ensure there are no cycles and each node has exactly one parent. While this approach is correct, it requires checking the tree property for every edge, resulting in O(n²) time complexity. This is inefficient for the upper bound of n = 1000.
Optimal Approach
The optimal approach leverages Union-Find (Disjoint Set Union) combined with parent tracking. The key insight is that the redundant edge can only fall into one of three categories:
- Node with two parents, no cycle.
- Node with two parents, cycle present.
- Cycle, no node with two parents.
First, we scan the edges to identify if a node has two parents. If it does, we temporarily ignore the second edge and check for cycles using Union-Find. If a cycle exists, the redundant edge is the first parent edge; otherwise, it is the second edge. If no node has two parents, we simply use Union-Find to detect the cycle and return the edge that completes it.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Remove each edge and check if the graph is a valid tree |
| Optimal | O(n) | O(n) | Use parent array and Union-Find to efficiently detect two parents and cycles |
Algorithm Walkthrough
- Initialize a
parentarray of sizen+1to keep track of each node’s parent. - Initialize two variables
cand1andcand2to store candidate edges for a node with two parents. - Iterate through each edge
[u, v]. Ifvalready has a parent, store[existing_parent, v]ascand1and[u, v]ascand2, then temporarily ignorecand2. - Use Union-Find to iterate through the edges:
a. Skip cand2 if it exists.
b. For each edge [u, v], union u and v.
c. If u and v are already connected, a cycle is detected.
5. Determine which edge to remove:
a. If there is no two-parent node, return the edge that creates the cycle.
b. If a two-parent node exists and cycle is detected, return cand1.
c. If a two-parent node exists but no cycle, return cand2.
Why it works: By tracking parent relationships and detecting cycles simultaneously, we handle all scenarios of the extra edge: nodes with two parents, cycles, or both. This ensures the returned edge is exactly the one causing the violation of the rooted tree property.
Python Solution
from typing import List
class Solution:
def findRedundantDirectedConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
parent = [0] * (n + 1)
cand1 = cand2 = None
# Step 1: Check for a node with two parents
for u, v in edges:
if parent[v] == 0:
parent[v] = u
else:
cand1 = [parent[v], v]
cand2 = [u, v]
# Temporarily remove the second edge
break
# Union-Find setup
def find(x):
if uf[x] != x:
uf[x] = find(uf[x])
return uf[x]
uf = list(range(n + 1))
# Step 2: Detect cycle
for u, v in edges:
if [u, v] == cand2:
continue
pu, pv = find(u), find(v)
if pu == pv:
if cand1:
return cand1
else:
return [u, v]
uf[pv] = pu
# Step 3: If no cycle, remove the second parent edge
return cand2
Explanation: The first loop identifies a node with two parents. The Union-Find logic then detects cycles efficiently, skipping the second candidate if necessary. Depending on the presence of a cycle and two-parent node, we return the correct edge.
Go Solution
func findRedundantDirectedConnection(edges [][]int) []int {
n := len(edges)
parent := make([]int, n+1)
var cand1, cand2 []int
for _, e := range edges {
u, v := e[0], e[1]
if parent[v] == 0 {
parent[v] = u
} else {
cand1 = []int{parent[v], v}
cand2 = []int{u, v}
break
}
}
uf := make([]int, n+1)
for i := 0; i <= n; i++ {
uf[i] = i
}
var find func(int) int
find = func(x int) int {
if uf[x] != x {
uf[x] = find(uf[x])
}
return uf[x]
}
for _, e := range edges {
if cand2 != nil && e[0] == cand2[0] && e[1] == cand2[1] {
continue
}
u, v := e[0], e[1]
pu, pv := find(u), find(v)
if pu == pv {
if cand1 != nil {
return cand1
}
return e
}
uf[pv] = pu
}
return cand2
}
Go-specific notes: We use slices instead of lists, nil checks for candidates, and functions are closures to handle recursive find.
Worked Examples
Example 1: [[1,2],[1,3],[2,3]]
- Node 3 has two parents: cand1 = [1,3], cand2 = [2,3].
- Union-Find:
- [1,2]: union success
- [1,3]: union success
- Skip cand2 ([2,3])
- No cycle detected. Remove cand2 = [2,3].
Example 2: [[1,2],[2,3],[3,4],[4,1],[1,5]]
- No node with two parents.
- Union-Find:
- [1,2], [2,3], [3,4]: union success
- [4,1]: cycle detected. Return [4,1]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Single pass to detect two parents + Union-Find with path compression |
| Space | O(n) | Parent array and Union-Find array of size n+1 |
The Union-Find operations are near constant time due to path compression, ensuring linear time overall.
Test Cases
# Provided examples
assert Solution().findRedundantDirectedConnection([[1,2],[1,3],[2,3]]) == [2,3] # node with two parents
assert Solution().findRedundantDirectedConnection([[1,2],[2,3],[3,4],[4,1],[1,5]]) == [4,1] # cycle
# Edge cases
assert Solution().findRedundantDirectedConnection([[2,1],[3,1],[4,2],[1,4]]) == [2,1] # two parents + cycle
assert Solution().findRedundantDirectedConnection([[1,2],[2,3],[3,1]]) == [3,1] # simple cycle
assert Solution().findRedundantDirectedConnection([[1,2],[2,3],[3,4],[4,5],[5,3]]) == [5,3] # cycle at the end
| Test | Why |
|---|---|
| [[1 |