1579. Remove Max Number of Edges to Keep Graph Fully Traversable - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Union-Find (Disjoint Set Union) - Implementing DSU with path compression and union by rank for efficient component tracking
  • Graph Connectivity - Understanding when a graph is fully connected (single component)
  • Greedy Algorithms - Processing edges in optimal order to maximize removable edges
  • Multiple Data Structures - Maintaining separate Union-Find structures for different constraints

1. Disjoint Set Union

Intuition

We need to keep both Alice and Bob connected across all nodes while removing as many edges as possible.
Type 3 edges (usable by both) are most valuable since they count toward connectivity for both users simultaneously.
We use two separate Union-Find structures to track connectivity for Alice and Bob independently.
By processing type 3 edges first, we maximize their usage, then fill in gaps with type 1 (Alice-only) and type 2 (Bob-only) edges.

Algorithm

  1. Create two separate DSU (Disjoint Set Union) structures, one for Alice and one for Bob.
  2. First pass: process all type 3 edges. For each edge, attempt to union in both DSUs. Count it as used if it connects new components in either DSU.
  3. Second pass: process type 1 edges (Alice only) and type 2 edges (Bob only). For each edge, attempt to union in the appropriate DSU and count if successful.
  4. After processing all edges, check if both DSUs have all nodes connected (single component).
  5. If both are fully connected, return total_edges - used_edges. Otherwise, return -1.
class DSU:
    def __init__(self, n):
        self.n = n
        self.Parent = list(range(n + 1))
        self.Size = [1] * (n + 1)

    def find(self, node):
        if self.Parent[node] != node:
            self.Parent[node] = self.find(self.Parent[node])
        return self.Parent[node]

    def union(self, u, v):
        pu = self.find(u)
        pv = self.find(v)
        if pu == pv:
            return 0
        if self.Size[pu] < self.Size[pv]:
            pu, pv = pv, pu
        self.Size[pu] += self.Size[pv]
        self.Parent[pv] = pu
        self.n -= 1
        return 1

    def isConnected(self):
        return self.n == 1

class Solution:
    def maxNumEdgesToRemove(self, n: int, edges: List[List[int]]) -> int:
        alice, bob = DSU(n), DSU(n)
        cnt = 0

        for type, src, dst in edges:
            if type == 3:
                cnt += (alice.union(src, dst) | bob.union(src, dst))

        for type, src, dst in edges:
            if type == 1:
                cnt += alice.union(src, dst)
            elif type == 2:
                cnt += bob.union(src, dst)

        if alice.isConnected() and bob.isConnected():
            return len(edges) - cnt
        return -1

Time & Space Complexity

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

Where VV is the number of verticies and EE is the number of edges.


Common Pitfalls

Processing Type 3 Edges After Type 1 and Type 2

The order of processing matters significantly. Type 3 edges (usable by both Alice and Bob) should be processed first because they provide connectivity to both users with a single edge. If you process type 1 and type 2 edges first, you may use two separate edges to connect nodes that could have been connected by one type 3 edge, resulting in fewer removable edges.

Using a Single Union-Find Structure for Both Users

Alice and Bob have different connectivity requirements since type 1 edges only work for Alice and type 2 edges only work for Bob. Using a single DSU structure fails to track their independent connectivity. You must maintain two separate Union-Find structures and update them according to which edge types each user can traverse.

Incorrect Counting of Used Type 3 Edges

When processing a type 3 edge, it counts as "used" if it connects new components for either Alice or Bob (or both). A common mistake is counting it as used only if it helps both, or counting it twice. Use bitwise OR (|) on the union results: alice.union() | bob.union() returns 1 if the edge was useful for at least one user.

Not Verifying Full Connectivity for Both Users

After processing all edges, you must verify that both Alice's and Bob's graphs are fully connected (single component each). Returning the edge count without this check can give a valid-looking answer even when full traversal is impossible. If either DSU has more than one component, return -1.

Off-by-One Errors in Component Counting

The Union-Find structure should track the number of connected components, starting at n (number of nodes) and decreasing by 1 with each successful union. A graph is fully connected when exactly 1 component remains. Initializing the count incorrectly or not decrementing on successful unions leads to wrong connectivity checks.