684. Redundant Connection
Problem Description
You are given a graph that was originally a tree with n
nodes (labeled from 1
to n
), but one extra edge has been added to it. This additional edge connects two different nodes that weren't already connected, creating exactly one cycle in the graph.
The graph is represented as an array edges
where each element edges[i] = [ai, bi]
indicates an undirected edge between nodes ai
and bi
.
Your task is to find and return one edge that can be removed to restore the graph back to a valid tree (a connected graph with no cycles). If there are multiple valid edges that could be removed, return the one that appears last in the input array.
For example, if you have nodes 1, 2, 3
forming a tree and then an edge is added between nodes 1
and 3
(creating a cycle), you need to identify which edge to remove to break the cycle and restore the tree structure.
The solution uses a Union-Find (Disjoint Set Union) data structure to detect the redundant edge. As we process each edge, we check if the two nodes it connects are already in the same connected component. If they are, adding this edge would create a cycle, so this is the redundant edge we need to return. If they aren't in the same component, we unite them. The first edge that would create a cycle when added is our answer.
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 an undirected graph that started as a tree with an additional edge added, creating a cycle.
Is it a tree?
- No: While the graph started as a tree, the addition of one extra edge means it's no longer a tree (it now contains exactly one cycle).
Is the problem related to directed acyclic graphs?
- No: The graph is undirected, and it actually contains a cycle (not acyclic).
Is the problem related to shortest paths?
- No: We're not finding shortest paths; we're identifying a redundant edge that creates a cycle.
Does the problem involve connectivity?
- Yes: The core of the problem is about connectivity - we need to identify when adding an edge would create a cycle (meaning the nodes are already connected through another path).
Disjoint Set Union
- Yes: This is the ideal approach. We can use DSU (Union-Find) to track connected components and detect when an edge would connect two nodes that are already in the same component.
Conclusion: The flowchart leads us to use Disjoint Set Union (DSU/Union-Find) to solve this problem. While DFS could also detect cycles, DSU is more efficient for this specific task of finding the redundant edge. As we process each edge, we check if it connects nodes already in the same component - if yes, that's our redundant edge.
Note: Although the problem can be solved with DFS by detecting cycles, the flowchart correctly guides us to DSU as it's a connectivity problem where we need to efficiently track and merge connected components.
Intuition
Think about what makes a tree special - it's a connected graph with exactly n-1
edges for n
nodes, and there's exactly one path between any two nodes. When we add one extra edge to a tree, we create exactly one cycle because we're connecting two nodes that already have a path between them.
The key insight is that the redundant edge is the one that "completes" a cycle. As we build the graph edge by edge, everything is fine until we try to add an edge between two nodes that are already connected through some other path. That's the moment we create a cycle, and that edge is redundant.
To detect this, we can use the Union-Find (Disjoint Set Union) data structure. Initially, each node is in its own separate component. As we process each edge:
- If the two nodes are in different components, this edge is necessary to connect them, so we unite their components
- If the two nodes are already in the same component, adding this edge would create a cycle - this is our redundant edge!
Why does this work? Because in a tree, there should be exactly one way to reach any node from any other node. The moment we find an edge trying to connect two nodes that are already reachable from each other, we've found the edge that creates the alternative path (the cycle).
The beauty of this approach is that we process edges in order, so if multiple edges could be removed to eliminate the cycle, we naturally return the one that appears last in the input (the first one we encounter that would create a cycle).
Learn more about Depth-First Search, Breadth-First Search, Union Find and Graph patterns.
Solution Approach
The solution implements the Union-Find (Disjoint Set Union) data structure to detect the redundant edge. Here's how the implementation works:
1. Initialize the Parent Array
p = list(range(len(edges)))
We create a parent array p
where initially each node is its own parent (representing separate components). The array is sized to len(edges)
which works because we have n
nodes and n
edges.
2. Find Function with Path Compression
def find(x: int) -> int:
if p[x] != x:
p[x] = find(p[x])
return p[x]
This recursive function finds the root parent of a node. The key optimization here is path compression - when we find the root, we update p[x]
to point directly to it, flattening the tree structure for faster future lookups.
3. Process Each Edge
for a, b in edges: pa, pb = find(a - 1), find(b - 1)
For each edge [a, b]
, we find the root parents of both nodes. Note that we use a - 1
and b - 1
because the nodes are labeled from 1
to n
, but our array is 0-indexed.
4. Check for Cycle
if pa == pb: return [a, b]
If both nodes have the same root parent, they're already in the same connected component. Adding this edge would create a cycle, so this is our redundant edge - we return it immediately.
5. Union Operation
p[pa] = pb
If the nodes are in different components, we unite them by making one root the parent of the other. This merges the two components into one.
The algorithm processes edges in the order they appear in the input. The first edge that would create a cycle (connecting two already-connected nodes) is the answer. Since we process edges sequentially and return immediately upon finding the redundant edge, we naturally return the last valid answer if multiple edges could be removed.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the Union-Find approach detects the redundant edge.
Example Input: edges = [[1,2], [1,3], [2,3]]
This represents a triangle where all three nodes are connected to each other. The redundant edge creates a cycle.
Initial State:
- Parent array
p = [0, 1, 2]
(each node is its own parent) - Nodes 1, 2, 3 are in separate components
Step 1: Process edge [1,2]
- Find parents:
find(0) = 0
,find(1) = 1
- Different parents β No cycle yet
- Union: Set
p[0] = 1
- Components: {1,2} and {3}
Step 2: Process edge [1,3]
- Find parents:
find(0) = 1
(follows 0β1),find(2) = 2
- Different parents β No cycle yet
- Union: Set
p[1] = 2
- Components: {1,2,3} all connected
Step 3: Process edge [2,3]
- Find parents:
find(1) = 2
(follows 1β2),find(2) = 2
- Same parent (2) β Cycle detected!
- Return
[2,3]
as the redundant edge
The edge [2,3] is redundant because nodes 2 and 3 were already connected through node 1. Adding this edge creates a cycle: 1β2β3β1.
Another Example: edges = [[1,2], [2,3], [3,4], [1,4], [1,5]]
Processing edges in order:
- [1,2]: Connect 1-2 (no cycle)
- [2,3]: Connect 2-3 to existing component (no cycle)
- [3,4]: Connect 3-4 to existing component (no cycle)
- [1,4]: Nodes 1 and 4 already connected via 1β2β3β4 β Cycle!
- Return [1,4] without processing [1,5]
The algorithm correctly identifies [1,4] as the edge that completes the cycle in the path 1β2β3β4β1.
Solution Implementation
1class Solution:
2 def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
3 """
4 Find the redundant edge in an undirected graph that started as a tree.
5 Uses Union-Find (Disjoint Set Union) data structure to detect cycles.
6
7 Args:
8 edges: List of edges where each edge is [node1, node2]
9
10 Returns:
11 The redundant edge that creates a cycle
12 """
13
14 def find(node: int) -> int:
15 """
16 Find the root parent of a node with path compression.
17
18 Args:
19 node: The node to find the root parent for
20
21 Returns:
22 The root parent of the node
23 """
24 if parent[node] != node:
25 # Path compression: make every node point directly to root
26 parent[node] = find(parent[node])
27 return parent[node]
28
29 # Initialize parent array where each node is its own parent
30 # Size is len(edges) since nodes are numbered from 1 to n
31 parent = list(range(len(edges)))
32
33 # Process each edge
34 for node1, node2 in edges:
35 # Convert to 0-indexed (nodes are 1-indexed in the problem)
36 parent_1 = find(node1 - 1)
37 parent_2 = find(node2 - 1)
38
39 # If both nodes have the same root parent, adding this edge creates a cycle
40 if parent_1 == parent_2:
41 return [node1, node2]
42
43 # Union: merge the two sets by making one root the parent of the other
44 parent[parent_1] = parent_2
45
46 # This line should never be reached given problem constraints
47 return []
48
1class Solution {
2 // Parent array for Union-Find data structure
3 private int[] parent;
4
5 /**
6 * Finds the redundant edge that forms a cycle in the graph.
7 * Uses Union-Find (Disjoint Set Union) to detect cycles.
8 *
9 * @param edges Array of edges where each edge connects two nodes
10 * @return The redundant edge that creates a cycle
11 */
12 public int[] findRedundantConnection(int[][] edges) {
13 int n = edges.length;
14
15 // Initialize parent array where each node is its own parent initially
16 parent = new int[n];
17 for (int i = 0; i < n; i++) {
18 parent[i] = i;
19 }
20
21 // Process each edge in order
22 for (int i = 0; ; i++) {
23 // Find the root parents of both nodes (converting 1-indexed to 0-indexed)
24 int rootA = find(edges[i][0] - 1);
25 int rootB = find(edges[i][1] - 1);
26
27 // If both nodes have the same root, adding this edge creates a cycle
28 if (rootA == rootB) {
29 return edges[i];
30 }
31
32 // Union: Connect the two components by making one root the parent of the other
33 parent[rootA] = rootB;
34 }
35 }
36
37 /**
38 * Finds the root parent of a node using path compression.
39 * Path compression optimizes future queries by directly connecting nodes to their root.
40 *
41 * @param x The node to find the root parent for
42 * @return The root parent of node x
43 */
44 private int find(int x) {
45 // If x is not its own parent, recursively find the root and compress the path
46 if (parent[x] != x) {
47 parent[x] = find(parent[x]);
48 }
49 return parent[x];
50 }
51}
52
1class Solution {
2public:
3 vector<int> findRedundantConnection(vector<vector<int>>& edges) {
4 // Get the number of nodes (equal to number of edges in this problem)
5 int n = edges.size();
6
7 // Initialize parent array for Union-Find
8 // Each node is initially its own parent
9 vector<int> parent(n);
10 iota(parent.begin(), parent.end(), 0);
11
12 // Find function with path compression
13 // Returns the root parent of node x
14 function<int(int)> find = [&](int x) {
15 if (x == parent[x]) {
16 return x;
17 }
18 // Path compression: make all nodes on path point directly to root
19 return parent[x] = find(parent[x]);
20 };
21
22 // Process each edge in order
23 for (int i = 0; i < edges.size(); ++i) {
24 // Convert to 0-indexed (edges are 1-indexed in the problem)
25 int node1 = edges[i][0] - 1;
26 int node2 = edges[i][1] - 1;
27
28 // Find root parents of both nodes
29 int root1 = find(node1);
30 int root2 = find(node2);
31
32 // If both nodes have the same root, adding this edge creates a cycle
33 if (root1 == root2) {
34 return edges[i];
35 }
36
37 // Union: merge the two components by making one root parent of the other
38 parent[root1] = root2;
39 }
40
41 // This should never be reached given problem constraints
42 return {};
43 }
44};
45
1/**
2 * Finds the redundant connection in an undirected graph that forms a cycle.
3 * Uses Union-Find (Disjoint Set Union) data structure to detect cycles.
4 *
5 * @param edges - Array of edges where each edge is [node1, node2] with 1-indexed nodes
6 * @returns The redundant edge that creates a cycle when added
7 */
8function findRedundantConnection(edges: number[][]): number[] {
9 // Number of nodes equals number of edges (since we have exactly one extra edge)
10 const nodeCount: number = edges.length;
11
12 // Parent array for Union-Find, initialized with each node as its own parent
13 // Using 0-indexed internally while edges use 1-indexed nodes
14 const parent: number[] = Array.from({ length: nodeCount }, (_, index) => index);
15
16 /**
17 * Find operation with path compression optimization.
18 * Finds the root parent of a node and compresses the path for efficiency.
19 *
20 * @param node - The node to find the root parent for
21 * @returns The root parent of the node
22 */
23 const find = (node: number): number => {
24 // If node is not its own parent, recursively find root and compress path
25 if (parent[node] !== node) {
26 parent[node] = find(parent[node]);
27 }
28 return parent[node];
29 };
30
31 // Process each edge in order
32 for (let i = 0; ; ++i) {
33 // Convert from 1-indexed to 0-indexed and find root parents
34 const rootA: number = find(edges[i][0] - 1);
35 const rootB: number = find(edges[i][1] - 1);
36
37 // If both nodes already belong to the same set, this edge creates a cycle
38 if (rootA === rootB) {
39 return edges[i];
40 }
41
42 // Union operation: merge the two sets by making one root parent of the other
43 parent[rootA] = rootB;
44 }
45}
46
Time and Space Complexity
The time complexity is O(n Γ Ξ±(n))
, where n
is the number of edges and Ξ±(n)
is the inverse Ackermann function. With path compression in the find
function, each find operation takes nearly constant time O(Ξ±(n))
, which is effectively O(1)
for all practical values of n
. Since we iterate through n
edges and perform at most two find operations and one union operation per edge, the overall time complexity is O(n Γ Ξ±(n))
, which can be approximated as O(n)
for practical purposes.
However, the reference answer states O(n log n)
. This would be the case if we consider the worst-case scenario without path compression optimization, where the find operation could take O(log n)
time in a balanced union-find tree. Since we perform up to 2n
find operations (two for each edge), this gives us O(n log n)
.
The space complexity is O(n)
. The parent array p
stores n
elements (one for each node, where the number of nodes equals the number of edges in this problem since we're adding one edge to a tree). The recursion stack for the find
function with path compression uses at most O(log n)
space in the worst case, but this doesn't exceed the O(n)
space used by the parent array.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Parent Array Size
One of the most common mistakes is initializing the parent array with size n
instead of n+1
when nodes are labeled from 1
to n
.
Incorrect:
# If we have n nodes, this creates an array of size n
parent = list(range(n)) # [0, 1, 2, ..., n-1]
Problem: When accessing find(node - 1)
where node
could be n
, we'd access index n-1
which works. However, if someone forgets to subtract 1 or makes the array too small, they'll get an IndexError.
Correct Approach:
# Option 1: Use len(edges) as size (since n edges means n nodes for this problem)
parent = list(range(len(edges)))
# Option 2: More explicit - create array of size n+1 to handle 1-indexed nodes
n = len(edges) # n nodes, n edges (tree + 1 extra edge)
parent = list(range(n + 1))
# Then use find(node) directly without subtracting 1
2. Forgetting to Handle 1-Indexed Nodes
The problem states nodes are labeled from 1
to n
, but arrays in Python are 0-indexed.
Incorrect:
for node1, node2 in edges: parent_1 = find(node1) # Wrong! Treats nodes as 0-indexed parent_2 = find(node2)
Correct:
# Option 1: Subtract 1 when finding
for node1, node2 in edges:
parent_1 = find(node1 - 1)
parent_2 = find(node2 - 1)
# Option 2: Use 1-indexed parent array (size n+1)
parent = list(range(n + 1)) # Index 0 unused
for node1, node2 in edges:
parent_1 = find(node1) # Now works directly
parent_2 = find(node2)
3. Missing Path Compression
Without path compression, the find operation becomes inefficient for deep trees.
Inefficient:
def find(x):
while parent[x] != x:
x = parent[x]
return x
Optimized with Path Compression:
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # Compress path
return parent[x]
4. Wrong Union Direction
While the direction of union doesn't affect correctness for this problem, inconsistent union can create unbalanced trees.
Less Optimal:
# Always making first parent point to second parent[parent_1] = parent_2
Better Practice (Union by Rank/Size):
# Track rank or size for optimization rank = [0] * (n + 1) if rank[parent_1] < rank[parent_2]: parent[parent_1] = parent_2 elif rank[parent_1] > rank[parent_2]: parent[parent_2] = parent_1 else: parent[parent_2] = parent_1 rank[parent_1] += 1
5. Not Returning the Edge Immediately
Some might try to collect all redundant edges and return the last one, which is unnecessary and inefficient.
Inefficient:
redundant_edges = [] for node1, node2 in edges: if find(node1 - 1) == find(node2 - 1): redundant_edges.append([node1, node2]) else: union(node1 - 1, node2 - 1) return redundant_edges[-1] # Unnecessary storage
Efficient:
for node1, node2 in edges: if find(node1 - 1) == find(node2 - 1): return [node1, node2] # Return immediately parent[find(node1 - 1)] = find(node2 - 1)
The immediate return works because we process edges in order, and the first edge that would create a cycle is the last one that could be removed (per problem requirements).
How many times is a tree node visited in a depth first search?
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!