203. Redundant Connection
Problem
Given n vertices and an ordered undirected edge list, return the first edge whose addition connects vertices already connected by earlier edges. Return its original endpoint tuple, or None if every edge merges components. A self-loop is immediately redundant; repeated edges can be redundant.
Examples
Example 1
first_redundant_edge(3,[(0,1),(1,2),(0,2)])
=> (0, 2)The third edge closes the first cycle.
Example 2
first_redundant_edge(2,[(0,1)])
=> NoneThe only edge is necessary for connectivity.
Example 3
first_redundant_edge(1,[(0,0)])
=> (0, 0)A self-loop connects an element to itself.
Reveal the worked solution and reasoning
Why this mechanism works
An edge creates an undirected cycle exactly when a path already joins its endpoints. DSU tests this condition without reconstructing the path. Scan in the supplied order and return the first failed union, rather than sorting edges or choosing the last failure.
State and boundaries
DSU represents connectivity from the edge prefix before the current edge. A false union result is the redundancy witness.
Protocol
- Create singleton components.
- Read edges in input order.
- Attempt each union.
- Return the current pair on the first false result.
- Return None if no union fails.
Visualization
0-1 accepted; 1-2 accepted
0-2 finds both endpoints in the same component
return (0,2) before considering later edgesPython implementation
from kata_201 import DSU
def first_redundant_edge(n, edges):
dsu = DSU(n)
for a, b in edges:
if not dsu.union(a, b):
return (a, b)
return None
Worked execution
first_redundant_edge(3,[(0,1),(1,2),(0,2)])
L4 first_redundant_edge: dsu = DSU(n)
n=3; edges=[(0, 1), (1, 2), (0, 2)]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':3}
L5 first_redundant_edge: for a, b in edges:
n=3; edges=[(0, 1), (1, 2), (0, 2)]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':3}; a=0; b=1
L6 first_redundant_edge: if not dsu.union(a, b):
n=3; edges=[(0, 1), (1, 2), (0, 2)]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':2}; a=0; b=1
L5 first_redundant_edge: for a, b in edges:
n=3; edges=[(0, 1), (1, 2), (0, 2)]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':2}; a=1; b=2
L6 first_redundant_edge: if not dsu.union(a, b):
n=3; edges=[(0, 1), (1, 2), (0, 2)]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':1}; a=1; b=2
L5 first_redundant_edge: for a, b in edges:
n=3; edges=[(0, 1), (1, 2), (0, 2)]; dsu=DSU{'parent':[..., ..., ...], 'size':[..., ..., ...], 'count':1}; a=0; b=2Result: (0, 2). The third edge closes the first cycle.
first_redundant_edge(2,[(0,1)])
L4 first_redundant_edge: dsu = DSU(n)
n=2; edges=[(0, 1)]; dsu=DSU{'parent':[..., ...], 'size':[..., ...], 'count':2}
L5 first_redundant_edge: for a, b in edges:
n=2; edges=[(0, 1)]; dsu=DSU{'parent':[..., ...], 'size':[..., ...], 'count':2}; a=0; b=1
L6 first_redundant_edge: if not dsu.union(a, b):
n=2; edges=[(0, 1)]; dsu=DSU{'parent':[..., ...], 'size':[..., ...], 'count':1}; a=0; b=1Result: None. The only edge is necessary for connectivity.
Why the solution is correct
If endpoints are already connected, their existing path plus the new edge forms a cycle, so that edge is redundant. Otherwise it merges components and creates no cycle. Because the scan stops at the first such case, the returned edge is the earliest required witness.
Time and space
O(n+m*alpha(n)) amortized worst-case time and O(n) storage, with early exit possible.
Common mistakes
Returning the final failed union violates this first-edge contract. Sorting the input changes which edge is first.
Reasoning card
State: DSU represents connectivity from the edge prefix before the current edge. A false union result is the redundancy witness.
Invariant: A same-root edge closes an undirected cycle.
Decision to rehearse: Query roots before union.
Finish: Return None if no union fails
Transfer exercise
Return the actual cycle by retaining accepted forest edges and finding the existing endpoint path when union fails.