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:
-
Union-Find tracks connectivity: π After first two edges, all three nodes connected through node 1
-
Third edge creates cycle: β Nodes 2 and 3 already have path through 1
-
find() reveals connection: π Both have same root (1), so already in same set
-
Immediate detection: β‘ No need to search entire graph, Union-Find knows instantly!
-
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) π¦
π Related Problems & Pattern
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
Related LeetCode Problems π
Direct Applications:
-
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! π―
-
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! π―
-
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! π‘
-
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! πͺβ¨π―