Facebook Pixel

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:

  1. Count the number of connected components
  2. Identify redundant edges within each component
  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Count how many separate components we have initially
  2. Count how many redundant cables we have (cables that create cycles)
  3. 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 where p[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]:

  1. Find the root parents: pa = find(a) and pb = find(b)
  2. If pa == pb, computers a and b are already in the same component
    • This connection is redundant, so increment cnt += 1
  3. 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)

Final Calculation: After processing all connections:

  • n now represents the number of remaining components
  • n - 1 is the number of cables needed to connect all components
  • cnt is the number of available redundant cables

Return:

  • -1 if n - 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 Evaluator

Example 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which data structure is used to implement priority queue?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More