LeetCode #684 Medium

Redundant Connection

Redundant Connection: A tree of n nodes had one extra edge added, creating exactly one cycle. Find the redundant edge that can be removed.

Constraints
  • n == edges.length
  • 3 <= n <= 1000
  • edges[i].length == 2
  • 1 <= ai < bi <= edges.length
  • ai != bi
  • There are no repeated edges.
  • The given graph is connected.
graphunion-find
Open on LeetCode ↗
02

Intuition

Redundant connection takes a tree that had one extra edge added, creating exactly one cycle, and asks which edge to remove. When several edges could be removed, the answer is the one appearing last in the input. The tempting plan is to find the cycle first — with a DFS — and then choose an edge from it. That works but requires locating the cycle, extracting its edges, and then deciding which one the problem wants. Union-Find answers it in a single pass with no cycle-finding at all. Process the edges in the given order, and for each, ask whether its two endpoints are already connected: - If two endpoints already share a root, this edge closes a cycle — and since edges are processed in order, it is the last such edge. That directly satisfies the tie-break rule without any extra reasoning. The first edge to fail the connectivity test is the answer, so the loop can return immediately. If the endpoints are in different components, the edge is a legitimate tree edge — union the two sets and continue. Two optimisations make Union-Find behave. Path compression flattens the tree on each find, and union by rank or size keeps merges shallow. Without them the structure can degenerate into a linked list and each find becomes O(n); with them the amortised cost is effectively constant.

How to spot this pattern

Union-find processing edges in order. The first edge whose endpoints already share a root closes a cycle, and since the input has exactly one extra edge, that edge is the answer. Processing in order is what satisfies the "return the last such edge" requirement.

03

Approach

Try it first

Before reading on: price up what the direct approach costs here, then ask whether you are really just merging groups and asking what connects. Aim for O(n * alpha(n)) time and O(n) space.

1

Process edges in the given order

The problem asks for the last removable edge, and processing in input order means the first edge that closes a cycle is exactly that one. No separate tie-break logic is needed.

2

Ask whether the endpoints are already connected

For each edge, compare the roots of its two vertices. Same root means both are already in one component, so this edge creates a cycle.

3

Return the first cycle-closing edge

When find(u) == find(v), return that edge immediately. Exactly one such edge exists by the problem's construction, so no further scanning is required.

4

Union and continue otherwise

Different roots mean the edge joins two separate components — a legitimate tree edge. Union them and move on, growing the forest one edge at a time.

5

Apply both Union-Find optimisations

Path compression flattens the tree on each find; union by rank keeps merges shallow. Without them the structure degenerates into a linked list and each operation costs O(n) instead of near-constant.

6

Cost of the single pass

Each of the n edges does two near-constant find operations, giving O(n · α(n)) time — effectively linear — with O(n) space for the parent array. Compare with DFS cycle-finding, which is the same order but needs more code.

04

Solution & live demo

▶1class Solution:
▶2 def findRedundantConnection(self, edges):
▶3 n = len(edges)
▶4 parent = list(range(n + 1))
▶5 def find(x):
▶6 while parent[x] != x:
▶7 parent[x] = parent[parent[x]]
▶8 x = parent[x]
▶9 return x
▶10 for u, v in edges:
▶11 ru, rv = find(u), find(v)
▶12 if ru == rv:
▶13 return [u, v]
▶14 parent[ru] = rv
▶15 return []
05

Common pitfalls

Continuing after finding a cycle

✗ Wrong
if ru == rv:
    answer = [u, v]
✓ Right
if ru == rv:
    return [u, v]

With exactly one redundant edge, the first cycle detected is the only one — and once it's added the structure is no longer a forest, so later detections would be spurious. Returning immediately is both correct and necessary.

Uniting before checking

✗ Wrong
parent[ru] = rv
if ru == rv: return [u, v]
✓ Right
if ru == rv: return [u, v]
parent[ru] = rv

Merging first makes the roots equal by construction, so the test can never fire. The cycle check must read the state from before the union.

Sizing the parent array to n

✗ Wrong
parent = list(range(n))
✓ Right
parent = list(range(n + 1))

Vertices are labelled 1 through n, so index n must exist. A zero-based array of length n throws on the highest-numbered vertex.

06

Edge cases

Cycle formed by the very first redundant edge encountered

Union-Find catches it the instant both endpoints share a root -- no lookahead needed.

Multiple edges could close different cycles

Problem guarantees exactly one extra edge, so only one edge ever finds matching roots.

Self-loop edge [a, a]

find(a) == find(a) trivially -- would be reported as redundant immediately.

Answer must be the LAST qualifying edge

Processing in input order and returning at the first same-root hit naturally gives the last edge in the original list that closes the cycle.

07

Complexity

Time
O(n * alpha(n))
Space
O(n)
Near-constant per operation with path compression and union by attaching roots.