3608. Minimum Time for K Connected Components
Problem Description
You are given an integer n and an undirected graph with n nodes labeled from 0 to n - 1. The graph is described by a 2D array edges, where each edges[i] = [u_i, v_i, time_i] represents an undirected edge connecting node u_i and node v_i. This edge can be removed at the moment time_i.
You are also given an integer k.
At the start, the graph might already be connected or it might be split into several pieces. Your goal is to determine the minimum time t so that, once every edge whose time <= t has been removed, the graph is broken into at least k connected components.
Return this minimum time t.
A connected component is a subgraph in which there is a path between any pair of vertices, and none of its vertices is connected by an edge to any vertex outside the subgraph.
In other words, as time increases, edges with smaller time values get removed first. Removing edges can only split components apart (or leave the count unchanged), increasing the number of connected components. You need to find the smallest t such that after deleting all edges with time <= t, the total number of connected components reaches k or more. If the graph already has at least k components without removing any edge, then the answer is 0.
How We Pick the Algorithm
Why Disjoint Set Union?
This problem maps to Disjoint Set Union through a short path in the full flowchart.
The problem finds the minimum time to reach k connected components by removing edges; reversing the process and adding edges largest-first with Union-Find tracks the component count.
Open in FlowchartIntuition
The first key observation is that the answer has a clear monotonic structure. If removing all edges with time <= t already produces at least k connected components, then choosing any larger time would only remove even more edges, which can never decrease the number of components. So the number of components is non-decreasing as t grows. This tells us we are looking for the smallest threshold t where the component count first reaches k.
Instead of thinking about removing edges (which is hard to model with a union-find, since union-find handles adding edges, not deleting them), we flip the process around. Imagine starting with no edges at all — every node is its own component, giving n components. Then we add edges back in decreasing order of time.
Why decreasing order? When we want the minimum time t, the edges that survive are exactly those with time > t. So if we add the largest-time edges first, at any point we are looking at the graph that remains after removing all edges with smaller times. Each edge we add represents an edge that "survives" a particular threshold.
As we add edges from largest time to smallest, we maintain the current number of connected components with a union-find. Every time an edge successfully joins two previously separate components, the component count drops by one. We keep going until the count falls below k. The time of the edge that caused this drop is precisely the answer: this edge must stay in the graph to keep the count at k or above, which means the threshold t must be at least its time, so removing everything with time <= t (where t equals this edge's time) gives us exactly k components.
If we can connect all edges and the component count never drops below k, it means the graph already satisfies the requirement even without removing anything, so the answer is 0.
Pattern Learn more about Union Find, Graph, Binary Search and Sorting patterns.
Solution Approach
Solution 1: Union-Find
We sort the edges by their time in ascending order, then add them back one by one starting from the edge with the largest time. Throughout this process, we use a union-find data structure to track the number of connected components in the current graph. The moment the component count drops below k, the current edge's time is the minimum time we are searching for.
Data Structure: Union-Find
The UnionFind class maintains two arrays:
p: wherep[x]is the parent of nodex. Initially every node is its own parent (p[x] = x).size: wheresize[x]records the size of the tree rooted atx, used for union by size.
The class supports two main operations:
find(x): returns the root representative of the set containingx. It uses path compression (self.p[x] = self.find(self.p[x])) to flatten the tree, speeding up future queries.union(a, b): tries to merge the sets containingaandb. It first finds their rootspaandpb. If they are already the same root, it returnsFalse(no merge happened). Otherwise it attaches the smaller tree under the larger one (union by size), updates the size, and returnsTrueto signal a successful merge.
Main Logic
- Sort
edgesby their third elementtimein ascending order:edges.sort(key=lambda x: x[2]). - Create a union-find structure over
nnodes, and initialize a countercnt = n, since with no edges added every node forms its own component. - Iterate over the edges in reverse order (
edges[::-1]), i.e., from the largesttimeto the smallest. For each edge(u, v, t):- Call
uf.union(u, v). If it returnsTrue, two separate components were merged, so we decrementcntby one. - After decrementing, check whether
cnt < k. If so, the edge withtime = tis the one that, if removed, keeps the graph atkor more components. Hence we returntas the answer.
- Call
- If we finish processing all edges and
cntnever falls belowk, the graph already has at leastkcomponents without removing anything, so we return0.
Why iterating in reverse works
By adding edges from largest to smallest time, at the step where we process an edge with time t, the graph we have built contains exactly all edges with time >= t. The edge that triggers cnt < k is the smallest-time edge that still needs to survive to keep at least k components — therefore the threshold t must equal its time, meaning all edges with time <= t are removed.
Complexity Analysis
- Time complexity:
O(n + m × α(n) + m log m), wheremis the number of edges. Sorting takesO(m log m), and the union-find operations are nearly constant time thanks to path compression and union by size (αis the inverse Ackermann function). - Space complexity:
O(n)for thepandsizearrays in the union-find structure.
Example Walkthrough
Let's trace through a small example to see the solution approach in action.
Input:
n = 4(nodes labeled0, 1, 2, 3)edges = [[0, 1, 5], [1, 2, 3], [2, 3, 8]]k = 2
We want the minimum time t such that after removing all edges with time <= t, the graph has at least 2 connected components.
Step 1: Sort edges by time in ascending order
[[1, 2, 3], [0, 1, 5], [2, 3, 8]]
Step 2: Initialize
- Union-Find over 4 nodes: each node is its own parent →
p = [0, 1, 2, 3] cnt = 4(no edges added yet, so 4 separate components)
Step 3: Iterate over edges in reverse (largest time first)
We process edges in this order: [2,3,8], then [0,1,5], then [1,2,3].
| Edge processed | union(u, v) result | Effect on cnt | cnt < k (cnt < 2)? | Action |
|---|---|---|---|---|
[2, 3, 8] | True (2 and 3 merged) | cnt = 4 → 3 | 3 < 2? No | Continue |
[0, 1, 5] | True (0 and 1 merged) | cnt = 3 → 2 | 2 < 2? No | Continue |
[1, 2, 3] | True (merges {0,1} and {2,3}) | cnt = 2 → 1 | 1 < 2? Yes | Return t = 3 |
Why is the answer 3?
When we process edge [1, 2, 3], the graph currently built contains all edges with time >= 3 (i.e., all three edges), forming a single connected component {0, 1, 2, 3} → cnt = 1, which is below k = 2.
This tells us that edge [1, 2, 3] is the smallest-time edge that must survive to keep the graph at 2 or more components. Therefore the threshold t must equal its time, 3.
Verification: If we remove all edges with time <= 3, we delete [1, 2, 3]. The surviving edges are [0, 1, 5] and [2, 3, 8], producing two components: {0, 1} and {2, 3}. That's exactly k = 2 components. ✓
If we had chosen any smaller threshold (e.g., t = 2), no edge would be removed, leaving just 1 component — not enough. So 3 is indeed the minimum valid time.
Solution Implementation
1from typing import List
2
3
4class UnionFind:
5 """Disjoint Set Union (Union-Find) with union by size and path compression."""
6
7 def __init__(self, n: int) -> None:
8 # parent[i] points to the parent of node i (itself initially)
9 self.parent: List[int] = list(range(n))
10 # size[i] tracks the number of nodes in the component rooted at i
11 self.size: List[int] = [1] * n
12
13 def find(self, x: int) -> int:
14 # Path compression: flatten the tree so future lookups are faster
15 if self.parent[x] != x:
16 self.parent[x] = self.find(self.parent[x])
17 return self.parent[x]
18
19 def union(self, a: int, b: int) -> bool:
20 root_a, root_b = self.find(a), self.find(b)
21 # Already in the same component, nothing to merge
22 if root_a == root_b:
23 return False
24 # Union by size: attach the smaller tree under the larger one
25 if self.size[root_a] > self.size[root_b]:
26 self.parent[root_b] = root_a
27 self.size[root_a] += self.size[root_b]
28 else:
29 self.parent[root_a] = root_b
30 self.size[root_b] += self.size[root_a]
31 return True
32
33
34class Solution:
35 def minTime(self, n: int, edges: List[List[int]], k: int) -> int:
36 # Sort edges by their time/weight in ascending order
37 edges.sort(key=lambda edge: edge[2])
38
39 uf = UnionFind(n)
40 # Number of connected components, starting with each node isolated
41 component_count = n
42
43 # Iterate edges from largest weight to smallest.
44 # Removing edges with weight >= t leaves only edges with smaller time;
45 # adding them in reverse simulates which connections survive at time t.
46 for u, v, t in reversed(edges):
47 # When a union actually merges two components, the count drops
48 if uf.union(u, v):
49 component_count -= 1
50 # Once we have fewer than k components, t is the answer
51 if component_count < k:
52 return t
53
54 # Already satisfies the component requirement without removing any edge
55 return 0
561/**
2 * Union-Find (Disjoint Set Union) data structure with
3 * path compression and union by size optimizations.
4 */
5class UnionFind {
6 // parent[i] holds the parent of node i; if parent[i] == i, then i is a root
7 private int[] parent;
8 // componentSize[i] holds the size of the tree rooted at i (valid only for roots)
9 private int[] componentSize;
10
11 /**
12 * Initializes n disjoint sets, each containing a single element.
13 *
14 * @param n the number of elements
15 */
16 public UnionFind(int n) {
17 parent = new int[n];
18 componentSize = new int[n];
19 for (int i = 0; i < n; i++) {
20 parent[i] = i; // each node is initially its own parent (root)
21 componentSize[i] = 1; // each component starts with size 1
22 }
23 }
24
25 /**
26 * Finds the representative (root) of the set containing x,
27 * applying path compression to flatten the tree.
28 *
29 * @param x the element to find
30 * @return the root of the set containing x
31 */
32 public int find(int x) {
33 if (parent[x] != x) {
34 // recursively find the root and compress the path
35 parent[x] = find(parent[x]);
36 }
37 return parent[x];
38 }
39
40 /**
41 * Merges the sets containing a and b using union by size.
42 *
43 * @param a the first element
44 * @param b the second element
45 * @return true if a merge occurred, false if they were already connected
46 */
47 public boolean union(int a, int b) {
48 int rootA = find(a);
49 int rootB = find(b);
50
51 // already in the same set, nothing to merge
52 if (rootA == rootB) {
53 return false;
54 }
55
56 // attach the smaller tree under the larger one to keep depth small
57 if (componentSize[rootA] > componentSize[rootB]) {
58 parent[rootB] = rootA;
59 componentSize[rootA] += componentSize[rootB];
60 } else {
61 parent[rootA] = rootB;
62 componentSize[rootB] += componentSize[rootA];
63 }
64 return true;
65 }
66}
67
68class Solution {
69 /**
70 * Determines the minimum edge weight (time) such that, after keeping only
71 * the edges processed so far (from largest weight downward), the graph is
72 * split into at least k connected components.
73 *
74 * @param n the number of nodes
75 * @param edges the edges, where each edge is [u, v, time]
76 * @param k the target number of connected components
77 * @return the answer time value, or 0 if it is never reached
78 */
79 public int minTime(int n, int[][] edges, int k) {
80 // sort edges by weight in ascending order
81 Arrays.sort(edges, (a, b) -> Integer.compare(a[2], b[2]));
82
83 UnionFind unionFind = new UnionFind(n);
84 // initially every node is its own component
85 int componentCount = n;
86
87 // iterate from the largest-weight edge down to the smallest
88 for (int i = edges.length - 1; i >= 0; i--) {
89 int u = edges[i][0];
90 int v = edges[i][1];
91 int time = edges[i][2];
92
93 // if this edge connects two previously separate components,
94 // merging them reduces the total component count by one
95 if (unionFind.union(u, v)) {
96 // once the component count drops below k, the current
97 // edge's time is the minimum satisfying value
98 if (--componentCount < k) {
99 return time;
100 }
101 }
102 }
103
104 // the target component count was never crossed
105 return 0;
106 }
107}
1081class UnionFind {
2public:
3 // parent[i] stores the representative (root) of the set that i belongs to
4 vector<int> parent;
5 // setSize[i] stores the size of the set rooted at i (only valid for roots)
6 vector<int> setSize;
7
8 // Initialize n disjoint sets, each element is its own parent
9 UnionFind(int n) {
10 parent.resize(n);
11 setSize.resize(n, 1);
12 for (int i = 0; i < n; i++) {
13 parent[i] = i;
14 }
15 }
16
17 // Find the root of x with path compression
18 int find(int x) {
19 if (parent[x] != x) {
20 parent[x] = find(parent[x]); // Flatten the tree for future queries
21 }
22 return parent[x];
23 }
24
25 // Union the sets containing a and b using union by size
26 // Returns true if a merge happened, false if they were already connected
27 bool unite(int a, int b) {
28 int rootA = find(a);
29 int rootB = find(b);
30 if (rootA == rootB) {
31 return false; // Already in the same set
32 }
33
34 // Attach the smaller tree under the larger one to keep trees shallow
35 if (setSize[rootA] > setSize[rootB]) {
36 parent[rootB] = rootA;
37 setSize[rootA] += setSize[rootB];
38 } else {
39 parent[rootA] = rootB;
40 setSize[rootB] += setSize[rootA];
41 }
42 return true;
43 }
44};
45
46class Solution {
47public:
48 int minTime(int n, vector<vector<int>>& edges, int k) {
49 // Sort edges by their time/weight in ascending order
50 sort(edges.begin(), edges.end(),
51 [](const vector<int>& a, const vector<int>& b) {
52 return a[2] < b[2];
53 });
54
55 UnionFind uf(n);
56 int componentCount = n; // Initially every node is its own component
57
58 // Iterate from the largest weight edge down to the smallest.
59 // By adding edges from heaviest to lightest, the answer is the weight
60 // of the edge that first makes the number of components drop below k.
61 for (int i = static_cast<int>(edges.size()) - 1; i >= 0; i--) {
62 int u = edges[i][0];
63 int v = edges[i][1];
64 int time = edges[i][2];
65
66 // Only merging distinct components reduces the component count
67 if (uf.unite(u, v)) {
68 componentCount--;
69 if (componentCount < k) {
70 // This edge's time is the minimum required answer
71 return time;
72 }
73 }
74 }
75
76 // Already satisfied the component requirement without removing any edge
77 return 0;
78 }
79};
801// Parent array for the disjoint set (each node's representative)
2let p: number[] = [];
3// Size array tracking the number of nodes in each component
4let size: number[] = [];
5
6// Find the root of x with path compression
7function find(x: number): number {
8 if (p[x] !== x) {
9 p[x] = find(p[x]);
10 }
11 return p[x];
12}
13
14// Merge the sets containing a and b using union by size.
15// Returns false if they are already connected, true otherwise.
16function union(a: number, b: number): boolean {
17 const pa = find(a);
18 const pb = find(b);
19 // Already in the same component, nothing to merge
20 if (pa === pb) return false;
21
22 // Attach the smaller tree under the larger one
23 if (size[pa] > size[pb]) {
24 p[pb] = pa;
25 size[pa] += size[pb];
26 } else {
27 p[pa] = pb;
28 size[pb] += size[pa];
29 }
30 return true;
31}
32
33function minTime(n: number, edges: number[][], k: number): number {
34 // Sort edges by their time/weight in ascending order
35 edges.sort((a, b) => a[2] - b[2]);
36
37 // Initialize the disjoint set: every node is its own parent with size 1
38 p = Array.from({ length: n }, (_, i) => i);
39 size = Array(n).fill(1);
40
41 // cnt tracks the current number of connected components
42 let cnt = n;
43
44 // Iterate edges from largest weight to smallest.
45 // By adding heavier edges first, we keep components merging;
46 // the moment the component count drops below k, the current
47 // edge's time is the answer (the minimum time to reach >= k components
48 // by removing edges with time >= t).
49 for (let i = edges.length - 1; i >= 0; i--) {
50 const [u, v, t] = edges[i];
51
52 // Merge the two endpoints; if a real merge happens, decrement count
53 if (union(u, v)) {
54 if (--cnt < k) {
55 return t;
56 }
57 }
58 }
59
60 // All nodes merged without reaching the threshold below k
61 return 0;
62}
63Time and Space Complexity
-
Time Complexity:
O(m × log m + m × α(n)), wheremis the number of edges andnis the number of nodes.- Sorting: The line
edges.sort(key=lambda x: x[2])sorts all edges by their time value, costingO(m × log m). - Union-Find traversal: The loop iterates over all
medges in reverse. For each edge, thefindoperation (with path compression) and theunionoperation (with union by size) take nearly constant amortized time,O(α(n)), whereαis the inverse Ackermann function. Thus this phase costsO(m × α(n)). - Combining both phases gives
O(m × log m + m × α(n)), which is dominated by the sorting termO(m × log m). If we follow the reference answer's convention of treating the edge count proportional tonand focusing on the union-find portion, this simplifies toO(n × α(n)).
- Sorting: The line
-
Space Complexity:
O(n).- The
UnionFindstructure maintains two arrays,self.pandself.size, each of lengthn, requiringO(n)space. - The recursive
findmethod may incurO(n)stack space in the worst case, but path compression keeps the effective tree height nearly flat in practice. - Sorting may use
O(log m)toO(m)auxiliary space depending on the implementation, but the dominant term remainsO(n).
- The
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Adding edges in ascending order instead of descending order
The most common mistake is iterating over the sorted edges in forward order (smallest time first) rather than in reverse. The core idea relies on the fact that at time t, all edges with time <= t are removed, leaving only edges with time > t intact. To simulate this correctly, we must rebuild the graph by adding the largest-time edges first, so that at the moment we process an edge with time t, the union-find structure already contains exactly the edges that would survive.
# WRONG: adds smallest-time edges first
for u, v, t in edges:
if uf.union(u, v):
component_count -= 1
if component_count < k:
return t
# CORRECT: adds largest-time edges first
for u, v, t in reversed(edges):
if uf.union(u, v):
component_count -= 1
if component_count < k:
return t
If you iterate forward, the returned t will be the time of an edge that does not correspond to the actual cut threshold, producing incorrect answers.
Pitfall 2: Forgetting the case where the graph already has k or more components
If the graph is already broken into at least k components without removing any edge (for example, when n is large but very few edges connect the nodes, or when k <= initial component count), no edge will ever drive component_count below k. In this situation, the loop completes without returning, and the answer must default to 0.
# Must return 0 after the loop ends return 0
Omitting this final return 0 (or returning some sentinel like -1) breaks the edge case where the requirement is satisfied at time 0. Always verify that the function returns 0 when the component count never falls below k.
Pitfall 3: Off-by-one error in the component-count comparison
A subtle bug arises from using the wrong comparison operator. The goal is at least k components, so we want the smallest t such that the number of components is >= k. When rebuilding in reverse, the count decreases with each successful union. The threshold edge is the one whose addition would drop the count below k:
# CORRECT: check strictly less than k if component_count < k: return t # WRONG variants that cause off-by-one errors: if component_count <= k: # returns too early return t if component_count == k: # may skip the right edge if it jumps past k return t
Using <= k returns one edge too early, and using == k can miss the answer entirely if multiple unions happen (though with union-find each union reduces the count by exactly one, == k still risks logical confusion). The correct semantics: as soon as adding this edge would make the surviving graph have fewer than k components, the edge's time must be the cut threshold.
Pitfall 4: Recursion depth limit in find with path compression
For very large graphs (large n), the recursive find method can hit Python's default recursion limit (typically 1000), causing a RecursionError on deep, unbalanced trees formed before path compression flattens them.
# Iterative alternative to avoid recursion depth issues
def find(self, x: int) -> int:
root = x
while self.parent[root] != root:
root = self.parent[root]
# Path compression in a second pass
while self.parent[x] != root:
self.parent[x], x = root, self.parent[x]
return root
Although union by size keeps trees shallow in practice, converting find to an iterative implementation is a safe defensive measure for competitive or production environments handling millions of nodes.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapDepth first search is equivalent to which of the tree traversal order?
Recommended Readings
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro So far in our DFS discussions we have mostly dealt with graphs with all the nodes connected to each other and thus forming one connected component Let's now look at a more general case where
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
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
Want a Structured Path to Master System Design Too? Don’t Miss This!