Hard#15798 min readGraphUnion-FindLeetCode

Remove Max Edges to Keep Graph Traversable

Union-Find with type-3 priority

Dual Union-Find. Process shared edges first (greedy).

Published Apr 16, 2026

Problem

Alice and Bob share an undirected graph of n nodes. There are three edge types:

  • Type 1 — usable by Alice only.
  • Type 2 — usable by Bob only.
  • Type 3 — usable by both.

Return the maximum number of edges you can remove so that both Alice and Bob can still independently traverse the entire graph. If the graph cannot be made fully traversable by both, return -1.

Input
n = 4, edges = [[3,1,2],[3,2,3],[1,1,3],[1,2,4],[1,1,2],[2,3,4]]
Output
2
Explanation
Remove edges [1,1,2] and [1,2,4]. Alice connects via type-3 + type-1 edges (1-2, 2-3, 1-3). Bob via type-3 + type-2 edges.
Input
n = 4, edges = [[3,1,2],[3,2,3],[1,1,4],[2,1,4]]
Output
0
Explanation
All four edges are needed. Removing any one disconnects either Alice or Bob.
Input
n = 4, edges = [[3,2,3],[1,1,2],[2,3,4]]
Output
-1
Explanation
Bob cannot reach all nodes — no type-2 or type-3 path connects node 1 to node 2.

Intuition

Maximize removals = minimize kept edges. An n-node connected graph needs at least n - 1 edges (a spanning tree). Alice needs her own spanning tree (type 1 + type 3) and Bob needs his (type 2 + type 3).

The trick: type-3 edges count for both trees simultaneously. Every type-3 edge used saves us from needing one type-1 and one type-2 edge. So greedily prefer type-3 edges.

Algorithm:

  1. Process all type-3 edges first with a shared union-find. Each successful union means one fewer edge needed in both Alice's and Bob's trees.
  2. Copy the union-find state into Alice's and Bob's separate DSUs.
  3. Process type-1 edges on Alice's DSU, type-2 on Bob's. Count how many unions succeed (each is a kept edge).
  4. If either DSU is not fully connected, return -1.
  5. Otherwise, return total_edges - kept_edges.

Approach

Brute Force

TimeO(2^E · E · α(V))
SpaceO(V)

Try all subsets of edges (exponential) and check connectivity for both. Not practical, but establishes correctness baseline.

impl Solution {
    pub fn max_num_edges_to_remove(n: i32, edges: Vec<Vec<i32>>) -> i32 {
        let n = n as usize;
        let m = edges.len();

        // Try removing as many edges as possible by checking all
        // 2^m subsets (only feasible for small m).
        let mut best = -1i32;
        'outer: for mask in 0..(1u64 << m) {
            let mut pa: Vec<usize> = (0..=n).collect();
            let mut pb: Vec<usize> = (0..=n).collect();
            fn find(p: &mut [usize], mut x: usize) -> usize {
                while p[x] != x { p[x] = p[p[x]]; x = p[x]; }
                x
            }
            fn union(p: &mut [usize], a: usize, b: usize) -> bool {
                let (ra, rb) = (find(p, a), find(p, b));
                if ra == rb { return false; }
                p[ra] = rb; true
            }
            for j in 0..m {
                if mask & (1u64 << j) == 0 { continue; }
                let (t, u, v) = (edges[j][0], edges[j][1] as usize, edges[j][2] as usize);
                if t == 1 || t == 3 { union(&mut pa, u, v); }
                if t == 2 || t == 3 { union(&mut pb, u, v); }
            }
            let ca = (1..=n).map(|i| find(&mut pa, i)).collect::<std::collections::HashSet<_>>().len();
            let cb = (1..=n).map(|i| find(&mut pb, i)).collect::<std::collections::HashSet<_>>().len();
            if ca == 1 && cb == 1 {
                let kept = (0..m).filter(|&j| mask & (1u64 << j) != 0).count() as i32;
                let removed = m as i32 - kept;
                if removed > best { best = removed; }
            }
        }
        best
    }
}
Rust · runnable

Union-Find with Type-3 Priority

TimeO(E · α(V))
SpaceO(V)
Recommended

Process type-3 edges on a shared DSU first. Then branch into Alice-only and Bob-only DSUs. Count total unions.

impl Solution {
    pub fn max_num_edges_to_remove(n: i32, edges: Vec<Vec<i32>>) -> i32 {
        let n = n as usize;
        let m = edges.len();

        let mut pa: Vec<usize> = (0..=n).collect();
        let mut ra = vec![0usize; n + 1];
        let mut ca = n;

        fn find(p: &mut [usize], mut x: usize) -> usize {
            while p[x] != x { p[x] = p[p[x]]; x = p[x]; }
            x
        }
        fn union(p: &mut [usize], r: &mut [usize], c: &mut usize, a: usize, b: usize) -> bool {
            let (ra2, rb2) = (find(p, a), find(p, b));
            if ra2 == rb2 { return false; }
            if r[ra2] < r[rb2] { p[ra2]= rb2; }
            else if r[ra2] > r[rb2] { p[rb2] = ra2; }
            else { p[rb2] = ra2; r[ra2] += 1; }
            *c -= 1;
            true
        }

        let mut kept = 0;

        // Phase 1: type-3 edges (shared).
        for e in &edges {
            if e[0] == 3 {
                if union(&mut pa, &mut ra, &mut ca, e[1] as usize, e[2] as usize) {
                    kept += 1;
                }
            }
        }

        // Copy state for Bob.
        let mut pb = pa.clone();
        let mut rb = ra.clone();
        let mut cb = ca;

        // Phase 2: type-1 (Alice only).
        for e in &edges {
            if e[0] == 1 {
                if union(&mut pa, &mut ra, &mut ca, e[1] as usize, e[2] as usize) {
                    kept += 1;
                }
            }
        }

        // Phase 3: type-2 (Bob only).
        for e in &edges {
            if e[0] == 2 {
                if union(&mut pb, &mut rb, &mut cb, e[1] as usize, e[2] as usize) {
                    kept += 1;
                }
            }
        }

        if ca != 1 || cb != 1 {
            return -1;
        }

        (m as i32) - kept
    }
}
Rust · runnable

Trace

n = 4, edges = [[3,1,2],[3,2,3],[1,1,3],[1,2,4],[1,1,2],[2,3,4]]:

phaseedgetypeactionkept
shared(1,2)3union1
shared(2,3)3union2
alice(1,3)1cycle (1,3 already connected)2
alice(2,4)1union3
alice(1,2)1cycle3
bob(3,4)2union4

After all phases: Alice has 1 component, Bob has 1 component. Kept = 4, total = 6, removed = 2.

Edge cases

  • All type-3 — one shared tree spans both. Remove m - (n - 1) edges.
  • No type-3 — Alice and Bob need independent spanning trees. Each needs n - 1 edges.
  • Disconnected even with all edges — return -1.
  • Parallel edges — the union-find naturally deduplicates; extra edges contribute to "removed".

Takeaway

Shared resources go first. The type-3-priority greedy is optimal because a shared edge is strictly more valuable (saves from both personal budgets). This greedy logic extends to real multi-tenant resource provisioning — shared infrastructure beats per-user infrastructure when capacity allows.

More in Advanced Graph