[1,2,3]A tree on n nodes has exactly n-1 edges. Given n edges, one must be extra. Sweep the edges in order with Union-Find: the first edge whose two endpoints already share a root is the one that creates a cycle — that's your answer.
You're given an undirected graph that started as a tree and had one extra edge added to it. The input is a list of n edges forming a graph with n nodes (labeled 1 to n). Return the extra edge. If multiple edges could be removed to restore the tree, return the one that appears last in the input.
Concrete example: edges = [[1,2],[1,3],[2,3]] — the three edges connect three nodes, but a tree on 3 nodes only needs 2 edges. Edge [2,3] forms a triangle with the first two, so the answer is [2,3].
[u, v], check whether u and v are already in the same component(same root). If they are, this edge would create a cycle — it's the answer. If not, union the two components and continue.parent array of size n + 1 where parent[i] = i — each node is its own root. (Size n + 1 so 1-indexed nodes work without offset arithmetic.)find(u) and find(v) to locate their roots. find walks up the parent chain; use path-halving (parent[x] = parent[parent[x]]) so future finds are faster.find(u) === find(v), both nodes already belong to the same tree — adding this edge would close a cycle. Return [u, v] immediately.parent[root(u)] = root(v). Continue to the next edge.Space is O(n) for the parent array. Adding union-by-rank alongside path compression guarantees the inverse-Ackermann bound; path-halving alone is sufficient in practice.
3 nodes is its own parent. We will process 3 edges one by one.1▶function findRedundantConnection(edges: number[][]): number[] {2▶ const parent: number[] = Array.from({ length: edges.length + 1 }, (_, i) => i);34 function find(x: number): number {5 while (parent[x] !== x) {6 parent[x] = parent[parent[x]]; // path-compression (halving)7 x = parent[x];8 }9 return x;10 }1112 for (const [u, v] of edges) {13 const ru = find(u);14 const rv = find(v);15 if (ru === rv) return [u, v]; // both already in same component → cycle16 parent[ru] = rv; // union: attach ru's tree under rv17 }1819 return []; // unreachable given valid input20}
[1,2,3]function findRedundantConnection(edges: number[][]): number[] {
const parent: number[] = Array.from({ length: edges.length + 1 }, (_, i) => i);
function find(x: number): number {
while (parent[x] !== x) {
parent[x] = parent[parent[x]]; // path-compression (halving)
x = parent[x];
}
return x;
}
for (const [u, v] of edges) {
const ru = find(u);
const rv = find(v);
if (ru === rv) return [u, v]; // both already in same component → cycle
parent[ru] = rv; // union: attach ru's tree under rv
}
return []; // unreachable given valid input
}parent[i] = i initializes every node as its own root. We allocate edges.length + 1 slots because nodes are 1-indexed and the number of nodes equals the number of edges in this problem.parent[x] === x finds the root. The line parent[x] = parent[parent[x]] (path-halving) shortens the chain on every traversal, keeping future finds nearly O(1).ru's tree under rv.return []satisfies TypeScript's control-flow checker.find(x) resolves to that root, and all nodes with the same root are in the same connected component.function findRedundantConnection(edges: number[][]): number[] {
const parent: number[] = Array.from({ length: edges.length + 1 }, (_, i) => i);
function find(x: number): number {
while (parent[x] !== x) {
parent[x] = parent[parent[x]]; // path-compression (halving)
x = parent[x];
}
return x;
}
for (const [u, v] of edges) {
const ru = find(u);
const rv = find(v);
if (ru === rv) return [u, v]; // both already in same component → cycle
parent[ru] = rv; // union: attach ru's tree under rv
}
return []; // unreachable given valid input
}Build the graph incrementally. Before inserting edge [u, v], run a DFS to ask “is there already a path from u to v?” If so, this edge closes a cycle — it's the redundant one.
function findRedundantConnection(edges: number[][]): number[] {
const adj = new Map<number, number[]>();
function hasPath(u: number, v: number, seen: Set<number>): boolean {
if (u === v) return true;
seen.add(u);
for (const next of adj.get(u) ?? []) {
if (!seen.has(next) && hasPath(next, v, seen)) return true;
}
return false;
}
for (const [u, v] of edges) {
// If u and v are already connected, this edge creates a cycle.
if (adj.has(u) && adj.has(v) && hasPath(u, v, new Set<number>())) {
return [u, v];
}
if (!adj.has(u)) adj.set(u, []);
if (!adj.has(v)) adj.set(v, []);
adj.get(u)!.push(v);
adj.get(v)!.push(u);
}
return []; // unreachable given valid input
}n edges triggers a DFS that can visit up to nnodes, so the worst case is O(n²). Union-Find with path compression answers the same “already connected?” question in near-constant amortized time, dropping the total to O(n·α(n)).| "extra edge in a tree" / detect cycle in undirected graph | Union-Find over edges |
| merge connected components incrementally | parent[root(u)] = root(v) |
| "graph valid tree" or "connected components" | same Union-Find skeleton |
| find(u) === find(v) before adding edge | cycle-closing check → return edge |
const parent = Array.from({ length: n + 1 }, (_, i) => i);
function find(x: number): number {
while (parent[x] !== x) {
parent[x] = parent[parent[x]]; // path-halving compression
x = parent[x];
}
return x;
}
for (const [u, v] of edges) {
const ru = find(u), rv = find(v);
if (ru === rv) return [u, v]; // first cycle-closing edge
parent[ru] = rv; // union
}edges = [[1,2],[1,3],[2,3]], what does the algorithm return?parent array with size edges.length + 1 rather than edges.length?[[1,2],[2,3],[3,4],[1,4],[1,5]]. Which edge is returned?