Hard

题目描述

Alice 和 Bob 有一个包含 n 个节点的无向图,图中有三种类型的边:

  • 类型 1:只能被 Alice 遍历。
  • 类型 2:只能被 Bob 遍历。
  • 类型 3:Alice 和 Bob 都可以遍历。

给你一个数组 edges,其中 edges[i] = [typei, ui, vi] 表示节点 ui 和 vi 之间有一条类型为 typei 的双向边。请你找到可以删除的最大边数,使得删除这些边之后,图仍然可以被 Alice 和 Bob 完全遍历。如果从任何节点开始,Alice 和 Bob 都可以到达所有其他节点,则认为图可以被 Alice 和 Bob 完全遍历。

返回可以删除的最大边数,如果 Alice 和 Bob 无法完全遍历图,则返回 -1。

示例 1:

输入:n = 4, edges = [[3,1,2],[3,2,3],[1,1,3],[1,2,4],[1,1,2],[2,3,4]]
输出:2
解释:如果我们删除边 [1,1,2] 和 [1,1,3],图仍然可以被 Alice 和 Bob 完全遍历。删除任何其他的边都无法做到这一点。所以我们可以删除的最大边数是 2。

示例 2:

输入:n = 4, edges = [[3,1,2],[3,2,3],[1,1,4],[2,1,4]]
输出:0
解释:注意删除任何一条边都不能使图被 Alice 和 Bob 完全遍历。

示例 3:

输入:n = 4, edges = [[3,2,3],[1,1,2],[2,3,4]]
输出:-1
解释:在当前图中,Alice 无法从其他节点到达节点 4。同样,Bob 也无法到达节点 1。因此无法使图完全遍历。

提示:

  • 1 <= n <= 10^5
  • 1 <= edges.length <= min(10^5, 3 * n * (n - 1) / 2)
  • edges[i].length == 3
  • 1 <= typei <= 3
  • 1 <= ui < vi <= n
  • 所有 (typei, ui, vi) 都是不同的。

解题思路

这是一个图论问题,需要使用并查集(Union-Find)数据结构来解决。

核心思想:

  1. 反向思考:与其考虑删除多少边,不如考虑保留最少多少边能让图连通
  2. 对于连通图,最少需要 n-1 条边形成生成树
  3. Alice 和 Bob 各自都需要一个连通图,但可以共享类型 3 的边

算法步骤:

  1. 优先使用类型 3 的边:因为它们对 Alice 和 Bob 都有用,性价比最高
  2. 分别为 Alice 和 Bob 建立并查集
    • Alice 可以使用类型 1 和类型 3 的边
    • Bob 可以使用类型 2 和类型 3 的边
  3. 贪心策略
    • 首先处理类型 3 的边,在两个并查集中同时尝试连接
    • 然后处理类型 1 和类型 2 的边,分别在对应的并查集中连接
  4. 统计结果
    • 记录实际使用的边数
    • 检查两个图是否都连通(连通分量数是否为 1)
    • 返回总边数减去使用的边数

时间复杂度分析:

  • 并查集操作近似 O(1)
  • 总体时间复杂度为 O(E),其中 E 是边数

这个算法的关键在于理解类型 3 的边的重要性,以及使用并查集高效地维护连通性。

代码实现

class Solution {
public:
    class UnionFind {
    public:
        vector<int> parent;
        int components;
        
        UnionFind(int n) {
            parent.resize(n + 1);
            for (int i = 1; i <= n; i++) {
                parent[i] = i;
            }
            components = n;
        }
        
        int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }
        
        bool unite(int x, int y) {
            int px = find(x), py = find(y);
            if (px == py) return false;
            parent[px] = py;
            components--;
            return true;
        }
    };
    
    int maxNumEdgesToRemove(int n, vector<vector<int>>& edges) {
        UnionFind alice(n), bob(n);
        int usedEdges = 0;
        
        // First process type 3 edges (can be used by both)
        for (auto& edge : edges) {
            if (edge[0] == 3) {
                bool aliceUsed = alice.unite(edge[1], edge[2]);
                bool bobUsed = bob.unite(edge[1], edge[2]);
                if (aliceUsed || bobUsed) {
                    usedEdges++;
                }
            }
        }
        
        // Process type 1 edges (Alice only)
        for (auto& edge : edges) {
            if (edge[0] == 1 && alice.unite(edge[1], edge[2])) {
                usedEdges++;
            }
        }
        
        // Process type 2 edges (Bob only)
        for (auto& edge : edges) {
            if (edge[0] == 2 && bob.unite(edge[1], edge[2])) {
                usedEdges++;
            }
        }
        
        // Check if both graphs are fully connected
        if (alice.components != 1 || bob.components != 1) {
            return -1;
        }
        
        return edges.size() - usedEdges;
    }
};
class Solution:
    def maxNumEdgesToRemove(self, n: int, edges: List[List[int]]) -> int:
        class UnionFind:
            def __init__(self, n):
                self.parent = list(range(n + 1))
                self.components = n
            
            def find(self, x):
                if self.parent[x] != x:
                    self.parent[x] = self.find(self.parent[x])
                return self.parent[x]
            
            def unite(self, x, y):
                px, py = self.find(x), self.find(y)
                if px == py:
                    return False
                self.parent[px] = py
                self.components -= 1
                return True
        
        alice = UnionFind(n)
        bob = UnionFind(n)
        used_edges = 0
        
        # First process type 3 edges (can be used by both)
        for edge_type, u, v in edges:
            if edge_type == 3:
                alice_used = alice.unite(u, v)
                bob_used = bob.unite(u, v)
                if alice_used or bob_used:
                    used_edges += 1
        
        # Process type 1 edges (Alice only)
        for edge_type, u, v in edges:
            if edge_type == 1 and alice.unite(u, v):
                used_edges += 1
        
        # Process type 2 edges (Bob only)
        for edge_type, u, v in edges:
            if edge_type == 2 and bob.unite(u, v):
                used_edges += 1
        
        # Check if both graphs are fully connected
        if alice.components != 1 or bob.components != 1:
            return -1
        
        return len(edges) - used_edges
public class Solution {
    public class UnionFind {
        private int[] parent;
        public int Components { get; private set; }
        
        public UnionFind(int n) {
            parent = new int[n + 1];
            for (int i = 1; i <= n; i++) {
                parent[i] = i;
            }
            Components = n;
        }
        
        public int Find(int x) {
            if (parent[x] != x) {
                parent[x] = Find(parent[x]);
            }
            return parent[x];
        }
        
        public bool Unite(int x, int y) {
            int px = Find(x), py = Find(y);
            if (px == py) return false;
            parent[px] = py;
            Components--;
            return true;
        }
    }
    
    public int MaxNumEdgesToRemove(int n, int[][] edges) {
        UnionFind alice = new UnionFind(n);
        UnionFind bob = new UnionFind(n);
        int usedEdges = 0;
        
        // First process type 3 edges (can be used by both)
        foreach (var edge in edges) {
            if (edge[0] == 3) {
                bool aliceUsed = alice.Unite(edge[1], edge[2]);
                bool bobUsed = bob.Unite(edge[1], edge[2]);
                if (aliceUsed || bobUsed) {
                    usedEdges++;
                }
            }
        }
        
        // Process type 1 edges (Alice only)
        foreach (var edge in edges) {
            if (edge[0] == 1 && alice.Unite(edge[1], edge[2])) {
                usedEdges++;
            }
        }
        
        // Process type 2 edges (Bob only)
        foreach (var edge in edges) {
            if (edge[0] == 2 && bob.Unite(edge[1], edge[2])) {
                usedEdges++;
            }
        }
        
        // Check if both graphs are fully connected
        if (alice.Components != 1 || bob.Components != 1) {
            return -1;
        }
        
        return edges.Length - usedEdges;
    }
}
var maxNumEdgesToRemove = function(n, edges) {
    class UnionFind {
        constructor(n) {
            this.parent = Array.from({length: n + 1}, (_, i) => i);
            this.components = n;
        }
        
        find(x) {
            if (this.parent[x] !== x) {
                this.parent[x] = this.find(this.parent[x]);
            }
            return this.parent[x];
        }
        
        union(x, y) {
            const px = this.find(x);
            const py = this.find(y);
            if (px !== py) {
                this.parent[px] = py;
                this.components--;
                return true;
            }
            return false;
        }
        
        isConnected() {
            return this.components === 1;
        }
    }
    
    const alice = new UnionFind(n);
    const bob = new UnionFind(n);
    let edgesUsed = 0;
    
    // Sort edges to process type 3 first
    edges.sort((a, b) => b[0] - a[0]);
    
    for (const [type, u, v] of edges) {
        if (type === 3) {
            const aliceUnion = alice.union(u, v);
            const bobUnion = bob.union(u, v);
            if (aliceUnion || bobUnion) {
                edgesUsed++;
            }
        } else if (type === 1) {
            if (alice.union(u, v)) {
                edgesUsed++;
            }
        } else if (type === 2) {
            if (bob.union(u, v)) {
                edgesUsed++;
            }
        }
    }
    
    if (alice.isConnected() && bob.isConnected()) {
        return edges.length - edgesUsed;
    }
    
    return -1;
};

复杂度分析

复杂度类型分析
时间复杂度O(E × α(N)),其中 E 是边数,N 是节点数,α 是反阿克曼函数,在实际情况下可视为常数
空间复杂度O(N),用于存储两个并查集的父节点数组