Redundant Connection
You are given a graph that began as a tree with n nodes labelled 1 to n, to which exactly one additional edge was added. The result is a connected undirected graph with n nodes and n edges, containing exactly one cycle.
Return an edge whose removal restores a tree — that is, leaves the graph connected and acyclic. Any edge on the cycle would work, so when several are valid, return the one appearing last in the input list.
- n == edges.length
- 3 <= n <= 1000
- edges[i] = [aᵢ, bᵢ] with 1 <= aᵢ < bᵢ <= n
- The graph is connected and contains exactly one cycle
- There are no self-loops and no repeated edges
edges = [[1,2],[1,3],[2,3]][2,3]edges = [[1,2],[2,3],[3,4],[1,4],[1,5]][1,4]edges = [[1,2],[2,3],[1,3]][1,3]edges = [[1,2],[1,3],[1,4],[2,3]][2,3]Start with the counting fact the problem is built on, because it explains why the situation is so tightly constrained.
A tree is a graph that is connected and has no cycles. Such a graph on n nodes always has exactly n-1 edges — no more, no fewer.
The reason is worth seeing rather than memorising. Build a tree by adding nodes one at a time: the first node needs no edge, and every subsequent node must be attached to the existing structure by exactly one edge — zero edges would leave it disconnected, and two or more would create a second route between two nodes, which is a cycle. So n nodes require n-1 edges.
Here you are given n nodes and n edges. That is one edge too many, and the surplus edge must have joined two nodes that were already connected, creating exactly one cycle. So the problem is not "find a cycle" — you already know one exists. It is identify which edge closed it.
Process edges in order and build the graph as you go. Before adding edge (u, v), check whether u and v are already connected by the edges added so far — a traversal from u looking for v. If they are, this edge closes the cycle and is the answer.
That is correct, and the tie-break falls out of it: processing in input order means the first edge found to close a cycle is the last-listed edge of the cycle, which is what the problem asks for.
The cost is the problem. Each check is a traversal costing O(n), performed for up to n edges, giving O(n²). Fine at n = 1000, but it repeats a great deal — each traversal re-derives connectivity information that the previous traversals already established and then discarded.
The check does not need a route between u and v. It needs a single yes-or-no: are they already in the same connected group?
So maintain the grouping directly. Give each group a representative — one distinguished member — and store, for each node, a pointer toward its group's representative. Then two nodes are connected exactly when their representatives match, which is two lookups and a comparison instead of a search.
Merging is equally cheap. To join two groups, point one representative at the other; every node that followed the first now reaches the second by one extra step. A single pointer write merges groups of any size. This structure is Union-Find.
The algorithm becomes: for each edge in order, look up both representatives. If they differ, the edge connects two previously separate groups — keep it, and merge. If they match, the two nodes were already connected by earlier edges, so this edge closes a loop. Return it.
def find_redundant_connection(edges):
n = len(edges)
parent = list(range(n + 1)) # nodes are 1..n, so size n+1
rank = [0] * (n + 1)
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]] # flatten the path while walking it
x = parent[x]
return x
for u, v in edges:
ru, rv = find(u), find(v)
if ru == rv:
return [u, v] # already connected: this edge closes the cycle
if rank[ru] < rank[rv]: # attach the shorter tree under the taller
ru, rv = rv, ru
parent[rv] = ru
if rank[ru] == rank[rv]:
rank[ru] += 1
return [] # unreachable given the problem's guaranteeThe two optimisations keep it fast. Path compression — repointing nodes at the root while walking up — means a slow lookup pays to eliminate itself. Union by rank — attaching the shorter tree beneath the taller — stops chains forming in the first place. Together they give O(α(n)) per operation, where α is the inverse Ackermann function, below 5 for any conceivable input. Total: O(n · α(n)), effectively linear.
edges = [[1,2],[2,3],[3,4],[1,4],[1,5]].
Every node starts as its own representative: {1}, {2}, {3}, {4}, {5}.
The edge [1,5] is never examined. Worth noticing that it appears later in the list yet is not the answer: the tie-break is "the last edge of the cycle", not the last edge overall. Node 5 hangs off node 1 as a leaf, and removing [1,5] would disconnect it rather than break any loop.
For the triangle [[1,2],[1,3],[2,3]]: the first two edges merge cleanly, putting 1, 2 and 3 in one group. The third finds 2 and 3 already sharing a representative and returns [2,3]. Reorder the input as [[1,2],[2,3],[1,3]] and the same logic returns [1,3] — the graph is identical, but the answer follows the input order.
The move to extract is turning an expensive question into a cheap one by maintaining the answer instead of computing it. "Are these two connected?" costs a traversal when asked cold, and costs two array lookups when the grouping is kept up to date as edges arrive. The extra bookkeeping during each merge is repaid many times over.
The recognition signal is specific and reliable: edges arriving one at a time, with connectivity queried in between. That combination is Union-Find. If instead the whole graph is given up front and asked about once, a traversal is usually simpler.
The same shape appears throughout. Detecting a cycle in an undirected graph is this exact test. Kruskal's minimum spanning tree algorithm is this plus sorting edges by weight — take each edge cheapest-first and keep it only if the union succeeds, which is precisely "keep it unless it closes a cycle". Counting connected components is this while tracking how many merges succeeded.
Train the reflex: when an edge would join two nodes that are already connected, that edge is redundant by definition. The only real question is how cheaply you can tell — and maintaining group representatives makes it nearly free.
Union-Find Evolution
Phase 1: Initial State
Every element starts as its own representative (Leader). Each node points to itself.
Path Compression
During find(), we reassign parent pointers to point directly to the root. This flattens the tree structure permanently.
Union by Rank
By merging the shorter tree under the taller one, we ensure the tree depth only grows when absolutely necessary, keeping lookups efficient.