LeetCode 1579 - Remove Max Number of Edges to Keep Graph Fully Traversable
The problem presents an undirected graph with n nodes and three types of edges: Type 1 for Alice, Type 2 for Bob, and Ty
Difficulty: 🔴 Hard
Topics: Union-Find, Graph Theory
Solution
Problem Understanding
The problem presents an undirected graph with n nodes and three types of edges: Type 1 for Alice, Type 2 for Bob, and Type 3 for both Alice and Bob. The input edges is an array where each element [type, u, v] represents a bidirectional edge connecting nodes u and v of the specified type. The goal is to determine the maximum number of edges that can be removed while still allowing both Alice and Bob to traverse the entire graph. If it is impossible for either Alice or Bob to traverse all nodes, we must return -1.
In simpler terms, we need to maintain connectivity for both Alice and Bob simultaneously, and edges that do not contribute to this connectivity can be removed. Since Alice and Bob share Type 3 edges, these edges are the most valuable because they count toward connectivity for both. Type 1 and Type 2 edges are only useful to their respective users. The constraints, with n up to 10^5 and edges up to 3 * n * (n - 1) / 2, indicate that brute-force solutions are infeasible and that an efficient Union-Find (Disjoint Set Union) approach is appropriate.
Important edge cases include graphs where connectivity is impossible without all edges, graphs where all edges are Type 3, and disconnected components for one user but not the other.
Approaches
Brute Force Approach: One could attempt to remove each edge one by one and check if the resulting graph is still fully traversable by both Alice and Bob using BFS or DFS. This approach is correct but extremely inefficient. For each edge removal, checking connectivity is O(n + m), leading to overall complexity of O(m*(n+m)), which is not feasible for the upper constraints.
Optimal Approach: The key insight is to use Union-Find to track connectivity. We process Type 3 edges first, because they contribute to both Alice and Bob's connectivity. Then we process Type 1 edges for Alice and Type 2 edges for Bob separately. If an edge connects nodes already in the same connected component, it is redundant and can be removed. After processing all edges, we check if Alice and Bob are each connected (i.e., their respective Union-Find structures have only one component). If so, the count of redundant edges gives the maximum number we can remove. Otherwise, return -1.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(m*(n+m)) | O(n) | Remove edges one by one and check connectivity using BFS/DFS |
| Optimal | O(m * α(n)) | O(n) | Use Union-Find, process Type 3 edges first, then Type 1 and Type 2 |
Here, α(n) is the inverse Ackermann function, which is nearly constant for all practical purposes.
Algorithm Walkthrough
- Initialize two Union-Find structures, one for Alice and one for Bob, each containing
nnodes. - Sort or separate edges into Type 3, Type 1, and Type 2 lists.
- Initialize a counter
removed = 0for redundant edges. - Process Type 3 edges first: For each edge
(u, v), ifuandvare already connected in Alice's Union-Find (and Bob's, since Type 3 affects both), incrementremoved. Otherwise, unionuandvin both Alice's and Bob's Union-Find. - Process Type 1 edges for Alice: If
uandvare already connected in Alice's Union-Find, incrementremoved. Otherwise, union them. - Process Type 2 edges for Bob: Similarly, union if disconnected, otherwise increment
removed. - After processing all edges, check if both Alice's and Bob's Union-Find structures have exactly one connected component. If not, return
-1. - Return the value of
removed.
Why it works: By prioritizing Type 3 edges, we maximize shared connectivity and minimize the need for separate Type 1 or Type 2 edges. Redundant edges do not contribute to connectivity and can safely be removed. Union-Find guarantees correct tracking of connected components efficiently.
Python Solution
from typing import List
class UnionFind:
def __init__(self, n: int):
self.parent = list(range(n + 1))
self.rank = [0] * (n + 1)
self.components = n
def find(self, x: int) -> int:
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x: int, y: int) -> bool:
xr, yr = self.find(x), self.find(y)
if xr == yr:
return False
if self.rank[xr] < self.rank[yr]:
self.parent[xr] = yr
elif self.rank[xr] > self.rank[yr]:
self.parent[yr] = xr
else:
self.parent[yr] = xr
self.rank[xr] += 1
self.components -= 1
return True
class Solution:
def maxNumEdgesToRemove(self, n: int, edges: List[List[int]]) -> int:
alice_uf = UnionFind(n)
bob_uf = UnionFind(n)
removed = 0
# Process Type 3 edges first
for t, u, v in edges:
if t == 3:
if not alice_uf.union(u, v):
removed += 1
else:
bob_uf.union(u, v)
# Process Type 1 edges for Alice
for t, u, v in edges:
if t == 1:
if not alice_uf.union(u, v):
removed += 1
# Process Type 2 edges for Bob
for t, u, v in edges:
if t == 2:
if not bob_uf.union(u, v):
removed += 1
if alice_uf.components != 1 or bob_uf.components != 1:
return -1
return removed
Explanation: We maintain separate Union-Find structures for Alice and Bob. Type 3 edges are applied to both. Union operations return False if the nodes are already connected, allowing us to count redundant edges. After all edges are processed, we confirm that each graph is fully connected.
Go Solution
package main
type UnionFind struct {
parent []int
rank []int
components int
}
func NewUnionFind(n int) *UnionFind {
parent := make([]int, n+1)
rank := make([]int, n+1)
for i := 0; i <= n; i++ {
parent[i] = i
}
return &UnionFind{parent, rank, n}
}
func (uf *UnionFind) Find(x int) int {
if uf.parent[x] != x {
uf.parent[x] = uf.Find(uf.parent[x])
}
return uf.parent[x]
}
func (uf *UnionFind) Union(x, y int) bool {
xr, yr := uf.Find(x), uf.Find(y)
if xr == yr {
return false
}
if uf.rank[xr] < uf.rank[yr] {
uf.parent[xr] = yr
} else if uf.rank[xr] > uf.rank[yr] {
uf.parent[yr] = xr
} else {
uf.parent[yr] = xr
uf.rank[xr]++
}
uf.components--
return true
}
func maxNumEdgesToRemove(n int, edges [][]int) int {
aliceUF := NewUnionFind(n)
bobUF := NewUnionFind(n)
removed := 0
for _, edge := range edges {
if edge[0] == 3 {
if !aliceUF.Union(edge[1], edge[2]) {
removed++
} else {
bobUF.Union(edge[1], edge[2])
}
}
}
for _, edge := range edges {
if edge[0] == 1 {
if !aliceUF.Union(edge[1], edge[2]) {
removed++
}
} else if edge[0] == 2 {
if !bobUF.Union(edge[1], edge[2]) {
removed++
}
}
}
if aliceUF.components != 1 || bobUF.components != 1 {
return -1
}
return removed
}
Go-specific notes: We define a UnionFind struct and constructor NewUnionFind. Slices handle the parent and rank arrays. Union returns a boolean indicating redundancy. Edge indices are accessed using edge[1] and edge[2], consistent with 1-based node indexing.
Worked Examples
Example 1: n = 4, edges = [[3,1,2],[3,2,3],[1,1,3],[1,2,4],[1,1,2],[2,3,4]]
| Step | Action | Alice UF | Bob UF | Removed |
|---|---|---|---|---|
| 1 | Type 3 edge [3,1, |