Alice 和 Bob 共有一个无向图,其中包含 n 个节点和 3 种类型的边:
类型 1:只能由 Alice 遍历。
类型 2:只能由 Bob 遍历。
类型 3:Alice 和 Bob 都可以遍历。
给你一个数组 edges
,其中 edges[i] = [typei, ui, vi]
表示节点 ui
和 vi
之间存在类型为typei
的双向边。请你在保证图仍能够被 Alice和 Bob 完全遍历的前提下,找出可以删除的最大边数。如果从任何节点开始,Alice 和 Bob 都可以到达所有其他节点,则认为图是可以完全遍历的。
返回可以删除的最大边数,如果 Alice 和 Bob 无法完全遍历图,则返回 -1 。
示例 1:
![](https://assets.leetcode-cn.com/aliyun-lc- upload/uploads/2020/09/06/5510ex1.png)
**输入:** 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:
![](https://assets.leetcode-cn.com/aliyun-lc- upload/uploads/2020/09/06/5510ex2.png)
**输入:** n = 4, edges = [[3,1,2],[3,2,3],[1,1,4],[2,1,4]]
**输出:** 0
**解释:** 注意,删除任何一条边都会使 Alice 和 Bob 无法完全遍历这个图。
示例 3:
![](https://assets.leetcode-cn.com/aliyun-lc- upload/uploads/2020/09/06/5510ex3.png)
**输入:** 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 <= edges[i][0] <= 3
1 <= edges[i][1] < edges[i][2] <= n
所有元组 (typei, ui, vi)
互不相同
方法一:并查集 思路与算法
我们称类型 1, 2, 3 的边分别为「Alice 独占边」「Bob 独占边」以及「公共边」。
首先我们需要思考什么样的图是可以被 Alice 和 Bob 完全遍历的。对于 Alice 而言,她可以经过的边是「Alice 独占边」以及「公共边」,由于她需要能够从任意节点到达任意节点,那么就说明:
当图中仅有「Alice 独占边」以及「公共边」时,整个图是连通的,即整个图只包含一个连通分量。
同理,对于 Bob 而言,当图中仅有「Bob 独占边」以及「公共边」时,整个图也要是连通的。
由于题目描述中希望我们删除最多数目 的边,这等价于保留最少数目 的边。换句话说,我们可以从一个仅包含 n 个节点(而没有边)的无向图开始,逐步添加边,使得满足上述的要求。
那么我们应该按照什么策略来添加边呢?直觉告诉我们,「公共边」的重要性大于「Alice 独占边」以及「Bob 独占边」,因为「公共边」是 Alice 和 Bob 都可以使用的,而他们各自的独占边却不能给对方使用。「公共边」的重要性也是可以证明的:
对于一条连接了两个不同 的连通分量的「公共边」而言,如果我们不保留这条公共边,那么 Alice 和 Bob 就无法往返这两个连通分量,即他们分别需要使用各自的独占边。因此,Alice 需要一条连接这两个连通分量的独占边,Bob 同样也需要一条连接这两个连通分量的独占边,那么一共需要两条边,这就严格不优于直接使用一条连接这两个连通分量的「公共边」了。
因此,我们可以遵从优先添加「公共边」的策略。具体地,我们遍历每一条「公共边」,对于其连接的的两个节点:
这就提示了我们使用并查集 来维护整个图的连通性,上述的策略只需要用到并查集的「查询」和「合并」这两个最基础的操作。
在处理完了所有的「公共边」之后,我们需要处理他们各自的独占边,而方法也与添加「公共边」类似。我们将当前的并查集复制一份,一份交给 Alice,一份交给 Bob。随后 Alice 不断地向并查集中添加「Alice 独占边」,Bob 不断地向并查集中添加「Bob 独占边」。在处理完了所有的独占边之后,如果这两个并查集都只包含一个连通分量,那么就说明 Alice 和 Bob 都可以遍历整个无向图。
细节
在使用并查集进行合并的过程中,我们每遇到一次失败的合并操作(即需要合并的两个点属于同一个连通分量),那么就说明当前这条边可以被删除,将答案增加 1。
代码
[sol1-C++] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 class UnionFind {public : vector<int > parent; vector<int > size; int n; int setCount; public : UnionFind (int _n): n (_n), setCount (_n), parent (_n), size (_n, 1 ) { iota (parent.begin (), parent.end (), 0 ); } int findset (int x) { return parent[x] == x ? x : parent[x] = findset (parent[x]); } bool unite (int x, int y) { x = findset (x); y = findset (y); if (x == y) { return false ; } if (size[x] < size[y]) { swap (x, y); } parent[y] = x; size[x] += size[y]; --setCount; return true ; } bool connected (int x, int y) { x = findset (x); y = findset (y); return x == y; } }; class Solution {public : int maxNumEdgesToRemove (int n, vector<vector<int >>& edges) { UnionFind ufa (n) , ufb (n) ; int ans = 0 ; for (auto & edge: edges) { --edge[1 ]; --edge[2 ]; } for (const auto & edge: edges) { if (edge[0 ] == 3 ) { if (!ufa.unite (edge[1 ], edge[2 ])) { ++ans; } else { ufb.unite (edge[1 ], edge[2 ]); } } } for (const auto & edge: edges) { if (edge[0 ] == 1 ) { if (!ufa.unite (edge[1 ], edge[2 ])) { ++ans; } } else if (edge[0 ] == 2 ) { if (!ufb.unite (edge[1 ], edge[2 ])) { ++ans; } } } if (ufa.setCount != 1 || ufb.setCount != 1 ) { return -1 ; } return ans; } };
[sol1-Java] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 class Solution { public int maxNumEdgesToRemove (int n, int [][] edges) { UnionFind ufa = new UnionFind (n); UnionFind ufb = new UnionFind (n); int ans = 0 ; for (int [] edge : edges) { --edge[1 ]; --edge[2 ]; } for (int [] edge : edges) { if (edge[0 ] == 3 ) { if (!ufa.unite(edge[1 ], edge[2 ])) { ++ans; } else { ufb.unite(edge[1 ], edge[2 ]); } } } for (int [] edge : edges) { if (edge[0 ] == 1 ) { if (!ufa.unite(edge[1 ], edge[2 ])) { ++ans; } } else if (edge[0 ] == 2 ) { if (!ufb.unite(edge[1 ], edge[2 ])) { ++ans; } } } if (ufa.setCount != 1 || ufb.setCount != 1 ) { return -1 ; } return ans; } } class UnionFind { int [] parent; int [] size; int n; int setCount; public UnionFind (int n) { this .n = n; this .setCount = n; this .parent = new int [n]; this .size = new int [n]; Arrays.fill(size, 1 ); for (int i = 0 ; i < n; ++i) { parent[i] = i; } } public int findset (int x) { return parent[x] == x ? x : (parent[x] = findset(parent[x])); } public boolean unite (int x, int y) { x = findset(x); y = findset(y); if (x == y) { return false ; } if (size[x] < size[y]) { int temp = x; x = y; y = temp; } parent[y] = x; size[x] += size[y]; --setCount; return true ; } public boolean connected (int x, int y) { x = findset(x); y = findset(y); return x == y; } }
[sol1-Python3] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 class UnionFind : def __init__ (self, n: int ): self.parent = list (range (n)) self.size = [1 ] * n self.n = n self.setCount = n def findset (self, x: int ) -> int : if self.parent[x] == x: return x self.parent[x] = self.findset(self.parent[x]) return self.parent[x] def unite (self, x: int , y: int ) -> bool : x, y = self.findset(x), self.findset(y) if x == y: return False if self.size[x] < self.size[y]: x, y = y, x self.parent[y] = x self.size[x] += self.size[y] self.setCount -= 1 return True def connected (self, x: int , y: int ) -> bool : x, y = self.findset(x), self.findset(y) return x == y class Solution : def maxNumEdgesToRemove (self, n: int , edges: List [List [int ]] ) -> int : ufa, ufb = UnionFind(n), UnionFind(n) ans = 0 for edge in edges: edge[1 ] -= 1 edge[2 ] -= 1 for t, u, v in edges: if t == 3 : if not ufa.unite(u, v): ans += 1 else : ufb.unite(u, v) for t, u, v in edges: if t == 1 : if not ufa.unite(u, v): ans += 1 elif t == 2 : if not ufb.unite(u, v): ans += 1 if ufa.setCount != 1 or ufb.setCount != 1 : return -1 return ans
[sol1-JavaScript] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 class UnionFind { constructor (n) { this .parent = new Array (n).fill (0 ).map ((element, index ) => index); this .size = new Array (n).fill (1 ); this .setCount = n; } findset (x) { if (this .parent [x] === x) { return x; } this .parent [x] = this .findset (this .parent [x]); return this .parent [x]; } unite (a, b) { let x = this .findset (a), y = this .findset (b); if (x === y) { return false ; } if (this .size [x] < this .size [y]) { [x, y] = [y, x]; } this .parent [y] = x; this .size [x] += this .size [y]; this .setCount -= 1 ; return true ; } connected (a, b) { const x = this .findset (a), y = this .findset (b); return x === y; } } var maxNumEdgesToRemove = function (n, edges ) { const ufa = new UnionFind (n), ufb = new UnionFind (n); let ans = 0 ; for (const edge of edges) { edge[1 ] -= 1 ; edge[2 ] -= 1 ; } for (const [t, u, v] of edges) { if (t === 3 ) { if (!ufa.unite (u, v)) { ans += 1 ; } else { ufb.unite (u, v); } } } for (const [t, u, v] of edges) { if (t === 1 ) { if (!ufa.unite (u, v)) { ans += 1 ; } } else if (t === 2 ) { if (!ufb.unite (u, v)) { ans += 1 ; } } } if (ufa.setCount !== 1 || ufb.setCount !== 1 ) { return -1 ; } return ans; };
[sol1-Golang] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 type unionFind struct { parent, size []int setCount int } func newUnionFind (n int ) *unionFind { parent := make ([]int , n) size := make ([]int , n) for i := range parent { parent[i] = i size[i] = 1 } return &unionFind{parent, size, 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 { fx, fy := uf.find(x), uf.find(y) if fx == fy { return false } if uf.size[fx] < uf.size[fy] { fx, fy = fy, fx } uf.size[fx] += uf.size[fy] uf.parent[fy] = fx uf.setCount-- return true } func (uf *unionFind) inSameSet(x, y int ) bool { return uf.find(x) == uf.find(y) } func maxNumEdgesToRemove (n int , edges [][]int ) int { ans := len (edges) alice, bob := newUnionFind(n), newUnionFind(n) for _, e := range edges { x, y := e[1 ]-1 , e[2 ]-1 if e[0 ] == 3 && (!alice.inSameSet(x, y) || !bob.inSameSet(x, y)) { alice.union(x, y) bob.union(x, y) ans-- } } uf := [2 ]*unionFind{alice, bob} for _, e := range edges { if tp := e[0 ]; tp < 3 && uf[tp-1 ].union(e[1 ]-1 , e[2 ]-1 ) { ans-- } } if alice.setCount > 1 || bob.setCount > 1 { return -1 } return ans }
[sol1-C] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 void swap (int * a, int * b) { int tmp = *a; *a = *b, *b = tmp; } struct DisjointSetUnion { int *f, *size; int n, setCount; }; void initDSU (struct DisjointSetUnion* obj, int n) { obj->f = malloc (sizeof (int ) * n); obj->size = malloc (sizeof (int ) * n); obj->n = n; obj->setCount = n; for (int i = 0 ; i < n; i++) { obj->f[i] = i; obj->size[i] = 1 ; } } int find (struct DisjointSetUnion* obj, int x) { return obj->f[x] == x ? x : (obj->f[x] = find(obj, obj->f[x])); } int unionSet (struct DisjointSetUnion* obj, int x, int y) { int fx = find(obj, x), fy = find(obj, y); if (fx == fy) { return false ; } if (obj->size[fx] < obj->size[fy]) { swap(&fx, &fy); } obj->size[fx] += obj->size[fy]; obj->f[fy] = fx; obj->setCount--; return true ; } int maxNumEdgesToRemove (int n, int ** edges, int edgesSize, int * edgesColSize) { struct DisjointSetUnion * ufa = malloc (sizeof (struct DisjointSetUnion)); initDSU(ufa, n); struct DisjointSetUnion * ufb = malloc (sizeof (struct DisjointSetUnion)); initDSU(ufb, n); int ans = 0 ; for (int i = 0 ; i < edgesSize; i++) { edges[i][1 ]--; edges[i][2 ]--; } for (int i = 0 ; i < edgesSize; i++) { if (edges[i][0 ] == 3 ) { if (!unionSet(ufa, edges[i][1 ], edges[i][2 ])) { ++ans; } else { unionSet(ufb, edges[i][1 ], edges[i][2 ]); } } } for (int i = 0 ; i < edgesSize; i++) { if (edges[i][0 ] == 1 ) { if (!unionSet(ufa, edges[i][1 ], edges[i][2 ])) { ++ans; } } else if (edges[i][0 ] == 2 ) { if (!unionSet(ufb, edges[i][1 ], edges[i][2 ])) { ++ans; } } } if (ufa->setCount != 1 || ufb->setCount != 1 ) { return -1 ; } return ans; }
复杂度分析