Edges are sorted so that type 3 edges (usable by both Alice and Bob) come first, followed by type 1 (Alice only), then type 2 (Bob only). This prioritizes edges that benefit both.
💡 Sorting ensures we use the most valuable edges first, maximizing shared connectivity and minimizing redundant edges.
Line:edges.sort(key=lambda x: -x[0]) # type 3 first
💡 Ordering edges by type is crucial to optimize union operations and edge usage.
setup
Initialize Union-Find for Alice and Bob
Two union-find structures are created, one for Alice and one for Bob, each with 4 nodes initially disconnected.
💡 Separate union-finds track connectivity for Alice and Bob independently, allowing us to manage their traversable graphs.
💡 Separate union-finds enable parallel connectivity checks for two users.
traverse
Process first edge (3,1,2) - type 3 edge
Consider the edge of type 3 connecting nodes 1 and 2. Attempt union in both Alice's and Bob's union-finds.
💡 Type 3 edges benefit both Alice and Bob, so we try to union in both structures to maximize connectivity.
Line:union_a = uf_alice.union(u - 1, v - 1)
union_b = uf_bob.union(u - 1, v - 1)
if union_a or union_b:
used_edges += 1
💡 Using type 3 edges first connects both Alice and Bob efficiently.
traverse
Process second edge (3,2,3) - type 3 edge
Consider the type 3 edge connecting nodes 2 and 3. Attempt union in both Alice's and Bob's union-finds.
💡 This edge can connect node 3 to the existing component containing nodes 1 and 2 for both users.
Line:union_a = uf_alice.union(u - 1, v - 1)
union_b = uf_bob.union(u - 1, v - 1)
if union_a or union_b:
used_edges += 1
💡 Type 3 edges continue to efficiently merge components for both users.
traverse
Process third edge (1,1,3) - type 1 edge (Alice only)
Consider the type 1 edge connecting nodes 1 and 3. Attempt union in Alice's union-find only.
💡 Type 1 edges only affect Alice's connectivity; Bob's union-find is not changed.
Line:if uf_alice.union(u - 1, v - 1):
used_edges += 1
💡 Edges connecting already connected nodes are redundant and can be removed.
traverse
Process fourth edge (1,2,4) - type 1 edge (Alice only)
Consider the type 1 edge connecting nodes 2 and 4. Attempt union in Alice's union-find only.
💡 This edge can connect node 4 to Alice's existing component, improving her connectivity.
Line:if uf_alice.union(u - 1, v - 1):
used_edges += 1
💡 Type 1 edges help Alice connect all nodes independently of Bob.
traverse
Process fifth edge (1,1,2) - type 1 edge (Alice only)
Consider the type 1 edge connecting nodes 1 and 2. Attempt union in Alice's union-find only.
💡 This edge connects nodes already connected in Alice's union-find, so it is redundant.
Line:if uf_alice.union(u - 1, v - 1):
used_edges += 1
💡 Redundant edges do not contribute to connectivity and can be removed.
traverse
Process sixth edge (2,3,4) - type 2 edge (Bob only)
Consider the type 2 edge connecting nodes 3 and 4. Attempt union in Bob's union-find only.
💡 This edge can connect node 4 to Bob's existing component, improving his connectivity.
Line:if uf_bob.union(u - 1, v - 1):
used_edges += 1
💡 Type 2 edges help Bob connect all nodes independently of Alice.
compare
Check full connectivity for Alice and Bob
Check if both Alice and Bob have only one connected component, meaning the graph is fully traversable for both.
💡 Full connectivity means all nodes are reachable by both Alice and Bob, so we can stop processing edges.
Line:if uf_alice.count == 1 and uf_bob.count == 1:
break
💡 Early termination saves unnecessary processing once full connectivity is achieved.
reconstruct
Calculate and return number of removable edges
Calculate removable edges as total edges minus used edges. Return 2 as the answer.
💡 Edges not used in union operations are redundant and can be removed without losing full traversability.
Line:return len(edges) - used_edges
💡 The answer represents the maximum number of edges that can be removed while keeping the graph fully traversable.
class UnionFind:
def __init__(self, n):
self.parent = list(range(n)) # STEP 2
self.rank = [0] * n
self.count = n
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]
return x
def union(self, x, y):
rootX = self.find(x)
rootY = self.find(y)
if rootX == rootY:
return False
if self.rank[rootX] < self.rank[rootY]:
self.parent[rootX] = rootY
elif self.rank[rootX] > self.rank[rootY]:
self.parent[rootY] = rootX
else:
self.parent[rootY] = rootX
self.rank[rootX] += 1
self.count -= 1
return True
def max_num_edges_to_remove_sorted(n, edges):
edges.sort(key=lambda x: -x[0]) # STEP 1
uf_alice = UnionFind(n) # STEP 2
uf_bob = UnionFind(n) # STEP 2
used_edges = 0
for t, u, v in edges: # STEP 3-8
if t == 3:
union_a = uf_alice.union(u - 1, v - 1) # STEP 3,4
union_b = uf_bob.union(u - 1, v - 1) # STEP 3,4
if union_a or union_b:
used_edges += 1
elif t == 1:
if uf_alice.union(u - 1, v - 1): # STEP 5,6,7
used_edges += 1
else: # t == 2
if uf_bob.union(u - 1, v - 1): # STEP 8
used_edges += 1
if uf_alice.count == 1 and uf_bob.count == 1: # STEP 9
break
if uf_alice.count == 1 and uf_bob.count == 1: # STEP 10
return len(edges) - used_edges
else:
return -1
📊
Remove Max Edges to Keep Graph Traversable - Watch the Algorithm Execute, Step by Step
Watching each union operation and the connectivity status helps understand how union-find efficiently manages connectivity and why some edges are redundant.
Step 1/10
·Active fill★Answer cell
Visiting: none
1
2
3
4
Queue / Stack
3-1-23-2-31-1-31-2-41-1-22-3-4
Visiting: none
1
2
3
4
Queue / Stack
3-1-23-2-31-1-31-2-41-1-22-3-4
Visiting: none
Edge: 1 → 2
1
2
3
4
Queue / Stack
3-2-31-1-31-2-41-1-22-3-4
Visiting: none
Edge: 2 → 3
1
2
3
4
Queue / Stack
1-1-31-2-41-1-22-3-4
Visiting: none
Edge: 1 → 3
1
2
3
4
Queue / Stack
1-2-41-1-22-3-4
Visiting: none
Edge: 2 → 4
1
2
3
4
Queue / Stack
1-1-22-3-4
Visiting: none
Edge: 1 → 2
1
2
3
4
Queue / Stack
2-3-4
Visiting: none
Edge: 3 → 4
1
2
3
4
Visiting: none
1
2
3
4
Visiting: none
1
2
3
4
Key Takeaways
✓ Prioritizing type 3 edges first maximizes shared connectivity for both Alice and Bob.
This insight is hard to see from code alone because the sorting step's impact on union operations is subtle without visualization.
✓ Union-find efficiently tracks connectivity and detects redundant edges that can be removed.
Visualizing union-find merges clarifies why some edges do not reduce component counts and are thus removable.
✓ Early termination when both users are fully connected saves unnecessary processing.
Seeing the break condition in action helps understand performance optimization in the algorithm.
Practice
(1/5)
1. You are given a list of user accounts, each with a username and multiple email addresses. Some accounts may belong to the same user if they share at least one email address. Which algorithmic approach best guarantees merging all accounts belonging to the same user efficiently?
easy
A. Greedy approach that merges accounts by comparing usernames only
B. Dynamic programming to find the longest common subsequence of emails
C. Brute force nested loops checking every pair of accounts for email overlap
D. Union-Find data structure to group emails that appear in multiple accounts
Solution
Step 1: Understand the problem structure
The problem requires grouping accounts by shared emails, which naturally forms connected components in a graph where emails are nodes.
Step 2: Identify the suitable algorithm
Union-Find efficiently merges sets of emails that belong to the same user, handling multiple overlaps without redundant comparisons.
Final Answer:
Option D -> Option D
Quick Check:
Union-Find is the classic pattern for merging overlapping sets [OK]
Hint: Accounts merge is a connected components problem [OK]
Common Mistakes:
Merging by username only misses shared emails
Brute force is too slow for large inputs
2. Consider the following Python code implementing Union-Find with path compression and union by rank to find a redundant connection. Given the input edges [[1, 2], [1, 3], [2, 3]], what is the output of the function?
easy
A. [1, 2]
B. [1, 3]
C. [2, 3]
D. None (no redundant edge)
Solution
Step 1: Trace union operations for edges [1,2], [1,3], [2,3]
Union(1,2) succeeds, Union(1,3) succeeds, Union(2,3) fails because 2 and 3 are already connected.
Step 2: Identify returned edge
The first edge that causes union to fail is [2,3], so it is returned as redundant.
Final Answer:
Option C -> Option C
Quick Check:
Union-Find detects cycle at edge [2,3] [OK]
Hint: Union fails when both nodes share the same root [OK]
Common Mistakes:
Returning first edge instead of the one causing cycle
Confusing union success/failure
Off-by-one indexing errors
3. What is the time complexity of the optimal union-find solution for the Most Stones Removed with Same Row or Column problem, given n stones?
medium
A. O(n α(n)) where α is the inverse Ackermann function, due to union-find operations
B. O(n^2) due to checking all pairs of stones
C. O(n log n) because of sorting stones by coordinates
D. O(n) since each stone is processed once without extra overhead
Solution
Step 1: Analyze union-find operations
Each union/find operation runs in nearly O(1) amortized time, specifically O(α(n)) where α is inverse Ackermann.
Step 2: Count total operations
We perform at most 2n find operations and n union operations, total O(n α(n)).
Final Answer:
Option A -> Option A
Quick Check:
Union-find with path compression and rank -> O(n α(n)) [OK]
Hint: Union-find with path compression -> near-linear time [OK]
Common Mistakes:
Assuming O(n^2) from brute force pairwise checks
Confusing sorting complexity with union-find
Ignoring inverse Ackermann function in complexity
4. Examine the following buggy Union-Find code snippet for the network connectivity problem. Which line contains the subtle bug that can cause incorrect results on some inputs?
medium
A. Line with 'return self.find(self.parent[x])' in find method
B. Line 'self.count = n' in __init__ method
C. Line 'self.rank[rootX] += 1' in union method
D. Line 'self.parent[rootX] = rootY' in union method
Solution
Step 1: Identify path compression implementation
The find method should assign the parent during recursion: self.parent[x] = self.find(self.parent[x]) to flatten the tree.
Step 2: Understand impact of missing assignment
Without path compression, find operations become slower and can cause TLE or incorrect component counts on large inputs.
Final Answer:
Option A -> Option A
Quick Check:
Missing path compression assignment is a classic subtle bug [OK]
Hint: Path compression requires assignment to parent during find [OK]
Common Mistakes:
Forgetting path compression assignment
Misplacing rank increment
Incorrectly decrementing count
5. Suppose you want to find a minimum spanning forest (MST for each connected component) in a graph that may be disconnected. Which modification to the Kruskal's algorithm with union-find is necessary to correctly handle this scenario?
hard
A. Remove the break condition and process all edges to connect all components separately
B. Use DFS to find connected components first, then run Kruskal on each separately
C. Stop after adding n-1 edges, as usual, since MST covers all nodes
D. Add edges only if they connect nodes in the same component to avoid cycles
Solution
Step 1: Understand MST vs MST forest
In disconnected graphs, MST is a forest; stopping after n-1 edges assumes connectivity, which may not hold.
Step 2: Modify Kruskal to process all edges
Removing the break condition allows union-find to connect all components separately, building MST forest.
Final Answer:
Option A -> Option A
Quick Check:
Process all edges to cover disconnected components [OK]
Hint: Don't stop early if graph is disconnected; build MST forest instead [OK]
Common Mistakes:
Stopping early assuming graph is connected
Running DFS first adds unnecessary complexity
Misusing union-find to connect nodes in same component
Recommended
Ready to prove it?
Pass the test and this pattern moves unknownstrongwhile earning +1 Grip toward your certificate.