Redundant Connection

Difficulty: Medium

You start with a tree of n nodes labeled 1 to n, connected by n - 1 edges. One extra edge then got added on top of that tree, which creates exactly one cycle somewhere in the graph.

You're given the final list of n edges, in the order they were added. Find the one edge that can be removed so the graph becomes a valid tree again. If more than one edge could be removed to break the cycle, return the one that appears last in the input list.

Examples

Input: edges = [[1, 2], [1, 3], [2, 3]]
Output: [2, 3]

1-2 and 1-3 already form a tree connecting all three nodes; adding 2-3 on top closes a cycle, and it's the last edge added.

Input: edges = [[1, 2], [2, 3], [3, 4], [1, 4], [1, 5]]
Output: [1, 4]

1-2-3-4 is a path, and 1-4 closes it into a cycle. 1-5 is added afterward but doesn't touch the cycle.

Constraints

  • n == edges.length

  • 3 <= n <= 1000

  • Each edges[i] = [ai, bi] with 1 <= ai, bi <= n and ai != bi

  • No repeated edges appear in the input.

Approach

Process the edges in the order they were added, using a Union-Find structure that starts with every node in its own separate group. For each edge [a, b]: if a and b are already in the same group, then this edge connects two nodes that could already reach each other - adding it is exactly what creates the cycle, so it's the answer. Since we're scanning in the original order and stop at the first such edge, that's automatically the last edge (relative to the tree that existed before it) that closes the loop.

If a and b are in different groups, merge those groups and move on - this edge is a genuine tree edge.

A more brute-force alternative: for each edge in order, before adding it, run a DFS/BFS over the edges added so far to check if its two endpoints are already reachable from each other. This answers the same question, just recomputed from scratch every time instead of incrementally.

Solutions

Brute Force - Reachability Check Per Edge

Build up the graph edge by edge. Before adding each new edge, run a DFS to check whether its two endpoints can already reach each other using only the edges added so far. The first edge where that's true is the one that closes the cycle.

function findRedundantConnection(edges) {
  const n = edges.length;
  const graph = Array.from({ length: n + 1 }, () => []);

  function canReach(start, target, visited) {
    if (start === target) return true;
    visited.add(start);
    for (const neighbor of graph[start]) {
      if (!visited.has(neighbor) && canReach(neighbor, target, visited)) {
        return true;
      }
    }
    return false;
  }

  for (const [a, b] of edges) {
    if (graph[a].length > 0 || graph[b].length > 0) {
      if (canReach(a, b, new Set())) return [a, b];
    }
    graph[a].push(b);
    graph[b].push(a);
  }

  return [];
}

Time: O(n²) - up to n edges, each requiring an O(n) reachability search · Space: O(n)

Optimal - Union-Find

Keep every node in its own group at the start. For each edge, try to union its two endpoints' groups. If they're already in the same group, this edge is redundant - return it immediately.

function findRedundantConnection(edges) {
  const n = edges.length;
  const parent = Array.from({ length: n + 1 }, (_, i) => i);

  function find(x) {
    if (parent[x] !== x) {
      parent[x] = find(parent[x]); // path compression
    }
    return parent[x];
  }

  for (const [a, b] of edges) {
    const rootA = find(a);
    const rootB = find(b);

    if (rootA === rootB) return [a, b]; // already connected - this edge closes the cycle

    parent[rootA] = rootB; // union
  }

  return [];
}

Time: O(n × α(n)) - nearly O(n), where α is the inverse Ackermann function · Space: O(n)