LeetCode 1319 - Number of Operations to Make Network Connected
This problem models a computer network as an undirected graph. Each computer is a node, and each ethernet cable is an ed
Difficulty: 🟡 Medium
Topics: Depth-First Search, Breadth-First Search, Union-Find, Graph Theory
Solution
Problem Understanding
This problem models a computer network as an undirected graph. Each computer is a node, and each ethernet cable is an edge connecting two nodes. The goal is to determine the minimum number of cable rewirings needed to make the entire network fully connected.
A network is considered fully connected if every computer can reach every other computer, either directly or through intermediate computers. The important detail is that we are allowed to remove an existing cable and reconnect it somewhere else. In other words, if there are redundant connections inside one connected component, those cables can potentially be reused to connect separate disconnected components.
The input consists of:
n, the number of computers labeled from0ton - 1connections, where each entry[a, b]represents a bidirectional cable between computersaandb
The output should be the minimum number of cable moves required to connect the entire network. If it is impossible, we return -1.
The constraints are important because they guide the algorithm choice:
ncan be as large as10^5- The number of connections can also be up to
10^5
This immediately rules out expensive graph algorithms with quadratic complexity. We need a near linear solution.
There is also a key graph theory observation hidden in the problem. A connected graph with n nodes requires at least n - 1 edges. If the total number of cables is less than n - 1, then it is mathematically impossible to connect all computers, regardless of how cables are rearranged.
Several edge cases are important:
- A network with too few cables, such as
n = 6and only4connections - Multiple disconnected components
- Already connected networks
- Isolated nodes with no direct connections
- Networks containing redundant cycles, where some edges can safely be removed and reused
The problem guarantees there are no duplicate connections and no self loops, which simplifies the implementation.
Approaches
Brute Force Approach
A brute force strategy would attempt to simulate cable removals and reconnections between disconnected components. One could repeatedly search for redundant edges, remove one, and reconnect two disconnected groups until either the graph becomes connected or no spare cables remain.
To determine connectivity after every operation, we might run DFS or BFS repeatedly across the graph. Since each operation changes the graph structure, connectivity checks would have to be recomputed many times.
This approach is correct because it explicitly models the allowed operations. However, it is extremely inefficient. Repeated graph traversals combined with repeated rewiring attempts could easily lead to quadratic or worse complexity, which is too slow for 10^5 nodes.
Optimal Approach
The key insight is that we do not actually need to simulate cable movement.
Suppose the network has k connected components. To connect all components into one connected network, we need exactly k - 1 cables. This is because each operation can merge two components together.
Now consider spare cables. Any cycle in a component contains redundant edges. Those redundant edges can be extracted and reused elsewhere.
Instead of explicitly counting redundant edges, we can use a simpler mathematical condition:
- A connected graph with
nnodes requires at leastn - 1edges. - If the graph already has at least
n - 1total edges, then there are enough cables overall to connect all components.
So the algorithm becomes:
- Check whether the total number of connections is at least
n - 1 - Count the number of connected components
- Return
components - 1
We can efficiently count connected components using either:
- DFS/BFS
- Union-Find
Union-Find is especially efficient for large graphs because it supports near constant time merging and lookup operations.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) or worse | O(n) | Repeatedly simulates rewiring and connectivity checks |
| Optimal | O(n + m) | O(n) | Counts connected components using Union-Find or DFS |
Here, m represents the number of connections.
Algorithm Walkthrough
Union-Find Based Algorithm
- First, check whether the number of cables is sufficient.
A connected graph with n nodes requires at least n - 1 edges. If len(connections) < n - 1, immediately return -1.
2. Initialize a Union-Find data structure.
Each computer initially belongs to its own separate component. We create:
- A
parentarray where each node is its own parent - A
rankorsizearray to optimize unions
- Process every connection.
For each edge [a, b], perform a union operation between a and b.
If the two computers already belong to the same component, the edge is redundant. We do not actually need to store redundant edges separately because the edge count condition already guarantees enough spare cables exist. 4. Count connected components.
Every unique root in the Union-Find structure represents one connected component. 5. Compute the answer.
If there are k connected components, we need exactly k - 1 operations to connect them all.
Why it works
Each operation can connect exactly two disconnected components. Therefore, reducing k components into one connected network always requires k - 1 operations.
The graph must also contain enough total cables. Since a connected graph with n nodes requires at least n - 1 edges, having fewer edges makes the task impossible.
Union-Find correctly groups all nodes belonging to the same connected component, so counting components gives the exact number of required merges.
Python Solution
from typing import List
class Solution:
def makeConnected(self, n: int, connections: List[List[int]]) -> int:
if len(connections) < n - 1:
return -1
parent = list(range(n))
rank = [0] * n
def find(node: int) -> int:
if parent[node] != node:
parent[node] = find(parent[node])
return parent[node]
def union(a: int, b: int) -> bool:
root_a = find(a)
root_b = find(b)
if root_a == root_b:
return False
if rank[root_a] < rank[root_b]:
parent[root_a] = root_b
elif rank[root_a] > rank[root_b]:
parent[root_b] = root_a
else:
parent[root_b] = root_a
rank[root_a] += 1
return True
components = n
for a, b in connections:
if union(a, b):
components -= 1
return components - 1
The implementation begins with the critical feasibility check. If the number of connections is smaller than n - 1, the function immediately returns -1 because a connected graph cannot exist with too few edges.
The parent array stores the representative parent of each node. Initially, every node is its own parent because each computer starts as an independent component.
The find function performs path compression. Whenever we search for the root of a node, we recursively compress the path so future lookups become faster.
The union function merges two components. If both nodes already share the same root, the edge is redundant and no merge occurs. Otherwise, union by rank keeps the tree shallow, improving efficiency.
The variable components starts at n because every node is initially disconnected. Every successful union reduces the component count by one.
Finally, the answer is components - 1, representing the number of operations needed to merge all remaining disconnected groups.
Go Solution
func makeConnected(n int, connections [][]int) int {
if len(connections) < n-1 {
return -1
}
parent := make([]int, n)
rank := make([]int, n)
for i := 0; i < n; i++ {
parent[i] = i
}
var find func(int) int
find = func(node int) int {
if parent[node] != node {
parent[node] = find(parent[node])
}
return parent[node]
}
union := func(a int, b int) bool {
rootA := find(a)
rootB := find(b)
if rootA == rootB {
return false
}
if rank[rootA] < rank[rootB] {
parent[rootA] = rootB
} else if rank[rootA] > rank[rootB] {
parent[rootB] = rootA
} else {
parent[rootB] = rootA
rank[rootA]++
}
return true
}
components := n
for _, connection := range connections {
if union(connection[0], connection[1]) {
components--
}
}
return components - 1
}
The Go implementation follows the same algorithmic structure as the Python version. Slices are used for the parent and rank arrays. Recursive path compression is implemented using a closure for find.
Go does not support nested named functions in the same way as Python, so closures are used for both find and union.
Integer overflow is not a concern because all values remain well within Go's standard integer range.
Worked Examples
Example 1
Input:
n = 4
connections = [[0,1],[0,2],[1,2]]
Initial state:
| Computer | Parent |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
Initial components: 4
Process connection [0,1]
- Union succeeds
- Components become
3
| Computer | Parent |
|---|---|
| 0 | 0 |
| 1 | 0 |
| 2 | 2 |
| 3 | 3 |
Process connection [0,2]
- Union succeeds
- Components become
2
| Computer | Parent |
|---|---|
| 0 | 0 |
| 1 | 0 |
| 2 | 0 |
| 3 | 3 |
Process connection [1,2]
- Both already belong to root
0 - Redundant edge
- Components remain
2
Final result:
components - 1 = 2 - 1 = 1
Answer: 1
Example 2
Input:
n = 6
connections = [[0,1],[0,2],[0,3],[1,2],[1,3]]
Total edges = 5
Since n - 1 = 5, enough cables exist.
Union operations produce:
- Component
{0,1,2,3} - Component
{4} - Component
{5}
Total components = 3
Required operations:
3 - 1 = 2
Answer: 2
Example 3
Input:
n = 6
connections = [[0,1],[0,2],[0,3],[1,2]]
Total edges = 4
A connected graph with 6 nodes requires at least 5 edges.
Since:
4 < 5
it is impossible to connect the network.
Answer: -1
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + m) | Union-Find operations are nearly constant time |
| Space | O(n) | Parent and rank arrays store one entry per node |
The Union-Find data structure uses path compression and union by rank, which makes each operation effectively amortized constant time, more precisely O(α(n)), where α is the inverse Ackermann function.
Since we process every connection exactly once, the total runtime is linear relative to the size of the input.
Test Cases
from typing import List
class Solution:
def makeConnected(self, n: int, connections: List[List[int]]) -> int:
if len(connections) < n - 1:
return -1
parent = list(range(n))
rank = [0] * n
def find(node):
if parent[node] != node:
parent[node] = find(parent[node])
return parent[node]
def union(a, b):
root_a = find(a)
root_b = find(b)
if root_a == root_b:
return False
if rank[root_a] < rank[root_b]:
parent[root_a] = root_b
elif rank[root_a] > rank[root_b]:
parent[root_b] = root_a
else:
parent[root_b] = root_a
rank[root_a] += 1
return True
components = n
for a, b in connections:
if union(a, b):
components -= 1
return components - 1
solution = Solution()
assert solution.makeConnected(
4,
[[0,1],[0,2],[1,2]]
) == 1 # Example 1
assert solution.makeConnected(
6,
[[0,1],[0,2],[0,3],[1,2],[1,3]]
) == 2 # Example 2
assert solution.makeConnected(
6,
[[0,1],[0,2],[0,3],[1,2]]
) == -1 # Example 3, insufficient cables
assert solution.makeConnected(
1,
[]
) == 0 # Single computer already connected
assert solution.makeConnected(
2,
[[0,1]]
) == 0 # Already fully connected
assert solution.makeConnected(
3,
[[0,1]]
) == -1 # Not enough edges
assert solution.makeConnected(
5,
[[0,1],[0,2],[3,4],[2,1]]
) == 1 # One redundant edge available
assert solution.makeConnected(
5,
[[0,1],[1,2],[2,3],[3,4]]
) == 0 # Tree structure already connected
assert solution.makeConnected(
5,
[[0,1],[1,2],[2,0],[3,4]]
) == 1 # One cycle creates reusable cable
assert solution.makeConnected(
10,
[[0,1],[1,2],[2,3],[3,4],[4,0],
[5,6],[6,7],[7,8],[8,9],[9,5]]
) == 1 # Two large cyclic components
| Test | Why |
|---|---|
n=4, [[0,1],[0,2],[1,2]] |
Validates basic redundant cable usage |
n=6, [[0,1],[0,2],[0,3],[1,2],[1,3]] |
Tests multiple disconnected components |
n=6, [[0,1],[0,2],[0,3],[1,2]] |
Validates insufficient cable detection |
n=1, [] |
Smallest possible input |
n=2, [[0,1]] |
Already connected minimal graph |
n=3, [[0,1]] |
Too few edges |
n=5, [[0,1],[0,2],[3,4],[2,1]] |
Uses a redundant cycle edge |
n=5, linear chain |
Tests already connected tree |
n=5, one cycle plus one component |
Verifies reusable cycle edge |
n=10, two cyclic components |
Larger disconnected cyclic graph |
Edge Cases
Insufficient Total Cables
One of the most important edge cases occurs when the graph simply does not contain enough edges overall. A connected graph with n nodes must contain at least n - 1 edges. If this condition fails, no amount of rewiring can make the graph connected.
For example:
n = 6
connections = 4 edges
Even if every edge were perfectly placed, six nodes cannot form a connected graph with only four edges. The implementation handles this immediately with:
if len(connections) < n - 1:
return -1
This early exit avoids unnecessary computation.
Already Connected Network
Another important case is when the graph is already fully connected. In this situation, the answer should be 0 because no rewiring operations are needed.
For example:
n = 5
connections = [[0,1],[1,2],[2,3],[3,4]]
The Union-Find structure merges all nodes into one component. Since the final component count becomes 1, the algorithm returns:
1 - 1 = 0
Multiple Isolated Components
A graph may contain several disconnected groups along with isolated nodes. These cases can easily cause bugs if component counting is incorrect.
For example:
n = 6
connections = [[0,1],[2,3]]
Here we have four components:
{0,1}{2,3}{4}{5}
The algorithm correctly counts all components through Union-Find root tracking. Since four components require three merges, the algorithm returns:
4 - 1 = 3
provided enough spare cables exist.