Redundant Connection

Imagine a group of people, and you keep drawing lines to connect them. At some point you draw one line too many, and a loop forms. Redundant Connection asks you to find that one extra line. This is the classic first question for a data structure called union-find, and once you learn it here you will spot it in many graph problems.

🎯 The Problem

You start with a tree, then someone adds one extra edge, and you must find that extra edge.

  • A tree is a set of nodes connected with no loops, where everything is reachable.
  • One extra edge is added. It creates exactly one cycle.
  • You get all the edges and must return the one edge that, if removed, leaves a valid tree again.
  • If more than one answer could work, return the edge that appears last in the input.

For edges [[1,2],[1,3],[2,3]], the first two connect 1-2 and 1-3, which is fine. The third edge 2-3 joins two nodes already connected through 1. That closes a loop. So [2,3] is the redundant edge.

Input: edges = [[1,2],[1,3],[2,3]]
Output: [2, 3]
Explanation: 1-2 and 1-3 form a tree. The edge 2-3 closes a cycle.

Here is the graph. Notice how the three edges form a triangle, which is a loop.

1

2

3

🐒 Approach 1: DFS Connection Check (Brute Force)

We add edges one by one and check for a loop before each add.

The idea:

  • Before adding an edge, ask if its two nodes are already connected.
  • To check, run a depth-first search. That means follow paths from one node to see if you can reach the other.
  • If they are already reachable, this edge would form a loop. So it is the answer.

How it works:

  • Build the graph edge by edge.
  • For each new edge, search from one end to the other before adding it.

Why it is weak:

  • Every connection check walks across the graph again.
  • For each edge you may scan the whole graph.
  • That gets slow when the graph is large.

Here is the DFS-before-union code:

redundant_connection_dfs.py
def find_redundant_connection(edges):
graph = {}
def connected(a, b, seen):
if a == b: return True
seen.add(a)
return any(nei not in seen and connected(nei, b, seen) for nei in graph.get(a, []))
for a, b in edges:
if a in graph and b in graph and connected(a, b, set()):
return [a, b]
graph.setdefault(a, []).append(b); graph.setdefault(b, []).append(a)

⚑ Approach 2: Union-Find (Best)

The idea in one line: keep each group of connected nodes together, and the first edge that joins two nodes already in the same group is the answer.

The idea:

  • Union-find tracks which group each node belongs to and merges two groups fast. It is also called a disjoint set, because groups never overlap.
  • It keeps one array called parent. Follow the parents up and you reach the top node of the group, called the root.
  • Two nodes are in the same group when they share the same root.

How it works:

  • find(x) follows the parent links up until it reaches the root.
  • union(a, b) joins two groups by pointing one root at the other.
  • Go through the edges in order. For each edge [a, b], find both roots.
  • If they already share a root, this edge closes a cycle, so return it.
  • Otherwise union the two nodes and continue.

Why it is fast:

  • Path compression points each node on the find path straight at the root, so the next lookup is shorter.
  • Union by rank attaches the shorter group under the taller one, keeping the trees flat.
  • Together these make every find or union run in almost constant time.

Here is how find with path compression flattens a chain. Before, node 3 points to 2, which points to 1. After, both point straight to the root 1.

Before

1 root

2

3

After path compression

1 root

2

3

Steps to Solve

  1. Create a parent array where every node starts as its own parent, and a rank array of zeros.
  2. Write find(x) that walks up to the root and points each node along the way straight to the root.
  3. Write union(a, b) that finds both roots and attaches the lower-rank root under the higher-rank one.
  4. Go through the edges in the given order.
  5. For each edge [a, b], find both roots. If they match, return this edge, because it closes a cycle.
  6. If the roots differ, union the two nodes and move to the next edge.

This Python version keeps parent and rank as lists and uses recursion for find.

redundant_connection.py
def redundant_connection(edges):
n = len(edges)
parent = list(range(n + 1)) # each node is its own parent
rank = [0] * (n + 1) # rough height of each group
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # path compression
return parent[x]
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb:
return False # already connected
if rank[ra] < rank[rb]: # union by rank
parent[ra] = rb
elif rank[ra] > rank[rb]:
parent[rb] = ra
else:
parent[rb] = ra
rank[ra] += 1
return True
for a, b in edges:
if not union(a, b): # this edge closes a cycle
return [a, b]
return []
edges = [[1, 2], [1, 3], [2, 3]]
print(redundant_connection(edges))

The output of the above code will be:

[2, 3]

Let us walk through the Python version line by line, because union-find is the part worth understanding deeply.

parent = list(range(n + 1)) sets every node as its own parent at the start. So at first every node is alone in its own group. rank = [0] * (n + 1) starts every group with rank zero, since each group is just one node.

Inside find(x), the line if parent[x] != x: checks whether x is the root. The root is the only node that is its own parent. If x is not the root, parent[x] = find(parent[x]) does two things at once. It finds the real root, and it points x straight at that root. That second part is path compression. It flattens the chain so future lookups are fast.

Inside union(a, b), we first get both roots with find. If ra == rb, the two nodes already share a root, so they are already connected. We return False, which is the signal that this edge would form a loop.

The rank lines decide which root becomes the new top. We attach the smaller-rank group under the larger-rank one. That keeps the tree short. When both ranks match, we pick one as the parent and bump its rank by one, because the tree grew slightly taller.

The main loop reads each edge in order. if not union(a, b): return [a, b] says: if the union failed, the two nodes were already connected, so this edge is the redundant one. We return it right away.

⏱️ Time and Space Complexity

The DFS approach checks connection by scanning the graph, so each edge can cost O(V). Across all edges that becomes O(V * E). Union-find is far better. With path compression and union by rank, each find or union runs in almost constant time. The exact term is the inverse Ackermann function, written as alpha of n, which stays under five for any realistic input. So processing all edges is nearly O(E). The space is O(V) for the parent and rank arrays.

Approach Time Complexity Space Complexity
DFS connection check per edge O(V * E) O(V + E)
Union-find with compression and rank O(E * alpha(V)) O(V)

Tip

Path compression and union by rank work best together. Use only one and you lose part of the speed. Use both and every operation is almost constant time. Mention both in the interview to show you know why union-find is fast.

🧩 Key Takeaways

  • βœ… Union-find tracks which group each node is in and merges groups fast.
  • βœ… The root is the top node of a group. Two nodes match when they share a root.
  • βœ… Path compression points nodes straight at the root, so lookups stay short.
  • βœ… Union by rank attaches the shorter group under the taller one, keeping trees flat.
  • βœ… The first edge whose two nodes already share a root is the redundant one.

Check Your Knowledge

4 questions Show quiz Hide quiz

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does the find operation in union-find return?

    Why: find follows parent links up to the root, which identifies the group a node belongs to.

  2. 2

    What does path compression do?

    Why: While walking up to the root, path compression reattaches each visited node directly to the root, flattening the tree.

  3. 3

    When is an edge the redundant connection?

    Why: If find returns the same root for both nodes, they are already connected, so this edge closes a cycle.

  4. 4

    Why use union by rank?

    Why: Attaching the shorter group under the taller one keeps trees short, so find does less work.

πŸš€ What’s Next?