1319. Number of Operations to Make Network Connected
Problem Description
You have n
computers labeled from 0
to n - 1
. These computers are connected by ethernet cables, where each cable connects exactly two computers. The connections are given as an array connections
, where each element connections[i] = [aᵢ, bᵢ]
represents a cable connecting computer aᵢ
to computer bᵢ
.
Initially, some computers may be connected to each other (either directly through a cable or indirectly through a chain of cables), forming separate network groups. Your goal is to make all computers connected so that every computer can reach every other computer through the network.
You can perform the following operation:
- Remove a cable between two directly connected computers
- Use that cable to connect any two computers that are not currently connected
The task is to find the minimum number of times you need to perform this operation to connect all computers into one network. If it's impossible to connect all computers (due to insufficient cables), return -1
.
For example:
- If you have 4 computers with connections
[[0,1],[0,2],[1,2]]
, computers 0, 1, and 2 are already connected, but computer 3 is isolated. You can take the redundant cable between 1 and 2 (since 1 and 2 are already connected through 0) and use it to connect computer 3 to any of the other computers. This requires 1 operation. - If you have 6 computers with connections
[[0,1],[0,2],[0,3],[1,4]]
, you have 5 cables but need at least 5 cables to connect 6 computers. However, the computers are split into groups, so you'd need to move cables to connect the groups. Since you have exactly enough cables, this is possible.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly involves computers (nodes) connected by ethernet cables (edges), forming a network graph structure.
Is it a tree?
- No: The network can have cycles (redundant connections between computers that are already connected through other paths) and may have multiple disconnected components.
Is the problem related to directed acyclic graphs?
- No: The connections are bidirectional (undirected graph), and the graph may contain cycles.
Is the problem related to shortest paths?
- No: We're not finding the shortest path between computers; we're concerned with making all computers connected.
Does the problem involve connectivity?
- Yes: The core of the problem is about connectivity - we need to ensure all computers are connected in a single network component.
Disjoint Set Union (DSU)
- Yes: DSU is the suggested approach for connectivity problems.
Conclusion: The flowchart suggests using Disjoint Set Union (DSU/Union-Find) for this connectivity problem. While the flowchart leads us to DSU, it's worth noting that DFS can also be used as an alternative approach to:
- Count the number of connected components
- Identify redundant edges within each component
- Determine if we have enough cables to connect all components
Both DSU and DFS are valid patterns for this problem, with DSU being more efficient for tracking connectivity and redundant edges dynamically.
Intuition
The key insight is to recognize that we need to connect separate network groups into one unified network. Think of it like connecting islands with bridges - we need to identify how many "islands" (disconnected components) we have and whether we have enough "bridges" (cables) to connect them all.
To connect k
separate components into one network, we need exactly k - 1
cables. For example, if we have 3 separate groups of computers, we need 2 cables to connect them all together.
But where do we get these cables? We can't create new cables - we can only reuse existing ones. The clever observation is that some cables might be "redundant" within a component. If computers A and B are already connected through some path (like A→C→B), then a direct cable between A and B is redundant - it doesn't add connectivity, it just creates a cycle.
This leads us to the solution strategy:
- Count how many separate components we have initially
- Count how many redundant cables we have (cables that create cycles)
- Check if we have enough redundant cables to connect all components
Using Union-Find (DSU) makes this elegant: as we process each cable connection, if two computers are already in the same component (have the same root), this cable is redundant. Otherwise, we union them, reducing our component count by 1.
The minimum operations needed equals the number of components minus 1 (since we need k-1
cables to connect k
components). If we don't have enough redundant cables to achieve this, it's impossible, so we return -1
.
The beauty of this approach is that we simultaneously track both the number of components and redundant cables in a single pass through the connections.
Learn more about Depth-First Search, Breadth-First Search, Union Find and Graph patterns.
Solution Approach
The implementation uses the Union-Find (Disjoint Set Union) data structure to efficiently track connectivity and count redundant connections.
Initialization:
- Create a parent array
p
wherep[i] = i
initially, meaning each computer is its own parent (separate component) - Set
cnt = 0
to track redundant connections - Variable
n
represents both the initial number of computers and will be decremented to track the number of components
Find Function with Path Compression:
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 a computer. Path compression optimizes future lookups by directly connecting nodes to their root.
Processing Connections:
For each connection [a, b]
:
- Find the root parents:
pa = find(a)
andpb = find(b)
- If
pa == pb
, computersa
andb
are already in the same component- This connection is redundant, so increment
cnt += 1
- This connection is redundant, so increment
- Otherwise, they're in different components:
- Union them:
p[pa] = pb
(make one root point to the other) - Decrement
n -= 1
(we've merged two components into one)
- Union them:
Final Calculation: After processing all connections:
n
now represents the number of remaining componentsn - 1
is the number of cables needed to connect all componentscnt
is the number of available redundant cables
Return:
-1
ifn - 1 > cnt
(not enough cables to connect all components)n - 1
otherwise (the minimum number of operations needed)
The clever trick here is reusing the variable n
: it starts as the total number of computers, and each successful union reduces it by 1, so it ends up representing the final number of components. This eliminates the need for a separate component counter.
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 concrete example with 5 computers and connections [[0,1], [1,2], [0,2], [3,4]]
.
Initial Setup:
- We have computers labeled 0, 1, 2, 3, 4
- Parent array:
p = [0, 1, 2, 3, 4]
(each computer is its own parent) cnt = 0
(redundant cables count)n = 5
(starts as number of computers, will track components)
Processing Connection [0,1]:
- Find roots:
find(0) = 0
,find(1) = 1
- Different roots, so union them:
p[0] = 1
- Decrement components:
n = 4
- State: Two components merged {0,1} and separate {2}, {3}, {4}
Processing Connection [1,2]:
- Find roots:
find(1) = 1
,find(2) = 2
- Different roots, so union them:
p[1] = 2
- Decrement components:
n = 3
- State: Components are now {0,1,2}, {3}, {4}
Processing Connection [0,2]:
- Find roots:
find(0)
→ follows 0→1→2, returns 2 find(2) = 2
- Same root! This cable is redundant (0 and 2 already connected via 1)
- Increment redundant count:
cnt = 1
- State: Still {0,1,2}, {3}, {4} but we identified a reusable cable
Processing Connection [3,4]:
- Find roots:
find(3) = 3
,find(4) = 4
- Different roots, so union them:
p[3] = 4
- Decrement components:
n = 2
- State: Final components are {0,1,2} and {3,4}
Final Calculation:
- We have
n = 2
components - Need
n - 1 = 1
cable to connect them into one network - Have
cnt = 1
redundant cable available - Since
1 ≤ 1
, we can connect all computers - Return
1
(minimum operations needed)
The solution correctly identifies that we can take the redundant cable between computers 0 and 2 (since they're already connected through 1) and use it to connect the two separate groups {0,1,2} and {3,4}, requiring exactly 1 operation.
Solution Implementation
1class Solution:
2 def makeConnected(self, n: int, connections: List[List[int]]) -> int:
3 """
4 Find minimum number of operations to connect all computers in a network.
5 Uses Union-Find (Disjoint Set Union) to detect redundant connections and count components.
6
7 Args:
8 n: Number of computers (nodes)
9 connections: List of existing connections between computers
10
11 Returns:
12 Minimum operations needed, or -1 if impossible
13 """
14
15 def find(node: int) -> int:
16 """
17 Find the root parent of a node with path compression.
18
19 Args:
20 node: The node to find root for
21
22 Returns:
23 Root parent of the node
24 """
25 if parent[node] != node:
26 # Path compression: directly connect node to root
27 parent[node] = find(parent[node])
28 return parent[node]
29
30 # Count redundant connections (cables that can be reused)
31 redundant_cables = 0
32
33 # Initialize parent array where each node is its own parent
34 parent = list(range(n))
35
36 # Number of connected components (starts at n, decreases with each union)
37 num_components = n
38
39 # Process each connection
40 for computer_a, computer_b in connections:
41 # Find root parents of both computers
42 root_a = find(computer_a)
43 root_b = find(computer_b)
44
45 if root_a == root_b:
46 # Already in same component, this cable is redundant
47 redundant_cables += 1
48 else:
49 # Union: connect the two components
50 parent[root_a] = root_b
51 # Decrease component count
52 num_components -= 1
53
54 # Need (num_components - 1) cables to connect all components
55 # Check if we have enough redundant cables
56 cables_needed = num_components - 1
57
58 if cables_needed > redundant_cables:
59 return -1 # Not enough cables to connect all components
60 else:
61 return cables_needed # Number of operations needed
62
1class Solution {
2 // Parent array for Union-Find (Disjoint Set Union)
3 private int[] parent;
4
5 /**
6 * Determines the minimum number of operations needed to make all computers connected.
7 *
8 * @param n The number of computers (nodes)
9 * @param connections The existing connections between computers (edges)
10 * @return The minimum number of operations needed, or -1 if impossible
11 */
12 public int makeConnected(int n, int[][] connections) {
13 // Initialize parent array where each node is its own parent initially
14 parent = new int[n];
15 for (int i = 0; i < n; i++) {
16 parent[i] = i;
17 }
18
19 // Count redundant connections (cables that connect already connected components)
20 int redundantCables = 0;
21
22 // Process each connection
23 for (int[] connection : connections) {
24 int node1 = connection[0];
25 int node2 = connection[1];
26
27 // Find the root parents of both nodes
28 int parentOfNode1 = find(node1);
29 int parentOfNode2 = find(node2);
30
31 if (parentOfNode1 == parentOfNode2) {
32 // Nodes are already in the same component - this cable is redundant
33 redundantCables++;
34 } else {
35 // Union: Connect the two components by making one parent point to the other
36 parent[parentOfNode1] = parentOfNode2;
37 // Decrease the count of disconnected components
38 n--;
39 }
40 }
41
42 // n now represents the number of disconnected components
43 // We need (n - 1) cables to connect n components
44 // Check if we have enough redundant cables to reconnect all components
45 int cablesNeeded = n - 1;
46 return cablesNeeded > redundantCables ? -1 : cablesNeeded;
47 }
48
49 /**
50 * Finds the root parent of a node using path compression optimization.
51 *
52 * @param x The node to find the root parent for
53 * @return The root parent of the node
54 */
55 private int find(int x) {
56 if (parent[x] != x) {
57 // Path compression: Make the node directly point to its root parent
58 parent[x] = find(parent[x]);
59 }
60 return parent[x];
61 }
62}
63
1class Solution {
2public:
3 int makeConnected(int n, vector<vector<int>>& connections) {
4 // Initialize parent array for Union-Find
5 // Each node is initially its own parent
6 vector<int> parent(n);
7 iota(parent.begin(), parent.end(), 0);
8
9 // Count of redundant edges (edges connecting nodes already in same component)
10 int redundantEdges = 0;
11
12 // Find function with path compression
13 // Returns the root parent of node x
14 function<int(int)> find = [&](int x) -> int {
15 if (parent[x] != x) {
16 parent[x] = find(parent[x]); // Path compression
17 }
18 return parent[x];
19 };
20
21 // Process each connection (edge)
22 for (const auto& connection : connections) {
23 int rootA = find(connection[0]);
24 int rootB = find(connection[1]);
25
26 if (rootA == rootB) {
27 // Nodes are already connected - this edge is redundant
28 ++redundantEdges;
29 } else {
30 // Union operation: connect the two components
31 parent[rootA] = rootB;
32 --n; // Decrease number of components
33 }
34 }
35
36 // To connect n components, we need n-1 edges
37 // Check if we have enough redundant edges to connect all components
38 return redundantEdges >= n - 1 ? n - 1 : -1;
39 }
40};
41
1/**
2 * Calculates the minimum number of operations needed to connect all computers in a network.
3 * Uses Union-Find (Disjoint Set Union) data structure to track connected components.
4 *
5 * @param n - The number of computers in the network (0 to n-1)
6 * @param connections - Array of connections where each connection is [computerA, computerB]
7 * @returns The minimum number of cable moves needed to connect all computers, or -1 if impossible
8 */
9function makeConnected(n: number, connections: number[][]): number {
10 // Initialize parent array for Union-Find where each node is its own parent initially
11 const parent: number[] = Array.from({ length: n }, (_, index) => index);
12
13 /**
14 * Find operation with path compression for Union-Find.
15 * Finds the root parent of a node and compresses the path for optimization.
16 *
17 * @param node - The node to find the root parent for
18 * @returns The root parent of the node
19 */
20 const find = (node: number): number => {
21 // If node is not its own parent, recursively find root and compress path
22 if (parent[node] !== node) {
23 parent[node] = find(parent[node]);
24 }
25 return parent[node];
26 };
27
28 // Count of redundant cables (cables connecting already connected computers)
29 let redundantCables = 0;
30
31 // Process each connection
32 for (const [computerA, computerB] of connections) {
33 // Find root parents of both computers
34 const rootA = find(computerA);
35 const rootB = find(computerB);
36
37 if (rootA === rootB) {
38 // Computers are already connected, this cable is redundant
39 redundantCables++;
40 } else {
41 // Union operation: connect the two components
42 parent[rootA] = rootB;
43 // Decrease the count of disconnected components
44 n--;
45 }
46 }
47
48 // n-1 represents the number of disconnected components minus 1
49 // We need at least (n-1) cables to connect n components
50 // Return the number of operations needed if we have enough redundant cables, otherwise -1
51 return redundantCables >= n - 1 ? n - 1 : -1;
52}
53
Time and Space Complexity
The time complexity is O(m × α(n))
, where m
is the number of connections and α(n)
is the inverse Ackermann function, which is effectively constant for all practical values of n
. This can be approximated as O(m)
for practical purposes, though it can also be expressed as O(m × log n)
as an upper bound since α(n) < log n
.
The space complexity is O(n)
, where n
is the number of computers. This space is used for the parent array p
which stores the parent of each node in the Union-Find data structure.
The code implements a Union-Find (Disjoint Set Union) algorithm with path compression in the find
function. Each find
operation has an amortized time complexity of O(α(n))
due to path compression, and since we perform at most 2m
find operations (two for each connection), the total time complexity is O(m × α(n))
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Component Counting Without Union-Find
A common mistake is trying to count components by simply tracking which computers are connected without properly implementing union-find. This leads to incorrect component counts when there are indirect connections.
Incorrect approach:
# Wrong: This doesn't handle transitive connections properly
connected = set()
for a, b in connections:
connected.add(a)
connected.add(b)
components = n - len(connected) # This is wrong!
Why it fails: This approach doesn't recognize that computers can be in the same component through indirect connections. For example, with connections [[0,1], [1,2]]
, computers 0 and 2 are in the same component but not directly connected.
Solution: Use proper Union-Find with path compression to correctly track component membership.
2. Forgetting to Check Cable Sufficiency First
Some implementations try to perform the union operations without first checking if there are enough cables total.
Incorrect approach:
def makeConnected(self, n: int, connections: List[List[int]]) -> int:
# Missing check!
parent = list(range(n))
# ... rest of union-find logic
Why it fails: To connect n computers, you need at least n-1 cables. Without this check, you might waste time processing when the answer is obviously -1.
Solution: Add an early return condition:
if len(connections) < n - 1:
return -1 # Not enough cables to connect all computers
3. Not Implementing Path Compression
Implementing find() without path compression can lead to performance issues with deep trees.
Inefficient approach:
def find(x):
while parent[x] != x:
x = parent[x]
return x
Why it's problematic: This creates long chains in worst-case scenarios, making find() operations O(n) instead of nearly O(1).
Solution: Implement path compression:
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # Compress path
return parent[x]
4. Confusing Redundant Cables with Total Operations
A subtle mistake is returning the number of redundant cables instead of the number of components minus one.
Incorrect return:
return redundant_cables # Wrong!
Why it fails: The number of operations needed equals the number of additional connections required to unite all components, which is (number_of_components - 1)
, not the total number of redundant cables available.
Solution: Return num_components - 1
after ensuring you have enough redundant cables.
Which data structure is used to implement priority queue?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
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
Want a Structured Path to Master System Design Too? Don’t Miss This!