684. Redundant Connection - Explanation

Problem Link

Description

You are given a connected undirected graph with n nodes labeled from 1 to n. Initially, it contained no cycles and consisted of n-1 edges.

We have now added one additional edge to the graph. The edge has two different vertices chosen from 1 to n, and was not an edge that previously existed in the graph.

The graph is represented as an array edges of length n where edges[i] = [ai, bi] represents an edge between nodes ai and bi in the graph.

Return an edge that can be removed so that the graph is still a connected non-cyclical graph. If there are multiple answers, return the edge that appears last in the input edges.

Example 1:

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

Output: [2,4]

Example 2:

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

Output: [3,4]

Constraints:

  • n == edges.length
  • 3 <= n <= 100
  • 1 <= edges[i][0] < edges[i][1] <= edges.length
  • There are no repeated edges and no self-loops in the input.


Topics

Recommended Time & Space Complexity

You should aim for a solution as good or better than O(V + E) time and O(V + E) space, where V is the number vertices and E is the number of edges in the graph.


Hint 1

There will be only one edge that creates the cycle in the given problem. Why? Because the graph is initially acyclic, and a cycle is formed only after adding one extra edge that was not present in the graph initially. Can you think of an algorithm that helps determine whether the current connecting edge forms a cycle? Perhaps a component-oriented algorithm?


Hint 2

We can use the Union-Find (DSU) algorithm to create the graph from the given edges. While connecting the edges, if we fail to connect any edge, it means this is the redundant edge, and we return it. How would you implement this?


Hint 3

We create an instance of the DSU object and traverse through the given edges. For each edge, we attempt to connect the nodes using the union function. If the union function returns false, indicating that the current edge forms a cycle, we immediately return that edge.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Graph Representation - Building adjacency lists from edge lists for undirected graphs
  • Depth First Search (DFS) - Traversing graphs and detecting cycles with parent tracking
  • Union-Find (Disjoint Set Union) - Efficiently tracking connected components and detecting when edges create cycles

1. Cycle Detection (DFS)

Intuition

A tree cannot contain a cycle.
While adding edges one by one, the first edge that creates a cycle is the redundant connection.

For each new edge (u, v):

  • Temporarily add it to the graph
  • Run dfs to check if a cycle exists
  • If dfs revisits a node (not coming from its parent), a cycle is formed
    → that edge is the answer

Algorithm

  1. Initialize an empty adjacency list.
  2. For each edge (u, v) in order:
    • Add (u, v) to the graph.
    • Run dfs starting from u to detect a cycle:
      • Track visited nodes.
      • Ignore the parent node during traversal.
      • If a visited node is reached again, a cycle exists.
  3. Return the first edge that causes a cycle.
class Solution:
    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        n = len(edges)
        adj = [[] for _ in range(n + 1)]

        def dfs(node, par):
            if visit[node]:
                return True

            visit[node] = True
            for nei in adj[node]:
                if nei == par:
                    continue
                if dfs(nei, node):
                    return True
            return False

        for u, v in edges:
            adj[u].append(v)
            adj[v].append(u)
            visit = [False] * (n + 1)

            if dfs(u, -1):
                return [u, v]
        return []

Time & Space Complexity

  • Time complexity: O(E(V+E))O(E * (V + E))
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of vertices and EE is the number of edges in the graph.


2. Depth First Search (Optimal)

Intuition

Instead of checking for a cycle after every edge, we build the whole graph once and find the cycle nodes in a single dfs.

Key idea:

  • In an undirected graph made from n edges on n nodes, there is exactly one cycle.
  • During dfs, if we reach a node that is already visited, we just found the start of the cycle.
  • While recursion "unwinds" back, we mark every node on that return path as part of the cycle, until we come back to the cycle start.

After we have the set cycle (all nodes that lie on the cycle):

  • The redundant edge must connect two cycle nodes.
  • The problem asks for the edge that appears last in the input among the cycle edges,
    so we scan edges from the end and return the first edge (u, v) where u and v are both in cycle.

Algorithm

  1. Build an adjacency list for all edges.
  2. Run dfs once to detect the cycle:
    • Maintain visited[].
    • If dfs enters an already visited node, mark it as cycleStart.
    • While returning from recursion, add nodes to cycle until reaching cycleStart, then stop marking.
  3. Iterate edges in reverse order:
    • Return the first edge (u, v) where both endpoints are in cycle.
  4. If none found, return [] (shouldn't happen for valid inputs).
class Solution:
    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        n = len(edges)
        adj = [[] for _ in range(n + 1)]
        for u, v in edges:
            adj[u].append(v)
            adj[v].append(u)

        visit = [False] * (n + 1)
        cycle = set()
        cycleStart = -1

        def dfs(node, par):
            nonlocal cycleStart
            if visit[node]:
                cycleStart = node
                return True

            visit[node] = True
            for nei in adj[node]:
                if nei == par:
                    continue
                if dfs(nei, node):
                    if cycleStart != -1:
                        cycle.add(node)
                    if node == cycleStart:
                        cycleStart = -1
                    return True
            return False

        dfs(1, -1)

        for u, v in reversed(edges):
            if u in cycle and v in cycle:
                return [u, v]

        return []

Time & Space Complexity

  • Time complexity: O(V+E)O(V + E)
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of vertices and EE is the number of edges in the graph.


3. Topological Sort (Kahn's Algorithm)

Intuition

This uses the "peel off leaves" idea (often called topological trimming).
Even though the graph is undirected, we can still remove nodes with degree 1 repeatedly:

  • Nodes with degree 1 cannot be inside a cycle (a cycle needs every node to have degree ≥ 2).
  • So we push all degree-1 nodes into a queue and remove them.
  • When we remove a node, its neighbor's degree decreases; that neighbor might become a new leaf (degree 1), so we remove it next.
  • After this process finishes, the only nodes left with degree > 0 are exactly the cycle nodes.

Finally, the redundant edge must be an edge whose both ends are still in the cycle.
Because we need the last such edge in input order, we scan edges in reverse and return the first edge connecting two remaining cycle nodes.

Algorithm

  1. Build the graph (adjacency list) and compute indegree/degree of every node.
  2. Add all nodes with degree 1 to a queue.
  3. While the queue is not empty:
    • Pop a leaf node x and "remove" it (decrease its degree).
    • For each neighbor y of x, decrease y's degree.
    • If y becomes degree 1, push y into the queue.
  4. Now, nodes with degree > 0 are cycle nodes.
  5. Traverse edges from the end:
    • Return the first edge (u, v) where both degree[u] > 0 and degree[v] > 0.
class Solution:
    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        n = len(edges)
        indegree = [0] * (n + 1)
        adj = [[] for _ in range(n + 1)]
        for u, v in edges:
            adj[u].append(v)
            adj[v].append(u)
            indegree[u] += 1
            indegree[v] += 1

        q = deque()
        for i in range(1, n + 1):
            if indegree[i] == 1:
                q.append(i)

        while q:
            node = q.popleft()
            indegree[node] -= 1
            for nei in adj[node]:
                indegree[nei] -= 1
                if indegree[nei] == 1:
                    q.append(nei)

        for u, v in reversed(edges):
            if indegree[u] == 2 and indegree[v]:
                return [u, v]
        return []

Time & Space Complexity

  • Time complexity: O(V+E)O(V + E)
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of vertices and EE is the number of edges in the graph.


4. Disjoint Set Union

Intuition

Use Disjoint Set Union (Union-Find) to track connected components while adding edges one by one.

  • Initially, every node is its own component.
  • When we add an edge (u, v):
    • If u and v are already in the same component, adding this edge creates a cycle.
    • That edge is exactly the redundant connection.
  • If they are in different components, we safely merge them.

Because edges are processed in order, the first edge that fails to union is the answer.

Algorithm

  1. Initialize DSU where each node is its own parent.
  2. For each edge (u, v):
    • Find the parent of u and v.
    • If both parents are the same:
      • Return (u, v) → this edge creates a cycle.
    • Otherwise, union the two components.
  3. The first edge that cannot be unioned is the redundant edge.

This works because a tree with n nodes has exactly n - 1 edges, and any extra edge must form a cycle.

class Solution:
    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        par = [i for i in range(len(edges) + 1)]
        rank = [1] * (len(edges) + 1)

        def find(n):
            p = par[n]
            while p != par[p]:
                par[p] = par[par[p]]
                p = par[p]
            return p

        def union(n1, n2):
            p1, p2 = find(n1), find(n2)

            if p1 == p2:
                return False
            if rank[p1] > rank[p2]:
                par[p2] = p1
                rank[p1] += rank[p2]
            else:
                par[p1] = p2
                rank[p2] += rank[p1]
            return True

        for n1, n2 in edges:
            if not union(n1, n2):
                return [n1, n2]

Time & Space Complexity

  • Time complexity: O(V+(Eα(V)))O(V + (E * α(V)))
  • Space complexity: O(V)O(V)

Where VV is the number of vertices and EE is the number of edges in the graph. α()α() is used for amortized complexity.


Common Pitfalls

Returning the First Cycle Edge Instead of the Last

The problem asks for the edge that appears last in the input among all edges that could be removed to break the cycle. A common mistake is returning the first edge that creates a cycle during processing. When using Union-Find, processing edges in order naturally returns the correct answer, but DFS-based approaches must scan edges in reverse to find the last valid edge.

Treating the Graph as Directed

This problem involves an undirected graph, but some solutions incorrectly handle edges as directed. When building the adjacency list, each edge (u, v) must be added in both directions. Forgetting this causes cycle detection to miss valid paths and return incorrect results.

Not Handling the Parent Node in DFS Cycle Detection

When using DFS to detect cycles in an undirected graph, you must skip the parent node to avoid false positives. In an undirected graph, the edge (u, v) appears as both u -> v and v -> u in the adjacency list. Without parent tracking, DFS would immediately detect a "cycle" by going back to the node it just came from.