Skip to content

198. Redundant Connection

πŸ”— LeetCode Problem: 684. Redundant Connection
πŸ“Š Difficulty: Medium
🏷️ Topics: Graph, Union-Find, DFS, Cycle Detection

Problem Statement

In this problem, a tree is an undirected graph that is connected and has no cycles.

You are given a graph that started as a tree with n nodes labeled from 1 to n, with one additional edge added. The added edge has two different vertices chosen from 1 to n, and was not an edge that already existed. The graph is represented as an array edges of length n where edges[i] = [aα΅’, bα΅’] indicates that there is an edge between nodes aα΅’ and bα΅’ in the graph.

Return an edge that can be removed so that the resulting graph is a tree of n nodes. If there are multiple answers, return the answer that occurs last in the input.

Example 1:

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

Output: [2,3]

Explanation:
  Original graph:
    1 --- 2
    |    /
    |   /
    3--/

  The edge [2,3] creates a cycle!
  Removing [2,3] makes it a tree:
    1 --- 2
    |
    3

Example 2:

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

Output: [1,4]

Explanation:
  The edge [1,4] creates a cycle: 1β†’2β†’3β†’4β†’1
  Removing [1,4] makes it a tree.

Constraints: - n == edges.length - 3 <= n <= 1000 - edges[i].length == 2 - 1 <= aα΅’ < bα΅’ <= edges.length - aα΅’ != bα΅’ - There are no repeated edges. - The given graph is connected.


🌳 Visual Understanding - The Complete Picture

The Problem We're Solving πŸ€”

Given edges that form a graph with ONE extra edge: πŸ”—

Key facts:
  βœ… Started as a tree (n nodes, n-1 edges)
  ❌ One extra edge added (now n edges!)
  ⚠️ This creates exactly ONE cycle!

Goal: Find the extra edge (that creates cycle) 🎯

Example:
  edges = [[1,2], [1,3], [2,3]]

  Add edges one by one:
    [1,2]: 1 --- 2  βœ… No cycle
    [1,3]: 1 --- 2  βœ… No cycle
           |
           3
    [2,3]: 1 --- 2  ❌ CYCLE DETECTED!
           |    /
           |   /
           3--/

  Answer: [2,3] (the edge that created cycle!)

Understanding Trees vs Graphs 🌳

Tree properties: 🌳
  βœ… n nodes β†’ n-1 edges (exactly!)
  βœ… Connected (all nodes reachable)
  βœ… No cycles
  βœ… Exactly one path between any two nodes

Example tree (4 nodes, 3 edges):
  1 --- 2
  |
  3 --- 4

Graph with cycle (4 nodes, 4 edges):
  1 --- 2
  |    /
  |   /
  3 --- 4

Extra edge creates cycle! ❌

Why Union-Find is Perfect Here! 🎯

Union-Find naturally detects cycles! πŸ’‘

The key insight:
  When adding edge [u, v]:
    If u and v are ALREADY in same set β†’ CYCLE! ❌
    Otherwise β†’ Safe to union βœ…

Example:
  edges = [[1,2], [2,3], [1,3]]

  Process [1,2]:
    find(1) = 1, find(2) = 2
    Different sets! Union them βœ…

  Process [2,3]:
    find(2) = 1, find(3) = 3
    Different sets! Union them βœ…

  Process [1,3]:
    find(1) = 1, find(3) = 1
    SAME SET! Cycle detected! ❌
    Return [1,3] 🎯

Beautiful cycle detection! 🌟

🧠 The Journey - Building Intuition

Part 1: Understanding the Problem - "What makes it redundant?" πŸ€”

πŸ’­ Let's think about what "redundant" means!

Tree definition:
  n nodes β†’ EXACTLY n-1 edges
  Any more edges β†’ cycles!

Our problem:
  n nodes β†’ n edges (one too many!)
  Extra edge creates a cycle!

Example with 3 nodes:
  Tree needs: 2 edges
  We have: 3 edges
  Extra: 1 edge (redundant!)

Which edge is redundant?
  The one that completes a cycle! 🎯

Part 2: Adding Edges One by One πŸ“

πŸ’­ Key insight: Process edges in ORDER!

Think of building graph step by step:

Example: [[1,2], [1,3], [2,3]]

Step 1: Add [1,2]
  1 --- 2

  No cycle! βœ…

Step 2: Add [1,3]
  1 --- 2
  |
  3

  No cycle! βœ…

Step 3: Add [2,3]
  1 --- 2
  |    /
  |   /
  3--/

  CYCLE! ❌

This edge [2,3] is redundant! 🎯

The redundant edge is the FIRST edge that creates a cycle!

Part 3: How to Detect Cycles? πŸ”

πŸ’­ Union-Find gives us instant cycle detection!

Remember from Problem 195:
  Union-Find tracks which nodes are connected

For cycle detection:
  Before adding edge [u, v]:
    Check: Are u and v already connected?

  If YES β†’ Adding this edge creates cycle! ❌
  If NO β†’ Safe to add βœ…

Example:
  edges = [[1,2], [2,3], [1,3]]
  Initially: Sets {1}, {2}, {3}

  Edge [1,2]:
    find(1) = 1, find(2) = 2
    Different! No cycle βœ…
    Union: Sets {1,2}, {3}

  Edge [2,3]:
    find(2) = 1, find(3) = 3
    Different! No cycle βœ…
    Union: Sets {1,2,3}

  Edge [1,3]:
    find(1) = 1, find(3) = 1
    SAME! They're already connected! ❌
    This edge creates cycle!
    Return [1,3] 🎯

Union-Find makes this O(α(n)) per edge! ⚑

Part 4: Why "Last in Input" Matters πŸ“‹

πŸ’­ Problem asks for "last" redundant edge in input!

What if multiple edges could be removed?

Example: [[1,2], [2,3], [3,1]]
  Any edge could be removed to break cycle!

  But we want LAST in input order!

  Process in order:
    [1,2]: Add βœ…
    [2,3]: Add βœ…  
    [3,1]: CYCLE! Return [3,1] (last!) 🎯

By processing edges in input order,
Union-Find naturally finds the last one! βœ…

Part 5: The Algorithm - Simple and Elegant! πŸ’‘

✨ Complete algorithm:

1. Initialize Union-Find for n nodes

2. For each edge [u, v] in order:

   a. Check if u and v are already connected:
      rootU = find(u)
      rootV = find(v)

   b. If rootU == rootV:
      β†’ Already connected!
      β†’ This edge creates cycle!
      β†’ Return [u, v] 🎯

   c. Otherwise:
      β†’ Not connected yet
      β†’ Union them
      β†’ Continue

3. If we get here, no cycle found
   (But problem guarantees one exists!)

Time: O(n Γ— Ξ±(n)) β‰ˆ O(n) ⚑
Space: O(n) πŸ“¦

Elegant and efficient! 🌟

🎯 Approach 1: Union-Find (Optimal!)

The Key Insight πŸ’‘

Use Union-Find for cycle detection πŸ”—! Process edges in input order πŸ“. For each edge, check if endpoints already connected using find() πŸ”. If same root (connected) β†’ cycle detected, return this edge! ❌ Otherwise, union them βœ…. First edge creating cycle = answer! 🎯 Time: O(n Γ— Ξ±(n)) β‰ˆ O(n) ⚑, Space: O(n) πŸ“¦!

Perfect for cycle detection! 🌟

Complete Implementation

class Solution {
    // ═══════════════════════════════════════════════════════
    // UNION-FIND FOR CYCLE DETECTION πŸ”—
    // ═══════════════════════════════════════════════════════
    private int[] parent;
    private int[] rank;

    public int[] findRedundantConnection(int[][] edges) {
        // ═══════════════════════════════════════════════════
        // UNION-FIND APPROACH 🌟
        // ═══════════════════════════════════════════════════
        // Process edges in order
        // First edge that connects already-connected nodes = answer!
        // ═══════════════════════════════════════════════════

        int n = edges.length;
        parent = new int[n + 1];  // 1-indexed!
        rank = new int[n + 1];

        // ═══════════════════════════════════════════════════
        // INITIALIZE: Each node is its own set πŸ—οΈ
        // ═══════════════════════════════════════════════════
        for (int i = 1; i <= n; i++) {
            parent[i] = i;
        }

        // ═══════════════════════════════════════════════════
        // PROCESS EDGES IN ORDER πŸ“
        // ═══════════════════════════════════════════════════
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];

            // ───────────────────────────────────────────────
            // FIND ROOTS OF BOTH NODES πŸ”
            // ───────────────────────────────────────────────
            int rootU = find(u);
            int rootV = find(v);

            // ───────────────────────────────────────────────
            // CHECK FOR CYCLE! 🎯
            // ───────────────────────────────────────────────
            if (rootU == rootV) {
                // ───────────────────────────────────────────
                // ALREADY CONNECTED! ❌
                // This edge creates a cycle!
                // Return this edge (last in input)
                // ───────────────────────────────────────────
                return edge;
            }

            // ───────────────────────────────────────────────
            // NOT CONNECTED YET - UNION THEM βœ…
            // ───────────────────────────────────────────────
            union(rootU, rootV);
        }

        // Should never reach here (problem guarantees cycle exists)
        return new int[]{};
    }

    // ═══════════════════════════════════════════════════════
    // FIND: Get root with path compression ⚑
    // ═══════════════════════════════════════════════════════
    private int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);  // Path compression!
        }
        return parent[x];
    }

    // ═══════════════════════════════════════════════════════
    // UNION: Merge two sets with union by rank πŸ“Š
    // ═══════════════════════════════════════════════════════
    private void union(int x, int y) {
        if (rank[x] < rank[y]) {
            parent[x] = y;
        } else if (rank[x] > rank[y]) {
            parent[y] = x;
        } else {
            parent[y] = x;
            rank[x]++;
        }
    }
}

Why This Works 🎯

Union-Find detects cycles naturally: πŸ”— - If find(u) == find(v): already connected! - Adding edge would create cycle! - This is our redundant edge!

Processing in order gives "last": πŸ“ - We process edges in input order - First edge that causes cycle = last redundant edge in input - Return immediately when cycle detected!

Optimizations make it fast: ⚑ - Path compression: O(α(n)) find operations - Union by rank: keeps trees balanced - Overall: nearly O(1) per edge!


🎯 Approach 2: DFS (Alternative)

The Key Insight πŸ’‘

Build graph gradually with DFS cycle detection 🌊. For each edge, check if path already exists between endpoints using DFS πŸ”. If path exists β†’ cycle! ❌ Otherwise, add edge to graph βœ…. Time: O(nΒ²) ⚑, Space: O(n) πŸ“¦!

More intuitive but slower! 🌟

Complete Implementation

class Solution {
    public int[] findRedundantConnection(int[][] edges) {
        // ═══════════════════════════════════════════════════
        // DFS APPROACH - CYCLE DETECTION 🌊
        // ═══════════════════════════════════════════════════
        // Build graph incrementally
        // Check for cycle before adding each edge
        // ═══════════════════════════════════════════════════

        int n = edges.length;
        List<List<Integer>> graph = new ArrayList<>();

        // Initialize graph
        for (int i = 0; i <= n; i++) {
            graph.add(new ArrayList<>());
        }

        // ═══════════════════════════════════════════════════
        // PROCESS EDGES ONE BY ONE πŸ“
        // ═══════════════════════════════════════════════════
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];

            // ───────────────────────────────────────────────
            // CHECK IF PATH EXISTS: u -> v? πŸ”
            // ───────────────────────────────────────────────
            boolean[] visited = new boolean[n + 1];

            if (hasPath(u, v, graph, visited)) {
                // ───────────────────────────────────────────
                // PATH EXISTS! Adding edge creates cycle! ❌
                // ───────────────────────────────────────────
                return edge;
            }

            // ───────────────────────────────────────────────
            // NO PATH - SAFE TO ADD EDGE βœ…
            // ───────────────────────────────────────────────
            graph.get(u).add(v);
            graph.get(v).add(u);
        }

        return new int[]{};
    }

    // ═══════════════════════════════════════════════════════
    // DFS: Check if path exists from source to target πŸ”
    // ═══════════════════════════════════════════════════════
    private boolean hasPath(int source, int target, 
                           List<List<Integer>> graph, boolean[] visited) {
        // ───────────────────────────────────────────────────
        // BASE CASE: Reached the target! 🎯
        // ───────────────────────────────────────────────────
        // This checks: "Am I at the destination node?"
        // If YES β†’ Path exists! We found it! βœ…
        // ───────────────────────────────────────────────────
        if (source == target) {
            return true;  // Found path! βœ…
        }

        visited[source] = true;

        for (int neighbor : graph.get(source)) {
            if (!visited[neighbor]) {
                if (hasPath(neighbor, target, graph, visited)) {
                    return true;
                }
            }
        }

        return false;  // No path found
    }
}

Understanding source == target πŸ€”

πŸ’­ Why do we check "source == target"?

The function asks: "Is there a path from source to target?"

The base case "source == target" means:
  "I've REACHED the target node!" 🎯
  "Path exists! Return true!" βœ…

Let's trace through an example...

╔══════════════════════════════════════════════════════════╗
β•‘  EXAMPLE: Why we need source == target check            β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

Current graph:
  1 --- 2
        |
        3

Try adding edge [1, 3]:
  Question: Does path exist from 1 to 3?

DFS Trace: hasPath(1, 3, graph, visited)

Step 1: hasPath(1, 3)
  source = 1, target = 3
  1 == 3? NO ❌ (not at target yet)
  visited[1] = true
  neighbors of 1: [2]

  Try neighbor 2:

Step 2: hasPath(2, 3)  
  source = 2, target = 3
  2 == 3? NO ❌ (not at target yet)
  visited[2] = true
  neighbors of 2: [1, 3]
  (skip 1, already visited)

  Try neighbor 3:

Step 3: hasPath(3, 3)
  source = 3, target = 3
  3 == 3? YES! βœ…βœ…βœ…

  🎯 WE REACHED THE TARGET!
  This is the SUCCESS condition!
  Return TRUE!

← Step 2 returns TRUE (found through neighbor 3)
← Step 1 returns TRUE (found through neighbor 2)

RESULT: Path exists 1β†’2β†’3! ❌
Don't add edge [1,3] - would create cycle!

The source == target check is HOW WE KNOW we found it! 🎯

Complete Visual Trace πŸ”

╔═══════════════════════════════════════════════════════╗
β•‘         CALL STACK VISUALIZATION                     β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

Graph: 1 --- 2 --- 3

Question: Path from 1 to 3?

hasPath(1, 3):
  β”‚
  β”œβ”€ Check: 1 == 3? NO ❌
  β”‚   β†’ Not at destination yet, keep searching
  β”‚
  β”œβ”€ Mark visited[1] = true
  β”‚
  └─ Try neighbor 2:
      β”‚
      hasPath(2, 3):
        β”‚
        β”œβ”€ Check: 2 == 3? NO ❌  
        β”‚   β†’ Still not there, keep going
        β”‚
        β”œβ”€ Mark visited[2] = true
        β”‚
        └─ Try neighbor 3:
            β”‚
            hasPath(3, 3):
              β”‚
              β”œβ”€ Check: 3 == 3? YES! βœ…
              β”‚   β†’ FOUND IT! We're AT the target!
              β”‚   β†’ This is the base case!
              β”‚
              └─ return TRUE

            ← Returns TRUE to caller

        ← Returns TRUE to caller

    ← Returns TRUE to caller

Final answer: Path exists! βœ…

What if we DIDN'T have this check? ❌

Without "source == target" check:

hasPath(3, 3):  // Reached node 3
  // No base case to recognize we found it!

  visited[3] = true
  neighbors of 3: [2]

  Try neighbor 2:
    Already visited! Skip.

  No more neighbors.
  return FALSE ❌❌❌

WRONG! We DID reach node 3 but didn't recognize it!

The "source == target" check explicitly says:
  "Hey! I'm AT the target node right now!"
  "That means I found the path!" βœ…

Without it, we'd be AT the destination but not realize it! 🎯

Comparison: Found vs Not Found πŸ”

CASE 1: Path EXISTS
  Graph: 1 --- 2 --- 3
  Query: hasPath(1, 3)

  DFS will eventually call hasPath(3, 3)
  3 == 3? YES! βœ… Return TRUE

  The base case triggers! Success! 🎯

CASE 2: Path DOESN'T EXIST
  Graph: 1 --- 2     4 --- 5
              |
              3
  Query: hasPath(1, 5)

  DFS explores: 1 β†’ 2 β†’ 3
  Never reaches node 5!
  Never triggers "source == target"

  Eventually all neighbors exhausted
  Return FALSE βœ…

  No base case triggered = no path! 😞

The "source == target" check is the ONLY way
DFS knows it successfully found the target! πŸ”‘

Think of it Like a Treasure Hunt πŸ—ΊοΈ

You're looking for treasure at location X:

source = your current location
target = treasure location X

As you explore:
  Check: "Am I at location X?" (source == target?)

  If YES β†’ Found treasure! βœ… Success!
  If NO β†’ Keep exploring neighbors

Without this check:
  You could be STANDING on the treasure
  But not realize you found it! ❌

The check is essential:
  It's how you recognize SUCCESS! 🎯

Why This Works 🎯

DFS checks for existing path: 🌊 - Before adding edge [u,v], DFS checks if path exists - If path exists: adding edge creates alternate path (cycle!) - If no path: safe to add

Incremental graph building: πŸ—οΈ - Build graph edge by edge - Check before each addition - Natural cycle detection!

Slower but more intuitive: πŸ“Š - Each DFS: O(n) time - n edges: O(nΒ²) total - But easier to understand!


πŸ” Detailed Dry Run - Union-Find Approach

Let's trace on:

edges = [[1,2],[1,3],[2,3]]

╔══════════════════════════════════════════════════════════════╗
β•‘                       INITIALIZATION                         β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

n = 3 (three edges, so three nodes)

Initialize Union-Find:
  parent = [_, 1, 2, 3]  // Index 0 unused (1-indexed)
  rank   = [_, 0, 0, 0]

Sets: {1}, {2}, {3}  (all separate)

Visual:
  1    2    3
╔══════════════════════════════════════════════════════════════╗
β•‘                    PROCESS EDGES IN ORDER                    β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

╔═════════════════════════════════════════════════╗
β•‘ EDGE 1: [1, 2]                                  β•‘
╠═════════════════════════════════════════════════╣
β•‘                                                 β•‘
β•‘ Check if 1 and 2 are connected:                β•‘
β•‘   rootU = find(1) = 1                          β•‘
β•‘   rootV = find(2) = 2                          β•‘
β•‘                                                 β•‘
β•‘ rootU == rootV? (1 == 2?)                      β•‘
β•‘   NO! Different sets! βœ…                       β•‘
β•‘                                                 β•‘
β•‘ No cycle! Union them:                           β•‘
β•‘   union(1, 2)                                   β•‘
β•‘   rank[1] == rank[2] (both 0)                  β•‘
β•‘   parent[2] = 1                                β•‘
β•‘   rank[1]++  (0 β†’ 1)                           β•‘
β•‘                                                 β•‘
β•‘ Result:                                         β•‘
β•‘   parent = [_, 1, 1, 3]                        β•‘
β•‘   rank   = [_, 1, 0, 0]                        β•‘
β•‘                                                 β•‘
β•‘ Visual:                                         β•‘
β•‘     1                                           β•‘
β•‘     ↑                                           β•‘
β•‘     2         3                                 β•‘
β•‘                                                 β•‘
β•‘ Sets: {1, 2}, {3}                              β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

╔═════════════════════════════════════════════════╗
β•‘ EDGE 2: [1, 3]                                  β•‘
╠═════════════════════════════════════════════════╣
β•‘                                                 β•‘
β•‘ Check if 1 and 3 are connected:                β•‘
β•‘   rootU = find(1) = 1                          β•‘
β•‘   rootV = find(3) = 3                          β•‘
β•‘                                                 β•‘
β•‘ rootU == rootV? (1 == 3?)                      β•‘
β•‘   NO! Different sets! βœ…                       β•‘
β•‘                                                 β•‘
β•‘ No cycle! Union them:                           β•‘
β•‘   union(1, 3)                                   β•‘
β•‘   rank[1] = 1, rank[3] = 0                     β•‘
β•‘   rank[1] > rank[3]                            β•‘
β•‘   parent[3] = 1                                β•‘
β•‘                                                 β•‘
β•‘ Result:                                         β•‘
β•‘   parent = [_, 1, 1, 1]                        β•‘
β•‘   rank   = [_, 1, 0, 0]                        β•‘
β•‘                                                 β•‘
β•‘ Visual:                                         β•‘
β•‘       1                                         β•‘
β•‘      / \                                        β•‘
β•‘     2   3                                       β•‘
β•‘                                                 β•‘
β•‘ Sets: {1, 2, 3}  (all connected!)              β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

╔═════════════════════════════════════════════════╗
β•‘ EDGE 3: [2, 3]                                  β•‘
╠═════════════════════════════════════════════════╣
β•‘                                                 β•‘
β•‘ Check if 2 and 3 are connected:                β•‘
β•‘   rootU = find(2):                             β•‘
β•‘     parent[2] = 1                              β•‘
β•‘     parent[1] = 1 βœ… (root!)                   β•‘
β•‘     return 1                                    β•‘
β•‘                                                 β•‘
β•‘   rootV = find(3):                             β•‘
β•‘     parent[3] = 1                              β•‘
β•‘     parent[1] = 1 βœ… (root!)                   β•‘
β•‘     return 1                                    β•‘
β•‘                                                 β•‘
β•‘ rootU == rootV? (1 == 1?)                      β•‘
β•‘   YES! SAME ROOT! ❌                           β•‘
β•‘                                                 β•‘
β•‘ ⚠️ CYCLE DETECTED! ⚠️                          β•‘
β•‘                                                 β•‘
β•‘ 2 and 3 are ALREADY CONNECTED through 1!       β•‘
β•‘ Adding edge [2,3] creates a cycle:              β•‘
β•‘                                                 β•‘
β•‘   Before [2,3]:      After [2,3]:              β•‘
β•‘       1                   1                     β•‘
β•‘      / \                 / \                    β•‘
β•‘     2   3               2---3  ← Cycle!        β•‘
β•‘                                                 β•‘
β•‘ RETURN [2, 3] 🎯                                β•‘
β•‘                                                 β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
╔══════════════════════════════════════════════════════════════╗
β•‘                    FINAL RESULT: [2, 3]                      β•‘
╠══════════════════════════════════════════════════════════════╣
β•‘                                                              β•‘
β•‘  The edge [2, 3] is redundant because:                      β•‘
β•‘    β€’ Node 2 is connected to node 1                          β•‘
β•‘    β€’ Node 3 is connected to node 1                          β•‘
β•‘    β€’ So 2 and 3 are already connected through 1!            β•‘
β•‘    β€’ Adding direct edge 2↔3 creates cycle                   β•‘
β•‘                                                              β•‘
β•‘  Removing [2, 3] leaves a tree:                             β•‘
β•‘       1                                                      β•‘
β•‘      / \                                                     β•‘
β•‘     2   3                                                    β•‘
β•‘                                                              β•‘
β•‘  Perfect tree: 3 nodes, 2 edges! βœ…                         β•‘
β•‘                                                              β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

πŸ”‘ Key Observations:

  1. Union-Find tracks connectivity: πŸ”— After first two edges, all three nodes connected through node 1

  2. Third edge creates cycle: ❌ Nodes 2 and 3 already have path through 1

  3. find() reveals connection: πŸ” Both have same root (1), so already in same set

  4. Immediate detection: ⚑ No need to search entire graph, Union-Find knows instantly!

  5. Last in input returned: πŸ“ [2,3] is the last edge, which causes the cycle


πŸ“Š Complexity Analysis

Union-Find Approach

Time Complexity: O(n Γ— Ξ±(n)) β‰ˆ O(n) ⚑

Where:
  n = number of edges
  Ξ±(n) = inverse Ackermann function

For each edge: O(Ξ±(n))
  find(u): O(Ξ±(n)) with path compression
  find(v): O(Ξ±(n)) with path compression
  union: O(1) after finds

Total: n edges Γ— O(Ξ±(n)) = O(n Γ— Ξ±(n))

Since Ξ±(n) ≀ 5 for all practical n:
  Effectively O(n)! ⚑

Example: n = 1000
  Operations: ~5000 (very fast!) βœ…

Space Complexity: O(n) πŸ“¦

Parent array: O(n)
Rank array: O(n)

Total: O(n) πŸ“¦

Very efficient! ✨

DFS Approach

Time Complexity: O(nΒ²) πŸ“Š

For each edge: O(n)
  DFS traversal: O(V + E) = O(n) worst case
  (Before adding ith edge, graph has i-1 edges)

Total: O(n) edges Γ— O(n) DFS = O(nΒ²)

Example: n = 1000
  Operations: ~1,000,000 (slower) ❌

Still acceptable but slower than Union-Find!

Space Complexity: O(n) πŸ“¦

Graph adjacency list: O(n)
Visited array: O(n)
Recursion stack: O(n)

Total: O(n) πŸ“¦


Union-Find Cycle Detection Pattern 🎯

// Template for cycle detection with Union-Find
class Solution {
    private int[] parent;
    private int[] rank;

    public EdgeType detectCycle(Edge[] edges) {
        int n = edges.length;
        parent = new int[n + 1];
        rank = new int[n + 1];

        // Initialize
        for (int i = 1; i <= n; i++) {
            parent[i] = i;
        }

        // Process edges in order
        for (Edge edge : edges) {
            int u = edge.from;
            int v = edge.to;

            int rootU = find(u);
            int rootV = find(v);

            // CYCLE DETECTION! πŸ”‘
            if (rootU == rootV) {
                return edge;  // This edge creates cycle!
            }

            union(rootU, rootV);
        }

        return null;  // No cycle
    }

    private int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    private void union(int x, int y) {
        if (rank[x] < rank[y]) {
            parent[x] = y;
        } else if (rank[x] > rank[y]) {
            parent[y] = x;
        } else {
            parent[y] = x;
            rank[x]++;
        }
    }
}

When to Use This Pattern 🎯

βœ… Perfect for:
  β€’ "Find redundant connection/edge"
  β€’ "Detect cycle in undirected graph"
  β€’ "Remove one edge to make tree"
  β€’ "Build minimum spanning tree"
  β€’ "Check if adding edge creates cycle"

πŸ” Recognition keywords:
  β€’ "Redundant"
  β€’ "Extra edge"
  β€’ "Creates cycle"
  β€’ "Remove to make tree"
  β€’ "Process edges one by one"

πŸ“Š Why Union-Find wins:
  β€’ O(Ξ±(n)) per edge (nearly O(1))
  β€’ No graph building needed
  β€’ Direct cycle detection
  β€’ Elegant and simple

Direct Applications:

  1. Graph Valid Tree (LC 261) - Medium 🌳 β€’ What's similar: Check for cycles, tree validation πŸ” β€’ What's different: Check if valid tree (not find redundant) βœ… β€’ Key insight: Tree: n nodes, n-1 edges, no cycles! 🎯

  2. Number of Connected Components (LC 323) - Medium πŸ”— β€’ What's similar: Union-Find to count components πŸ“Š β€’ What's different: No cycle detection needed βœ… β€’ Key insight: Count leaders in Union-Find! 🎯

  3. Redundant Connection II (LC 685) - Hard πŸ”„ β€’ What's similar: Find redundant edge in graph πŸ” β€’ What's different: DIRECTED graph (more complex!) ⚠️ β€’ Key insight: Handle in-degree violations! πŸ’‘

  4. Accounts Merge (LC 721) - Medium πŸ“§ β€’ What's similar: Union-Find to merge groups 🀝 β€’ What's different: Merging based on common emails βœ… β€’ Key insight: Union-Find for grouping! 🎯


πŸ“ Quick Revision Notes

🎯 Core Concept

Redundant Connection finds edge that creates cycle in graph πŸ”—. Use Union-Find for cycle detection 🎯! Process edges in order πŸ“. For each edge [u,v], check if find(u) == find(v) πŸ”. If same root β†’ already connected β†’ adding edge creates cycle! ❌ Return this edge (last redundant)! Otherwise union them βœ…. Time: O(n Γ— Ξ±(n)) β‰ˆ O(n) ⚑, Space: O(n) πŸ“¦! Union-Find makes cycle detection O(1)! πŸš€

⚑ Quick Implementation

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;

public class Solution {
  public int[] findRedundantConnection(int[][] edges) {
    // // Approach 1 - DFS
    // return dfs(edges);

    // Approach 2 - Union Find
    return unionFind(edges);
  }

  private int[] unionFind(int[][] edges) {
    // Step 1: initialize (parents as themselves and zero ranks).
    int len = edges.length;
    int[] parent = new int[len + 1];
    int[] rank = new int[len + 1];

    for (int i = 0; i <= len; i++) {
      parent[i] = i;
      rank[i] = 0;
    }

    for (int[] edge : edges) {
      int rootX = find(edge[0], parent);
      int rootY = find(edge[1], parent);

      if (rootX == rootY) {
        return edge;
      }

      union(rootX, rootY, parent, rank);
    }

    return new int[] {};
  }

  private void union(int x, int y, int[] parent, int[] rank) {
    if (rank[x] < rank[y]) {
      parent[x] = y;
    } else if (rank[x] > rank[y]) {
      parent[y] = x;
    } else {
      parent[y] = x;
      rank[x]++;
    }
  }

  private int find(int i, int[] parent) {
    if (parent[i] != i) {
      parent[i] = find(parent[i], parent);
    }

    return parent[i];
  }

  private int[] dfs(int[][] edges) {
    // Step 1: start creating graph from vertices.
    List<List<Integer>> graph = new ArrayList<>();

    // Initializing.
    for (int i = 0; i <= edges.length; i++) {
      graph.add(new ArrayList<>());
    }

    // Start addding edges one by one.
    for (int[] edge : edges) {
      // GOTCHA: we need to reinitialize visited set every loop.
      // If not, 1 -> 2 makes visited: {1, 2} and when edge 1 -> 3 comes,
      // since 1 and 3 are already in visited set, never gets added and
      // always return an empty array.
      HashSet<Integer> visited = new HashSet<>();
      // before adding check if there is a path already that exists between
      // edge[0] and edge[1] that makes the graph cyclic.
      if (hasPathExist(edge[0], edge[1], graph, visited)) {
        return edge;
      }

      // If not add the edge to the graph and proceed next.
      graph.get(edge[0]).add(edge[1]);
      graph.get(edge[1]).add(edge[0]);
    }

    return new int[] {};
  }

  private boolean hasPathExist(int source, int destination, List<List<Integer>> graph, HashSet<Integer> visited) {
    // Means I have destination reached from source. There
    // exists a path and have to return true.
    if (source == destination) {
      return true;
    }

    visited.add(source);

    for (int neighbour : graph.get(source)) {
      if (!visited.contains(neighbour)) {
        if (hasPathExist(neighbour, destination, graph, visited)) {
          return true;
        }
      }
    }

    return false;
  }

  public static void main(String[] args) {
    Solution s = new Solution();

    System.out.println(Arrays.toString(s.findRedundantConnection(new int[][] { { 1, 2 }, { 1, 3 }, { 2, 3 } })));
    System.out.println(
        Arrays.toString(s.findRedundantConnection(new int[][] { { 1, 2 }, { 2, 3 }, { 3, 4 }, { 1, 4 }, { 1, 5 } })));
  }
}

πŸ”‘ Key Insights

Cycle Detection with Union-Find: πŸ”‘

The brilliant insight:

Before adding edge [u, v]:
  Check: Are u and v already connected?

How? Find their roots!
  rootU = find(u)
  rootV = find(v)

If rootU == rootV:
  β†’ They're in SAME set!
  β†’ Already connected!
  β†’ Adding edge creates alternate path (cycle!) ❌

If rootU != rootV:
  β†’ Different sets
  β†’ Not connected yet
  β†’ Safe to add edge βœ…
  β†’ Union them

Example:
  Graph: 1 --- 2
         |
         3

  Sets: {1, 2, 3} (all connected through 1)

  Try adding [2, 3]:
    find(2) = 1
    find(3) = 1
    Same root! Already connected through 1!
    Adding [2,3] creates triangle (cycle!)

    1 --- 2
    |    /
    |   /
    3--/  ← Cycle!

Union-Find detects this instantly! ⚑

Why Process in Input Order: πŸ“

Problem requirement: Return LAST redundant edge

How we achieve it:
  Process edges in input order
  Return FIRST edge that creates cycle

This first edge that creates cycle IS the last redundant edge!

Example:
  edges = [[1,2], [2,3], [3,1]]

  All three edges form cycle
  But which to remove?

  Process order:
    [1,2]: Add βœ…
    [2,3]: Add βœ…
    [3,1]: Cycle! ❌ Return [3,1]

  [3,1] is LAST in input, as required! 🎯

By processing in order, we naturally find last! βœ…

Tree Properties Reminder: 🌳

Valid tree requirements:

1. n nodes β†’ n-1 edges (exactly!) πŸ”’
   More edges β†’ has cycle
   Fewer edges β†’ disconnected

2. Connected (all reachable) πŸ”—

3. No cycles πŸ”„

Our problem:
  βœ… Connected (guaranteed)
  ❌ n nodes, n edges (one too many!)
  ❌ Has exactly ONE cycle

Goal: Remove the one extra edge! 🎯

Union-Find vs DFS: βš–οΈ

Union-Find approach:
  βœ… O(n Γ— Ξ±(n)) β‰ˆ O(n) time
  βœ… Direct cycle detection
  βœ… No graph building
  βœ… Clean and elegant
  ⭐ Recommended!

DFS approach:
  ❌ O(n²) time
  βœ… More intuitive
  βœ… Explicit path checking
  πŸ“Š Good for understanding

For this problem:
  Union-Find is superior! πŸ†

But DFS helps build intuition:
  "Is there already a path between u and v?"
  If yes β†’ adding edge creates cycle!

Union-Find answers same question faster! ⚑

πŸŽͺ Memory Aid

"Check before you connect" πŸ”
"Same root means cycle direct" πŸ”—
"Return edge that makes it incorrect" ❌
"Union-Find detects it, perfect!" 🎯 ✨

⚠️ Don't Forget

  • 1-indexed nodes - use arrays of size n+1! πŸ”’
  • Check roots, not nodes - find(u) vs u! πŸ”
  • Return edge immediately - when cycle detected! ⚑
  • Process in order - for "last" requirement! πŸ“
  • Path compression - for O(Ξ±(n)) performance! πŸš€
  • Union by rank - keeps trees balanced! πŸ“Š

πŸŽ‰ Congratulations!

You've mastered Cycle Detection with Union-Find! 🌟

What You Learned:

βœ… Cycle detection - instant with Union-Find! πŸ”—
βœ… Same root check - if find(u) == find(v) β†’ cycle! πŸ”
βœ… Processing order - return last redundant naturally! πŸ“
βœ… Tree properties - n nodes need n-1 edges! 🌳
βœ… Pattern application - Union-Find for dynamic graphs! 🎯

The Beautiful Pattern:

Union-Find Cycle Detection Pattern:

For each edge [u, v]:
  1. Find roots: rootU = find(u), rootV = find(v)
  2. Check: if rootU == rootV β†’ CYCLE! ❌
  3. Otherwise: union(rootU, rootV) βœ…

This pattern solves:
  βœ… Redundant connection detection
  βœ… Tree validation
  βœ… MST construction (Kruskal's)
  βœ… Dynamic graph connectivity
  βœ… All cycle detection problems

Master this = Master Union-Find! πŸ”‘

Union-Find becomes more powerful with each problem! πŸš€
Cycle detection is your second superpower! πŸ’ͺ✨🎯