class DSU:
def __init__(self, n):
self.parent = list(range(n+1))
self.rank = [0]*(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 union(self, x, y):
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, edges):
alice = DSU(n)
bob = DSU(n)
res = 0
# Type 3 edges first
for t,u,v in edges:
if t == 3:
merged_a = alice.union(u,v)
merged_b = bob.union(u,v)
if not (merged_a or merged_b):
res += 1
# Type 1 (Alice only)
for t,u,v in edges:
if t == 1:
if not alice.union(u,v):
res += 1
# Type 2 (Bob only)
for t,u,v in edges:
if t == 2:
if not bob.union(u,v):
res += 1
if alice.components > 1 or bob.components > 1:
return -1
return res
class DSU {
public:
vector<int> parent, rank;
int components;
DSU(int n) : parent(n+1), rank(n+1,0), components(n) {
for (int i=0; i<=n; ++i) parent[i]=i;
}
int find(int x) {
if (parent[x]!=x) parent[x]=find(parent[x]);
return parent[x];
}
bool unite(int x, int y) {
int xr=find(x), yr=find(y);
if (xr==yr) return false;
if (rank[xr]<rank[yr]) parent[xr]=yr;
else if (rank[xr]>rank[yr]) parent[yr]=xr;
else {parent[yr]=xr; rank[xr]++;}
components--;
return true;
}
};
class Solution {
public:
int maxNumEdgesToRemove(int n, vector<vector<int>>& edges) {
DSU alice(n), bob(n);
int res=0;
// Type 3 first
for (auto& e: edges)
if (e[0]==3) {
bool a = alice.unite(e[1],e[2]);
bool b = bob.unite(e[1],e[2]);
if (!a && !b) res++;
}
// Type 1
for (auto& e: edges)
if (e[0]==1)
if (!alice.unite(e[1],e[2])) res++;
// Type 2
for (auto& e: edges)
if (e[0]==2)
if (!bob.unite(e[1],e[2])) res++;
if (alice.components>1 || bob.components>1) return -1;
return res;
}
};
class DSU {
int[] parent, rank;
int components;
DSU(int n) {
parent = new int[n+1];
rank = new int[n+1];
components = n;
for (int i=0; i<=n; ++i) parent[i]=i;
}
int find(int x) {
if (parent[x]!=x) parent[x]=find(parent[x]);
return parent[x];
}
boolean union(int x, int y) {
int xr=find(x), yr=find(y);
if (xr==yr) return false;
if (rank[xr]<rank[yr]) parent[xr]=yr;
else if (rank[xr]>rank[yr]) parent[yr]=xr;
else {parent[yr]=xr; rank[xr]++;}
components--;
return true;
}
}
class Solution {
public int maxNumEdgesToRemove(int n, int[][] edges) {
DSU alice = new DSU(n), bob = new DSU(n);
int res = 0;
// Type 3
for (int[] e: edges)
if (e[0]==3) {
boolean a = alice.union(e[1],e[2]);
boolean b = bob.union(e[1],e[2]);
if (!a && !b) res++;
}
// Type 1
for (int[] e: edges)
if (e[0]==1)
if (!alice.union(e[1],e[2])) res++;
// Type 2
for (int[] e: edges)
if (e[0]==2)
if (!bob.union(e[1],e[2])) res++;
if (alice.components>1 || bob.components>1) return -1;
return res;
}
}
class DSU {
constructor(n) {
this.parent = Array(n+1).fill(0).map((_,i)=>i);
this.rank = Array(n+1).fill(0);
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) {
let xr=this.find(x), yr=this.find(y);
if (xr==yr) return false;
if (this.rank[xr]<this.rank[yr]) this.parent[xr]=yr;
else if (this.rank[xr]>this.rank[yr]) this.parent[yr]=xr;
else {this.parent[yr]=xr; this.rank[xr]++;}
this.components--;
return true;
}
}
var maxNumEdgesToRemove = function(n, edges) {
let alice = new DSU(n), bob = new DSU(n), res = 0;
// Type 3
for (let [t,u,v] of edges)
if (t==3) {
let a=alice.union(u,v), b=bob.union(u,v);
if (!a && !b) res++;
}
// Type 1
for (let [t,u,v] of edges)
if (t==1)
if (!alice.union(u,v)) res++;
// Type 2
for (let [t,u,v] of edges)
if (t==2)
if (!bob.union(u,v)) res++;
if (alice.components>1 || bob.components>1) return -1;
return res;
};
You are given an undirected graph with n
nodes, labeled from 1
to n
. There are three types of edges:
n
and a list edges
, where each edge is represented as [type, u, v]
.
Your task is to remove the maximum number of edges from the graph so that the graph remains fully traversable for both Alice and Bob. This means both Alice and Bob must be able to reach every node from any starting point, using the edges available to them.
If it's not possible for both Alice and Bob to traverse the entire graph after removing edges, return -1
.
Key constraints:
At first glance, you might consider trying all possible combinations of edges to remove, checking each time if both Alice and Bob can still traverse the entire graph. However, this brute-force approach is not practical, especially as the number of possible edge combinations grows exponentially with the number of edges.
Instead, notice that the problem is about keeping the graph connected for two different "users" (Alice and Bob), each with their own set of usable edges, but with some overlap (type 3 edges). The goal is to maximize the number of removable edges, which means we want to keep only the essential edges that maintain connectivity for both Alice and Bob.
This leads us to think about minimum spanning trees (MSTs) and the use of Disjoint Set Union (DSU) data structures, which efficiently track connected components in a graph. By trying to add edges in a specific order (prioritizing shared edges), we can ensure that we only keep necessary edges and count the rest as removable.
The solution uses the Disjoint Set Union (DSU) data structure to efficiently check and merge connected components for both Alice and Bob. The approach is as follows:
-1
because it's impossible to keep the graph fully traversable for both.
Why DSU? DSU allows us to efficiently merge components and check if two nodes are already connected, which is crucial for avoiding cycles and ensuring minimal edge usage.
Let's walk through an example:
Input: n = 4
, edges = [[3,1,2],[3,2,3],[1,1,3],[1,2,4],[1,1,2],[2,3,4]]
[3,1,2]
and [3,2,3]
[3,1,2]
to both Alice and Bob DSUs. They now connect 1-2.[3,2,3]
to both DSUs. They now connect 1-2-3.[1,1,3], [1,2,4], [1,1,2]
[1,1,3]
: In Alice's DSU, 1 and 3 are already connected (via type 3), so this is redundant and can be removed.[1,2,4]
: Connects 4 to the main component in Alice's DSU.[1,1,2]
: 1 and 2 are already connected, so this is redundant and can be removed.[2,3,4]
[1,1,3]
and [1,1,2]
).
Output: 2
O(2^E)
where E
is the number of edges. This is infeasible for large inputs.
find
and union
operation is nearly constant time due to path compression and union by rank, so overall O(E * α(N))
, where α
is the inverse Ackermann function (very slow-growing).
O(N)
for the DSU structures (parent and rank arrays).
The problem asks us to maximize the number of removable edges while keeping the graph fully traversable for both Alice and Bob. By using the Disjoint Set Union (DSU) data structure and prioritizing shared edges (type 3), we efficiently determine which edges are essential and which are redundant. This approach ensures both users can traverse the entire graph, and the solution is both elegant and efficient, leveraging classic graph connectivity techniques.