685. Redundant Connection II
Problem Description
You are given a directed graph that was originally a rooted tree with n
nodes (labeled from 1
to n
), but one additional directed edge has been added to it.
A rooted tree in this problem is defined as:
- A directed graph with exactly one root node
- All other nodes are descendants of the root
- Every node has exactly one parent (except the root which has no parent)
The input is provided as a 2D array edges
, where each element [ui, vi]
represents a directed edge from parent ui
to child vi
.
Your task is to find and return the one edge that can be removed so that the resulting graph becomes a valid rooted tree again. If there are multiple valid answers, return the edge that appears last in the input array.
For example, the added edge could create scenarios like:
- A node having two parents (in-degree of 2)
- A cycle in the graph
The challenge is to identify which specific edge, when removed, will restore the tree structure while maintaining all nodes connected with the proper parent-child relationships.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly deals with a directed graph (a rooted tree with an additional edge). We have nodes connected by directed edges.
Is it a tree?
- Yes/Partially: The original structure is a rooted tree, but with one extra edge added. We need to identify and remove that extra edge to restore the tree structure.
DFS
- Yes: Since we're dealing with a tree-like structure and need to explore parent-child relationships, detect cycles, and verify connectivity, DFS is applicable.
However, let's also consider the alternative path in the flowchart:
Is it a tree?
- No (considering the current state with the extra edge): Let's explore other options.
Is the problem related to directed acyclic graphs?
- No: With the extra edge, the graph might have a cycle, so it's not necessarily acyclic.
Is the problem related to shortest paths?
- No: We're not finding shortest paths; we're identifying a redundant edge.
Does the problem involve connectivity?
- Yes: We need to ensure all nodes remain connected after removing an edge, and we need to detect if nodes are already connected (forming a cycle).
Disjoint Set Union (DSU)
- Yes: DSU/Union-Find is perfect for detecting cycles and maintaining connectivity information efficiently.
Conclusion: The flowchart suggests using either DFS (through the tree path) or DSU (through the connectivity path) for this problem. The provided solution actually uses DSU (Union-Find) as the primary approach, which aligns with detecting cycles and maintaining connectivity in the graph. DFS could also be used to detect cycles and verify tree structure, making both approaches valid for solving this problem.
Intuition
When we add one extra edge to a valid rooted tree, we break the tree property in one of two ways:
- A node gets two parents (in-degree becomes 2)
- A cycle is formed in the graph
Let's think about what happens in each case:
Case 1: Node with two parents If a node has two parents, one of those parent-child edges must be the redundant one. But which one? Here's the key insight: if removing one of these edges still leaves a cycle in the graph, then that edge cannot be the answer - we must remove the other one.
Case 2: Only a cycle exists (no node with in-degree 2) This means the extra edge created a cycle without giving any node two parents. We need to find which edge in the cycle to remove. Since the problem asks for the edge that appears last in the input when multiple answers exist, we can use Union-Find to detect when we're about to create a cycle - that edge is our answer.
The Union-Find approach works perfectly here because:
- As we process edges one by one, we can detect the exact moment a cycle would be formed (when two nodes we're trying to connect already belong to the same component)
- It naturally gives us the "last occurring" edge that creates the problem
The algorithm strategy becomes:
- First, check if any node has in-degree 2
- If yes, identify the two edges pointing to that node. Try removing the second one - if the remaining graph is valid (no cycles), that's our answer. Otherwise, it's the first edge.
- If no node has in-degree 2, just find the edge that creates a cycle using Union-Find
This approach elegantly handles all three possible scenarios that can occur when adding an edge to a rooted tree.
Learn more about Depth-First Search, Breadth-First Search, Union Find and Graph patterns.
Solution Approach
The implementation follows our intuition by handling the two main scenarios: nodes with in-degree 2 and cycle detection.
Step 1: Calculate In-degrees First, we count the in-degree of each node by iterating through all edges:
ind = [0] * n for _, v in edges: ind[v - 1] += 1
Step 2: Find Edges Pointing to Node with In-degree 2 We identify all edges that point to nodes with in-degree 2. These edges are our candidates for removal:
dup = [i for i, (_, v) in enumerate(edges) if ind[v - 1] == 2]
The list dup
will contain the indices of the two edges if a node has two parents.
Step 3: Initialize Union-Find Structure We create a parent array for Union-Find, where each node initially points to itself:
p = list(range(n))
Step 4: Handle Case with In-degree 2
If we found a node with two parents (dup
is not empty):
- We skip the second edge (
dup[1]
) and try to build the graph with remaining edges - While building, we check for cycles using Union-Find
- If we find a cycle (two nodes already in same component), it means removing
dup[1]
doesn't fix the problem, sodup[0]
must be removed - If no cycle is found, then
dup[1]
is the redundant edge
if dup:
for i, (u, v) in enumerate(edges):
if i == dup[1]: # Skip the second edge
continue
pu, pv = find(u - 1), find(v - 1)
if pu == pv: # Cycle detected
return edges[dup[0]]
p[pu] = pv # Union operation
return edges[dup[1]]
Step 5: Handle Case with Only Cycle (No In-degree 2) If no node has in-degree 2, we simply find which edge creates a cycle:
for i, (u, v) in enumerate(edges):
pu, pv = find(u - 1), find(v - 1)
if pu == pv: # Found the edge creating cycle
return edges[i]
p[pu] = pv # Union operation
The Find Function The find function with path compression optimizes the Union-Find structure:
def find(x: int) -> int:
if p[x] != x:
p[x] = find(p[x]) # Path compression
return p[x]
This approach efficiently handles all scenarios in O(n)
time complexity, where n
is the number of edges, making it optimal for this problem.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how the solution works.
Example 1: Node with Two Parents
Consider the graph with edges: [[1,2], [1,3], [2,3]]
Initial tree should be: 1 / \ 2 3 But with the extra edge [2,3], node 3 has two parents: 1 / \ 2ββ3
Step 1: Calculate in-degrees
- Node 1: in-degree = 0
- Node 2: in-degree = 1 (from edge [1,2])
- Node 3: in-degree = 2 (from edges [1,3] and [2,3])
Step 2: Find candidate edges Node 3 has in-degree 2, so we identify the two edges pointing to it:
dup[0] = 1
(edge [1,3] at index 1)dup[1] = 2
(edge [2,3] at index 2)
Step 3: Try removing the second edge [2,3] Build the graph without edge at index 2:
- Process edge [1,2]: Union nodes 1 and 2 (no cycle)
- Process edge [1,3]: Union nodes 1 and 3 (no cycle)
- Result: Valid tree formed!
Since no cycle was detected, the answer is edge [2,3].
Example 2: Cycle Without Two Parents
Consider the graph with edges: [[1,2], [2,3], [3,1]]
This creates a cycle: 1 β 2 β β 3 β
Step 1: Calculate in-degrees
- All nodes have in-degree = 1
- No node has two parents
Step 2: No edges in dup
list
Step 3: Detect cycle using Union-Find
- Process edge [1,2]: Union nodes 1 and 2 (parent[1] = 2)
- Process edge [2,3]: Union nodes 2 and 3 (parent[2] = 3)
- Process edge [3,1]:
- Find(3) = 3, Find(1) = 3 (following parent pointers: 1β2β3)
- They're already in the same component! Cycle detected.
The answer is edge [3,1] - the edge that would create the cycle.
Example 3: Two Parents with Hidden Cycle
Consider edges: [[2,1], [3,1], [3,2], [1,4]]
Should form: But we have: 3 3 | / \ 2 2 1 | | 1 4 | 4
Step 1: In-degrees
- Node 1: in-degree = 2 (from [2,1] and [3,1])
Step 2: Candidate edges
dup[0] = 0
(edge [2,1])dup[1] = 1
(edge [3,1])
Step 3: Try removing [3,1] Build graph without edge at index 1:
- Process [2,1]: Union 2 and 1
- Process [3,2]: Union 3 and 2
- Process [1,4]: Check Find(1) and Find(4)
- Find(1) = 3 (path: 1β2β3)
- Find(4) = 4
- Different components, so union them
Wait, let's check if there's a cycle by looking at the structure:
- After [2,1]: 2β1
- After [3,2]: 3β2β1
- After [1,4]: 3β2β1β4
This forms a valid tree! So the answer is [3,1].
These examples demonstrate how the algorithm handles different scenarios: simple two-parent cases, pure cycles, and complex situations where both conditions exist.
Solution Implementation
1class Solution:
2 def findRedundantDirectedConnection(self, edges: List[List[int]]) -> List[int]:
3 """
4 Find the redundant edge in a directed graph that should be removed.
5 The graph should form a rooted tree after removal.
6
7 Args:
8 edges: List of directed edges [parent, child]
9
10 Returns:
11 The redundant edge to remove
12 """
13
14 def find_root(node: int) -> int:
15 """Find the root of the node using path compression."""
16 if parent[node] != node:
17 parent[node] = find_root(parent[node]) # Path compression
18 return parent[node]
19
20 n = len(edges)
21
22 # Count in-degrees for each node (to find nodes with 2 parents)
23 in_degree = [0] * n
24 for parent_node, child_node in edges:
25 in_degree[child_node - 1] += 1
26
27 # Find edges where the child has 2 parents (in-degree = 2)
28 duplicate_parent_edges = [
29 i for i, (parent_node, child_node) in enumerate(edges)
30 if in_degree[child_node - 1] == 2
31 ]
32
33 # Initialize Union-Find parent array
34 parent = list(range(n))
35
36 if duplicate_parent_edges:
37 # Case 1: A node has two parents
38 # Try removing the second edge first
39 for i, (u, v) in enumerate(edges):
40 if i == duplicate_parent_edges[1]:
41 continue # Skip the second duplicate edge
42
43 root_u, root_v = find_root(u - 1), find_root(v - 1)
44
45 if root_u == root_v:
46 # Found a cycle without the second edge
47 # So the first duplicate edge must be removed
48 return edges[duplicate_parent_edges[0]]
49
50 # Union the components
51 parent[root_u] = root_v
52
53 # No cycle found after removing second edge
54 # So the second edge should be removed
55 return edges[duplicate_parent_edges[1]]
56
57 # Case 2: No node has two parents, just find the cycle
58 for i, (u, v) in enumerate(edges):
59 root_u, root_v = find_root(u - 1), find_root(v - 1)
60
61 if root_u == root_v:
62 # Found the edge that creates a cycle
63 return edges[i]
64
65 # Union the components
66 parent[root_u] = root_v
67
68 # This line should never be reached given valid input
69 return []
70
1class Solution {
2 // Parent array for Union-Find data structure
3 private int[] parent;
4
5 public int[] findRedundantDirectedConnection(int[][] edges) {
6 int n = edges.length;
7
8 // Count in-degree for each node (nodes are 1-indexed, array is 0-indexed)
9 int[] inDegree = new int[n];
10 for (int[] edge : edges) {
11 inDegree[edge[1] - 1]++;
12 }
13
14 // Find edges pointing to nodes with in-degree of 2 (two parents)
15 List<Integer> candidateEdges = new ArrayList<>();
16 parent = new int[n];
17 for (int i = 0; i < n; i++) {
18 if (inDegree[edges[i][1] - 1] == 2) {
19 candidateEdges.add(i);
20 }
21 parent[i] = i; // Initialize Union-Find
22 }
23
24 // Case 1: There exists a node with two parents
25 if (!candidateEdges.isEmpty()) {
26 // Try building the tree without the second candidate edge
27 for (int i = 0; i < n; i++) {
28 if (i == candidateEdges.get(1)) {
29 continue; // Skip the second candidate edge
30 }
31
32 int parentU = find(edges[i][0] - 1);
33 int parentV = find(edges[i][1] - 1);
34
35 // If adding this edge creates a cycle
36 if (parentU == parentV) {
37 // The first candidate edge must be removed
38 return edges[candidateEdges.get(0)];
39 }
40
41 // Union the two components
42 parent[parentU] = parentV;
43 }
44 // If no cycle found, the second candidate edge should be removed
45 return edges[candidateEdges.get(1)];
46 }
47
48 // Case 2: No node has two parents, find the edge that creates a cycle
49 for (int i = 0; ; i++) {
50 int parentU = find(edges[i][0] - 1);
51 int parentV = find(edges[i][1] - 1);
52
53 // If both nodes already belong to the same component
54 if (parentU == parentV) {
55 // This edge creates a cycle
56 return edges[i];
57 }
58
59 // Union the two components
60 parent[parentU] = parentV;
61 }
62 }
63
64 // Find operation with path compression for Union-Find
65 private int find(int x) {
66 if (parent[x] != x) {
67 parent[x] = find(parent[x]); // Path compression
68 }
69 return parent[x];
70 }
71}
72
1class Solution {
2public:
3 vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
4 int n = edges.size();
5
6 // Count in-degree for each node (nodes are 1-indexed, stored as 0-indexed)
7 vector<int> inDegree(n);
8 for (const auto& edge : edges) {
9 ++inDegree[edge[1] - 1];
10 }
11
12 // Find edges pointing to nodes with in-degree of 2 (duplicate parent case)
13 vector<int> duplicateEdges;
14 for (int i = 0; i < n; ++i) {
15 if (inDegree[edges[i][1] - 1] == 2) {
16 duplicateEdges.push_back(i);
17 }
18 }
19
20 // Initialize Union-Find parent array
21 vector<int> parent(n);
22 iota(parent.begin(), parent.end(), 0);
23
24 // Union-Find find operation with path compression
25 function<int(int)> find = [&](int x) {
26 return x == parent[x] ? x : parent[x] = find(parent[x]);
27 };
28
29 // Case 1: There exists a node with two parents
30 if (!duplicateEdges.empty()) {
31 // Try building the tree without the second duplicate edge
32 for (int i = 0; i < n; ++i) {
33 // Skip the second edge that creates duplicate parent
34 if (i == duplicateEdges[1]) {
35 continue;
36 }
37
38 int parentU = find(edges[i][0] - 1);
39 int parentV = find(edges[i][1] - 1);
40
41 // If cycle detected, the first duplicate edge is redundant
42 if (parentU == parentV) {
43 return edges[duplicateEdges[0]];
44 }
45
46 // Union the two components
47 parent[parentU] = parentV;
48 }
49
50 // If no cycle found, the second duplicate edge is redundant
51 return edges[duplicateEdges[1]];
52 }
53
54 // Case 2: No node has two parents, find the edge creating a cycle
55 for (int i = 0;; ++i) {
56 int parentU = find(edges[i][0] - 1);
57 int parentV = find(edges[i][1] - 1);
58
59 // Found the edge that creates a cycle
60 if (parentU == parentV) {
61 return edges[i];
62 }
63
64 // Union the two components
65 parent[parentU] = parentV;
66 }
67 }
68};
69
1/**
2 * Finds the redundant directed edge that should be removed to form a valid tree
3 * @param edges - Array of directed edges where each edge is [parent, child]
4 * @returns The redundant edge that should be removed
5 */
6function findRedundantDirectedConnection(edges: number[][]): number[] {
7 const n: number = edges.length;
8
9 // Count in-degree for each node (how many edges point to each node)
10 const inDegree: number[] = Array(n).fill(0);
11 for (const [_, child] of edges) {
12 inDegree[child - 1]++;
13 }
14
15 // Find edges pointing to nodes with in-degree of 2 (duplicate parent case)
16 const duplicateParentEdges: number[] = [];
17 for (let i = 0; i < n; i++) {
18 if (inDegree[edges[i][1] - 1] === 2) {
19 duplicateParentEdges.push(i);
20 }
21 }
22
23 // Initialize parent array for Union-Find (Disjoint Set Union)
24 const parent: number[] = Array.from({ length: n }, (_, index) => index);
25
26 /**
27 * Find operation with path compression for Union-Find
28 * @param x - Node to find root for
29 * @returns Root of the set containing x
30 */
31 const find = (x: number): number => {
32 if (parent[x] !== x) {
33 parent[x] = find(parent[x]); // Path compression
34 }
35 return parent[x];
36 };
37
38 // Case 1: Node has two parents (in-degree = 2)
39 if (duplicateParentEdges.length > 0) {
40 // Try building tree without the second duplicate edge
41 for (let i = 0; i < n; i++) {
42 if (i === duplicateParentEdges[1]) {
43 continue; // Skip the second edge pointing to the duplicate node
44 }
45
46 const parentU: number = find(edges[i][0] - 1);
47 const parentV: number = find(edges[i][1] - 1);
48
49 // If adding this edge creates a cycle, the first duplicate edge is redundant
50 if (parentU === parentV) {
51 return edges[duplicateParentEdges[0]];
52 }
53
54 // Union operation: merge the sets
55 parent[parentU] = parentV;
56 }
57
58 // If no cycle found, the second duplicate edge is redundant
59 return edges[duplicateParentEdges[1]];
60 }
61
62 // Case 2: No node has two parents, find the edge that creates a cycle
63 for (let i = 0; ; i++) {
64 const parentU: number = find(edges[i][0] - 1);
65 const parentV: number = find(edges[i][1] - 1);
66
67 // If both nodes are already in the same set, this edge creates a cycle
68 if (parentU === parentV) {
69 return edges[i];
70 }
71
72 // Union operation: merge the sets
73 parent[parentU] = parentV;
74 }
75}
76
Time and Space Complexity
The time complexity is O(n * Ξ±(n))
where Ξ±(n)
is the inverse Ackermann function, which is practically O(n)
for all reasonable values of n
. With path compression in the find
function, each find operation takes nearly constant time amortized. Since we iterate through the edges at most twice (once to find duplicates and once to build the union-find structure), and perform at most 2n
find operations, the overall time complexity is effectively O(n)
.
However, the reference answer states O(n log n)
, which represents a more conservative upper bound for the union-find operations without considering the optimization from path compression. Without path compression optimization being fully amortized, each find
operation could take O(log n)
time in a balanced tree structure, leading to O(n log n)
total complexity.
The space complexity is O(n)
, where n
is the number of edges. The algorithm uses:
- Array
ind
of sizen
to track in-degrees - Array
dup
which stores at most 2 indices (constant space) - Array
p
of sizen
for the parent pointers in union-find - The recursive call stack for
find
which is at mostO(log n)
depth with path compression
Therefore, the total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrectly Handling the Two-Parent Case Priority
Pitfall: When a node has two parents, many solutions incorrectly assume that simply removing either edge will work. The critical mistake is not understanding that if removing one edge still leaves a cycle, then the other edge must be the one to remove.
Example scenario:
Edges: [[1,2], [1,3], [2,3]] Node 3 has two parents (1 and 2)
If we remove edge [1,3], we still have 1β2β3 which forms a valid tree. But if we remove edge [2,3], we have 1β2 and 1β3, which creates a disconnected graph with node 2 having no children.
Solution: Always test by removing the second (later) edge first. Only if this creates a cycle should we conclude the first edge needs removal:
# Correct approach - try removing dup[1] first
if duplicate_parent_edges:
for i, (u, v) in enumerate(edges):
if i == duplicate_parent_edges[1]: # Skip second edge
continue
root_u, root_v = find_root(u - 1), find_root(v - 1)
if root_u == root_v: # Cycle found
return edges[duplicate_parent_edges[0]] # Remove first edge instead
return edges[duplicate_parent_edges[1]] # No cycle, remove second edge
2. Off-by-One Errors with Node Indexing
Pitfall: The problem states nodes are labeled from 1 to n, but arrays in most programming languages are 0-indexed. Forgetting to adjust indices leads to incorrect parent tracking or array out-of-bounds errors.
Incorrect code:
# Wrong - treating 1-indexed nodes as 0-indexed in_degree[child_node] += 1 # Should be child_node - 1 root_u = find_root(u) # Should be u - 1
Solution: Consistently convert between 1-indexed nodes and 0-indexed arrays:
# Correct indexing in_degree[child_node - 1] += 1 root_u, root_v = find_root(u - 1), find_root(v - 1)
3. Not Using Path Compression in Union-Find
Pitfall: Implementing find() without path compression can lead to O(n) time complexity per find operation in worst case, making the overall solution O(nΒ²) instead of O(n).
Inefficient implementation:
def find_root(node):
while parent[node] != node:
node = parent[node]
return node
Solution: Always implement path compression to maintain near-constant time complexity:
def find_root(node: int) -> int:
if parent[node] != node:
parent[node] = find_root(parent[node]) # Path compression
return parent[node]
4. Returning Wrong Edge Format
Pitfall: Returning the edge index instead of the actual edge, or modifying the edge before returning it.
Wrong:
return duplicate_parent_edges[0] # Returns index, not edge return [edges[i][0] - 1, edges[i][1] - 1] # Modifies values
Solution: Always return the original edge from the input array:
return edges[duplicate_parent_edges[0]] # Correct - returns actual edge
In a binary min heap, the maximum element can be found in:
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!