3613. Minimize Maximum Component Cost
Problem Description
You have an undirected connected graph with n
nodes labeled from 0 to n - 1
. The graph is defined by a 2D array edges
where each edges[i] = [ui, vi, wi]
represents an undirected edge between nodes ui
and vi
with weight wi
. You're also given an integer k
.
Your task is to remove some edges from the graph so that the resulting graph has at most k
connected components.
After removing edges, each connected component will have a cost, which is defined as the maximum edge weight within that component. If a component has no edges (it's just an isolated node), its cost is 0.
Your goal is to minimize the maximum cost among all components after removing edges.
For example, if after removing edges you have 3 components with costs [5, 8, 2], the maximum cost would be 8. You want to find a way to remove edges such that this maximum cost is as small as possible.
The problem asks you to return the minimum possible value of the maximum cost among all components after optimal edge removals.
Intuition
The key insight is to think about this problem in reverse. Instead of removing edges from a connected graph, we can start with all nodes isolated and gradually add edges to form components.
If we're allowed to have k
components, we need to reduce the number of components from n
(all isolated) down to k
. This means we need to add exactly n - k
edges to merge components together.
Here's the crucial observation: to minimize the maximum edge weight in any component, we should prioritize adding edges with smaller weights first. Why? Because once we add an edge with weight w
, at least one component will have a maximum edge weight of at least w
.
So the strategy becomes:
- Sort all edges by weight in ascending order
- Start with
n
isolated nodes (components) - Add edges one by one from smallest to largest weight
- Each edge that connects two different components reduces the total component count by 1
- Stop when we reach exactly
k
components
The weight of the last edge we add will be our answer. This is because:
- All edges with smaller weights have already been added (and are distributed among the components)
- All edges with larger weights can be removed (we don't need them to maintain
k
components) - The last edge we add represents the minimum possible maximum edge weight we must tolerate
Special case: If k = n
, we can remove all edges, leaving all nodes isolated. In this case, the maximum cost is 0 since isolated nodes have no edges.
Learn more about Union Find, Graph, Binary Search and Sorting patterns.
Solution Approach
The solution uses Sorting combined with Union-Find (Disjoint Set Union) data structure to efficiently solve this problem.
Step 1: Handle Special Case
if k == n: return 0
If k
equals n
, we can have all nodes as separate components (remove all edges), so the maximum cost is 0.
Step 2: Sort Edges by Weight
edges.sort(key=lambda x: x[2])
We sort all edges in ascending order by their weights. This allows us to process edges from smallest to largest weight.
Step 3: Initialize Union-Find Structure
cnt = n
p = list(range(n))
cnt
tracks the current number of connected components (initiallyn
since all nodes are isolated)p
is the parent array for Union-Find, wherep[i]
initially points to itself (each node is its own parent)
Step 4: Union-Find Find Operation
def find(x: int) -> int:
if p[x] != x:
p[x] = find(p[x]) # Path compression
return p[x]
This function finds the root parent of node x
with path compression optimization, which flattens the tree structure for faster future lookups.
Step 5: Process Edges and Union Components
for u, v, w in edges: pu, pv = find(u), find(v) if pu != pv: p[pu] = pv cnt -= 1 if cnt <= k: return w
For each edge (u, v, w)
:
- Find the root parents of nodes
u
andv
- If they have different parents (
pu != pv
), they're in different components:- Union them by setting
p[pu] = pv
- Decrease the component count by 1
- If we've reached
k
or fewer components, return the current edge weightw
- Union them by setting
The algorithm works because:
- We process edges from smallest to largest weight
- We only add an edge if it connects two different components
- The moment we reach
k
components, the current edge weight is the minimum possible maximum weight - All remaining edges with larger weights can be safely removed
Time Complexity: O(E log E + E × α(n))
where E is the number of edges and α is the inverse Ackermann function (practically constant)
Space Complexity: O(n)
for the Union-Find parent array
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Given:
n = 4
nodes (labeled 0, 1, 2, 3)edges = [[0,1,3], [1,2,5], [2,3,2], [0,3,4]]
k = 2
(we want at most 2 connected components)
Step 1: Check special case
- Since
k = 2
andn = 4
, we proceed (k ≠ n)
Step 2: Sort edges by weight
After sorting: edges = [[2,3,2], [0,1,3], [0,3,4], [1,2,5]]
Step 3: Initialize Union-Find
- Start with 4 isolated nodes:
cnt = 4
- Parent array:
p = [0, 1, 2, 3]
(each node is its own parent) - Visual representation:
0 1 2 3 (4 components)
Step 4: Process edges from smallest to largest
Edge 1: [2,3,2] with weight 2
- Find parents:
find(2) = 2
,find(3) = 3
- Different parents, so union them:
p[2] = 3
- Component count:
cnt = 3
- Visual:
0 1 2-3 (3 components)
- Check:
cnt = 3 > k = 2
, continue
Edge 2: [0,1,3] with weight 3
- Find parents:
find(0) = 0
,find(1) = 1
- Different parents, so union them:
p[0] = 1
- Component count:
cnt = 2
- Visual:
0-1 2-3 (2 components)
- Check:
cnt = 2 = k
, return weight 3
Result: The minimum possible maximum cost is 3.
Why this works:
- We formed exactly 2 components: {0,1} and {2,3}
- Component {0,1} has edge weight 3
- Component {2,3} has edge weight 2
- The maximum cost among components is 3
- We didn't need to add edges [0,3,4] or [1,2,5], which would have created larger maximum weights
If we had added edge [0,3,4] instead, it would merge all nodes into one component, violating our constraint of having at most 2 components. By stopping at weight 3, we achieve the minimum possible maximum cost while maintaining exactly k=2 components.
Solution Implementation
1class Solution:
2 def minCost(self, n: int, edges: List[List[int]], k: int) -> int:
3 """
4 Find the minimum cost to partition n nodes into at most k connected components.
5 Uses Union-Find with Kruskal's algorithm approach.
6
7 Args:
8 n: Number of nodes (0-indexed)
9 edges: List of edges [u, v, weight]
10 k: Maximum number of connected components allowed
11
12 Returns:
13 Minimum cost (maximum edge weight used) to achieve k or fewer components
14 """
15
16 def find(node: int) -> int:
17 """
18 Find the root parent of a node with path compression.
19
20 Args:
21 node: The node to find the root for
22
23 Returns:
24 The root parent of the node
25 """
26 if parent[node] != node:
27 # Path compression: make every node point directly to root
28 parent[node] = find(parent[node])
29 return parent[node]
30
31 # If we need n components and have n nodes, they're already disconnected
32 if k == n:
33 return 0
34
35 # Sort edges by weight in ascending order
36 edges.sort(key=lambda edge: edge[2])
37
38 # Initialize Union-Find structure
39 component_count = n # Initially, each node is its own component
40 parent = list(range(n)) # Each node is its own parent initially
41
42 # Process edges in order of increasing weight
43 for source, destination, weight in edges:
44 # Find the root parents of both nodes
45 parent_source = find(source)
46 parent_destination = find(destination)
47
48 # If nodes are in different components, merge them
49 if parent_source != parent_destination:
50 # Union: merge the two components
51 parent[parent_source] = parent_destination
52 component_count -= 1
53
54 # If we've reached k or fewer components, return current weight
55 if component_count <= k:
56 return weight
57
58 # If we can't achieve k components, return 0
59 return 0
60
1class Solution {
2 // Parent array for Union-Find (Disjoint Set Union) data structure
3 private int[] parent;
4
5 /**
6 * Finds the minimum cost to connect components until we have at most k components.
7 * Uses Kruskal's algorithm with Union-Find to build a Minimum Spanning Forest.
8 *
9 * @param n The number of nodes (0-indexed)
10 * @param edges Array of edges where each edge is [u, v, weight]
11 * @param k Target number of components
12 * @return The minimum cost (maximum edge weight used) to achieve k components
13 */
14 public int minCost(int n, int[][] edges, int k) {
15 // If we need n components, no edges are needed (all nodes isolated)
16 if (k == n) {
17 return 0;
18 }
19
20 // Initialize Union-Find with each node as its own parent
21 parent = new int[n];
22 Arrays.setAll(parent, i -> i);
23
24 // Sort edges by weight in ascending order for greedy selection
25 Arrays.sort(edges, Comparator.comparingInt(edge -> edge[2]));
26
27 // Track the current number of connected components
28 int componentCount = n;
29
30 // Process edges in order of increasing weight
31 for (int[] edge : edges) {
32 int nodeU = edge[0];
33 int nodeV = edge[1];
34 int weight = edge[2];
35
36 // Find the root parents of both nodes
37 int parentU = find(nodeU);
38 int parentV = find(nodeV);
39
40 // If nodes belong to different components, merge them
41 if (parentU != parentV) {
42 // Union operation: merge the two components
43 parent[parentU] = parentV;
44
45 // Decrease component count and check if we've reached target
46 componentCount--;
47 if (componentCount <= k) {
48 // Return the weight of the last edge added to achieve k components
49 return weight;
50 }
51 }
52 }
53
54 // If we can't achieve k components, return 0
55 return 0;
56 }
57
58 /**
59 * Find operation with path compression for Union-Find.
60 * Finds the root parent of a node and compresses the path for efficiency.
61 *
62 * @param x The node to find the root parent for
63 * @return The root parent of node x
64 */
65 private int find(int x) {
66 // Path compression: make every node point directly to root
67 if (parent[x] != x) {
68 parent[x] = find(parent[x]);
69 }
70 return parent[x];
71 }
72}
73
1class Solution {
2public:
3 int minCost(int n, vector<vector<int>>& edges, int k) {
4 // If k equals n, all nodes are already separate components
5 if (k == n) {
6 return 0;
7 }
8
9 // Initialize parent array for Union-Find (Disjoint Set Union)
10 vector<int> parent(n);
11 // Initially, each node is its own parent
12 ranges::iota(parent, 0);
13
14 // Sort edges by weight in ascending order
15 ranges::sort(edges, {}, [](const auto& edge) { return edge[2]; });
16
17 // Find function with path compression for Union-Find
18 auto findRoot = [&](this auto&& findRoot, int node) -> int {
19 if (parent[node] != node) {
20 // Path compression: make every node point directly to root
21 parent[node] = findRoot(parent[node]);
22 }
23 return parent[node];
24 };
25
26 // Start with n connected components (each node is isolated)
27 int componentsCount = n;
28
29 // Process edges in ascending order of weight
30 for (const auto& edge : edges) {
31 int nodeU = edge[0];
32 int nodeV = edge[1];
33 int weight = edge[2];
34
35 // Find roots of both nodes
36 int rootU = findRoot(nodeU);
37 int rootV = findRoot(nodeV);
38
39 // If nodes belong to different components, merge them
40 if (rootU != rootV) {
41 // Union operation: merge the two components
42 parent[rootU] = rootV;
43
44 // Decrease component count and check if we reached target
45 componentsCount--;
46 if (componentsCount <= k) {
47 // Return the weight of the edge that achieved the target
48 return weight;
49 }
50 }
51 }
52
53 // If we cannot reduce to k components, return 0
54 return 0;
55 }
56};
57
1/**
2 * Finds the minimum cost to connect n nodes into k connected components
3 * @param n - The number of nodes (0-indexed)
4 * @param edges - Array of edges where each edge is [u, v, weight]
5 * @param k - The target number of connected components
6 * @returns The minimum cost (maximum edge weight used) to achieve k components
7 */
8function minCost(n: number, edges: number[][], k: number): number {
9 // Initialize parent array for Union-Find data structure
10 // Each node is initially its own parent (self-loop)
11 const parent: number[] = Array.from({ length: n }, (_, index) => index);
12
13 /**
14 * Find operation with path compression for Union-Find
15 * @param x - The node to find the root parent for
16 * @returns The root parent of node x
17 */
18 const find = (x: number): number => {
19 // Path compression: directly connect x to its root parent
20 if (parent[x] !== x) {
21 parent[x] = find(parent[x]);
22 }
23 return parent[x];
24 };
25
26 // If we need n components (each node isolated), no edges needed
27 if (k === n) {
28 return 0;
29 }
30
31 // Sort edges by weight in ascending order
32 edges.sort((edgeA, edgeB) => edgeA[2] - edgeB[2]);
33
34 // Start with n connected components (each node is isolated)
35 let componentCount = n;
36
37 // Process edges in order of increasing weight
38 for (const [nodeU, nodeV, weight] of edges) {
39 const parentU = find(nodeU);
40 const parentV = find(nodeV);
41
42 // If nodes are in different components, merge them
43 if (parentU !== parentV) {
44 // Union operation: merge the two components
45 parent[parentU] = parentV;
46 componentCount--;
47
48 // If we've reached k components, return the current edge weight
49 // This is the minimum cost needed to achieve k components
50 if (componentCount <= k) {
51 return weight;
52 }
53 }
54 }
55
56 // If we cannot achieve k components, return 0
57 return 0;
58}
59
Time and Space Complexity
The time complexity of this code is O(m × log m + m × α(n))
, where m
is the number of edges and α(n)
is the inverse Ackermann function. However, since α(n)
grows extremely slowly and is effectively constant for all practical purposes, and considering that in the worst case m
can be O(n²)
for a complete graph, the time complexity can be simplified to O(n² × log n)
in the worst case. But if we consider the number of edges as the primary input size, it would be O(m × log m)
.
Breaking down the operations:
- Sorting the edges:
O(m × log m)
wherem = len(edges)
- Union-Find operations: Each
find
operation takesO(α(n))
amortized time with path compression, and we perform at mostm
union operations - The loop iterates through all edges once:
O(m)
The reference answer states O(n × log n)
, which appears to be considering a sparse graph where the number of edges m = O(n)
, making the sorting operation O(n × log n)
.
The space complexity is O(n)
where n
is the number of nodes:
- The parent array
p
storesn
elements:O(n)
- The recursion stack for the
find
function in the worst case (before path compression) could beO(n)
, but with path compression, it becomesO(log n)
on average - Other variables use constant space:
O(1)
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Problem Goal
Pitfall: Many people initially think we need to create exactly k
components, but the problem asks for "at most k
" components. This confusion can lead to incorrect termination conditions.
Solution: The algorithm correctly handles this by returning as soon as component_count <= k
, not waiting for exactly k
components.
2. Forgetting Path Compression in Union-Find
Pitfall: Implementing the find operation without path compression:
# Incorrect - No path compression
def find(x):
while parent[x] != x:
x = parent[x]
return x
This leads to degraded performance from O(α(n)) to O(n) per operation in worst case.
Solution: Always implement path compression:
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # Recursively compress path
return parent[x]
3. Incorrect Edge Case Handling for k = n
Pitfall: Forgetting to handle the special case where k == n
. Without this check, the algorithm might process all edges unnecessarily or return an incorrect result.
Solution: Add the special case check at the beginning:
if k == n: return 0 # All nodes can be isolated, no edges needed
4. Using Wrong Union Strategy
Pitfall: Some implementations might try to always make the smaller indexed node the parent:
# Potentially problematic if parent_source < parent_destination: parent[parent_destination] = parent_source else: parent[parent_source] = parent_destination
While this works, it's unnecessary complexity. The direction of union doesn't affect correctness here.
Solution: Keep it simple - consistently union in one direction:
parent[parent_source] = parent_destination
5. Misinterpreting the Return Value
Pitfall: Returning the wrong value when the target is reached. Some might return the index, the next weight, or accumulate weights:
# Wrong - returning accumulated weight total_weight += weight if component_count <= k: return total_weight
Solution: Return the current edge weight when reaching k components, as this represents the maximum edge weight needed to form at most k components:
if component_count <= k: return weight # This is the minimum possible maximum weight
What's the output of running the following function using the following tree as input?
1def serialize(root):
2 res = []
3 def dfs(root):
4 if not root:
5 res.append('x')
6 return
7 res.append(root.val)
8 dfs(root.left)
9 dfs(root.right)
10 dfs(root)
11 return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4 StringJoiner res = new StringJoiner(" ");
5 serializeDFS(root, res);
6 return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10 if (root == null) {
11 result.add("x");
12 return;
13 }
14 result.add(Integer.toString(root.val));
15 serializeDFS(root.left, result);
16 serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2 let res = [];
3 serialize_dfs(root, res);
4 return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8 if (!root) {
9 res.push("x");
10 return;
11 }
12 res.push(root.val);
13 serialize_dfs(root.left, res);
14 serialize_dfs(root.right, res);
15}
16
Recommended Readings
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Want a Structured Path to Master System Design Too? Don’t Miss This!