1135. Connecting Cities With Minimum Cost 🔒
Problem Description
You have n
cities numbered from 1
to n
. You're given an array connections
where each element connections[i] = [xi, yi, costi]
represents a bidirectional connection between city xi
and city yi
with a cost of costi
.
Your task is to find the minimum total cost to connect all n
cities such that every city can reach every other city through some path of connections. If it's impossible to connect all cities, return -1
.
The total cost is calculated as the sum of all the connection costs you choose to use.
For example, if you have 3 cities and connections [[1,2,5], [1,3,6], [2,3,1]]
, you could connect all cities by using the connections between cities 1-2 (cost 5) and cities 2-3 (cost 1), giving a total minimum cost of 6. The connection between cities 1-3 is not needed since city 1 can already reach city 3 through city 2.
The problem essentially asks you to build a network that connects all cities using the least expensive set of connections, which is a classic minimum spanning tree problem where you need to find the subset of edges that connects all vertices with the minimum total weight.
Intuition
When we need to connect all cities with minimum cost, we're essentially building a network where every city can reach every other city. This is exactly what a spanning tree does - it connects all nodes in a graph with the minimum number of edges (exactly n-1
edges for n
nodes).
Since we want to minimize the total cost, we need a minimum spanning tree. The key insight is that we should greedily pick the cheapest connections available, but we need to be smart about it - we shouldn't create cycles (redundant connections) because they add cost without providing any additional connectivity.
Think of it like building a road network between cities. If cities A and B are already connected (either directly or through other cities), adding another road between them would be wasteful. We only want to add a new road if it connects previously unconnected regions.
This leads us to Kruskal's algorithm:
- Sort all connections by cost (cheapest first)
- Go through connections one by one
- For each connection, check if it connects two different groups of cities
- If yes, use this connection and merge the groups
- If no, skip it (it would create a cycle)
To efficiently track which cities are already connected, we use a Union-Find (Disjoint Set Union) data structure. Each city initially forms its own group. When we add a connection, we merge the groups of the two cities. The find
operation tells us which group a city belongs to, and if two cities have the same group leader, they're already connected.
We keep track of how many separate groups we have (initially n
groups). Each successful connection reduces this by 1. When we have exactly 1 group left, all cities are connected and we return the total cost. If we process all connections and still have multiple groups, it means some cities can't be connected, so we return -1
.
Learn more about Union Find, Graph, Minimum Spanning Tree and Heap (Priority Queue) patterns.
Solution Approach
The solution implements Kruskal's algorithm with Union-Find to find the minimum spanning tree:
Step 1: Sort Connections by Cost
connections.sort(key=lambda x: x[2])
We sort all connections in ascending order by their cost (index 2). This ensures we always consider the cheapest available connections first.
Step 2: Initialize Union-Find Structure
p = list(range(n))
Create a parent array p
where p[i] = i
initially. Each city is its own parent, representing n
separate components.
Step 3: Find Operation with Path Compression
def find(x):
if p[x] != x:
p[x] = find(p[x]) # Path compression
return p[x]
The find
function returns the root parent of a city. Path compression optimization makes future lookups faster by directly connecting nodes to their root.
Step 4: Process Each Connection
for x, y, cost in connections: x, y = x - 1, y - 1 # Convert to 0-indexed if find(x) == find(y): continue # Skip if already connected
For each connection, convert cities from 1-indexed to 0-indexed. Check if the two cities are already in the same component using find
. If they share the same root, adding this edge would create a cycle, so skip it.
Step 5: Union Operation and Cost Accumulation
p[find(x)] = find(y) # Union the components ans += cost # Add cost to total n -= 1 # Reduce component count
If the cities are in different components:
- Union them by making one root the parent of the other
- Add the connection cost to our running total
- Decrease the component count by 1
Step 6: Check for Complete Connection
if n == 1: return ans return -1
If we reach exactly 1 component (all cities connected), return the total cost. If we process all connections and still have multiple components, some cities remain disconnected, so return -1
.
The algorithm has time complexity O(E log E)
for sorting edges, where E
is the number of connections, and nearly O(E)
for the union-find operations with path compression.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with 4 cities and the following connections:
n = 4
connections = [[1,2,3], [3,4,4], [1,4,7], [2,3,5]]
Initial Setup:
- Sort connections by cost:
[[1,2,3], [3,4,4], [2,3,5], [1,4,7]]
- Parent array
p = [0, 1, 2, 3]
(0-indexed, each city is its own parent) - Component count = 4, Total cost = 0
Step 1: Process connection [1,2,3]
- Convert to 0-indexed: cities 0 and 1
find(0) = 0
,find(1) = 1
(different components)- Union: Set
p[0] = 1
(connect component 0 to component 1) - Add cost: Total cost = 3
- Component count = 3
- Current forest:
{0,1}
,{2}
,{3}
Step 2: Process connection [3,4,4]
- Convert to 0-indexed: cities 2 and 3
find(2) = 2
,find(3) = 3
(different components)- Union: Set
p[2] = 3
(connect component 2 to component 3) - Add cost: Total cost = 3 + 4 = 7
- Component count = 2
- Current forest:
{0,1}
,{2,3}
Step 3: Process connection [2,3,5]
- Convert to 0-indexed: cities 1 and 2
find(1) = 1
,find(2) = 3
(through path compression: 2→3)- Different components, so connect them
- Union: Set
p[1] = 3
(connect component {0,1} to component {2,3}) - Add cost: Total cost = 7 + 5 = 12
- Component count = 1
- All cities now connected:
{0,1,2,3}
Result: Since component count = 1, all cities are connected. Return total cost = 12.
Note that the connection [1,4,7]
is never used because by the time we would consider it, cities 1 and 4 (0 and 3 in 0-indexed) are already connected through the path 0→1→3, making this edge redundant.
Solution Implementation
1class Solution:
2 def minimumCost(self, n: int, connections: List[List[int]]) -> int:
3 """
4 Find the minimum cost to connect all cities using Kruskal's algorithm.
5
6 Args:
7 n: Number of cities (1 to n)
8 connections: List of [city1, city2, cost] representing edges
9
10 Returns:
11 Minimum cost to connect all cities, or -1 if impossible
12 """
13
14 def find(node: int) -> int:
15 """
16 Find the root parent of a node with path compression.
17
18 Args:
19 node: The node to find the root parent for
20
21 Returns:
22 The root parent of the node
23 """
24 if parent[node] != node:
25 # Path compression: directly connect node to its root parent
26 parent[node] = find(parent[node])
27 return parent[node]
28
29 # Sort connections by cost in ascending order for greedy approach
30 connections.sort(key=lambda edge: edge[2])
31
32 # Initialize parent array where each node is its own parent
33 parent = list(range(n))
34
35 # Track total cost of selected edges
36 total_cost = 0
37
38 # Track number of components (initially n cities are n components)
39 num_components = n
40
41 # Process each edge in order of increasing cost
42 for city1, city2, cost in connections:
43 # Convert to 0-indexed (cities are numbered 1 to n)
44 city1_idx = city1 - 1
45 city2_idx = city2 - 1
46
47 # Find root parents of both cities
48 root1 = find(city1_idx)
49 root2 = find(city2_idx)
50
51 # Skip if cities are already connected (same component)
52 if root1 == root2:
53 continue
54
55 # Union: connect the two components
56 parent[root1] = root2
57
58 # Add edge cost to total
59 total_cost += cost
60
61 # Decrease number of components
62 num_components -= 1
63
64 # If all cities are connected (one component), return result
65 if num_components == 1:
66 return total_cost
67
68 # If not all cities are connected, return -1
69 return -1
70
1class Solution {
2 // Parent array for Union-Find (Disjoint Set Union)
3 private int[] parent;
4
5 /**
6 * Finds the minimum cost to connect all cities using given connections.
7 * Uses Kruskal's algorithm with Union-Find to build a Minimum Spanning Tree.
8 *
9 * @param n The number of cities (numbered from 1 to n)
10 * @param connections Array of connections where each connection is [city1, city2, cost]
11 * @return The minimum cost to connect all cities, or -1 if impossible
12 */
13 public int minimumCost(int n, int[][] connections) {
14 // Sort connections by cost in ascending order (greedy approach for Kruskal's algorithm)
15 Arrays.sort(connections, Comparator.comparingInt(connection -> connection[2]));
16
17 // Initialize parent array for Union-Find
18 parent = new int[n];
19 for (int i = 0; i < n; i++) {
20 parent[i] = i; // Each city is initially its own parent
21 }
22
23 int totalCost = 0;
24 int remainingComponents = n; // Track number of disconnected components
25
26 // Process each connection in order of increasing cost
27 for (int[] connection : connections) {
28 // Convert to 0-indexed (cities are 1-indexed in input)
29 int city1 = connection[0] - 1;
30 int city2 = connection[1] - 1;
31 int cost = connection[2];
32
33 // Check if cities are already connected (same component)
34 if (find(city1) == find(city2)) {
35 continue; // Skip this edge to avoid creating a cycle
36 }
37
38 // Union: Connect the two components
39 parent[find(city1)] = find(city2);
40
41 // Add the cost of this connection
42 totalCost += cost;
43
44 // Decrease the number of components
45 remainingComponents--;
46
47 // If all cities are connected (only 1 component remains), return the total cost
48 if (remainingComponents == 1) {
49 return totalCost;
50 }
51 }
52
53 // If we couldn't connect all cities, return -1
54 return -1;
55 }
56
57 /**
58 * Finds the root parent of a node using path compression optimization.
59 *
60 * @param node The node to find the root parent for
61 * @return The root parent of the node
62 */
63 private int find(int node) {
64 if (parent[node] != node) {
65 // Path compression: Make each node point directly to the root
66 parent[node] = find(parent[node]);
67 }
68 return parent[node];
69 }
70}
71
1class Solution {
2public:
3 int minimumCost(int n, vector<vector<int>>& connections) {
4 // Initialize parent array for Union-Find (Disjoint Set Union)
5 // Each node is initially its own parent
6 vector<int> parent(n);
7 iota(parent.begin(), parent.end(), 0); // Fill with values 0, 1, 2, ..., n-1
8
9 // Sort edges by cost in ascending order (Kruskal's algorithm requirement)
10 sort(connections.begin(), connections.end(),
11 [](const auto& a, const auto& b) {
12 return a[2] < b[2];
13 });
14
15 int totalCost = 0;
16 int componentsCount = n; // Initially we have n separate components
17
18 // Find function with path compression for Union-Find
19 function<int(int)> findRoot = [&](int node) -> int {
20 if (parent[node] != node) {
21 // Path compression: make every node point directly to root
22 parent[node] = findRoot(parent[node]);
23 }
24 return parent[node];
25 };
26
27 // Process each edge in ascending order of cost
28 for (const auto& edge : connections) {
29 // Convert to 0-indexed (problem uses 1-indexed nodes)
30 int nodeA = edge[0] - 1;
31 int nodeB = edge[1] - 1;
32 int edgeCost = edge[2];
33
34 int rootA = findRoot(nodeA);
35 int rootB = findRoot(nodeB);
36
37 // Skip if nodes are already in the same component (would create cycle)
38 if (rootA == rootB) {
39 continue;
40 }
41
42 // Union operation: connect the two components
43 parent[rootA] = rootB;
44
45 // Add edge cost to total
46 totalCost += edgeCost;
47
48 // Decrease component count
49 componentsCount--;
50
51 // If all nodes are connected (only 1 component left), return result
52 if (componentsCount == 1) {
53 return totalCost;
54 }
55 }
56
57 // If we couldn't connect all nodes, return -1
58 return -1;
59 }
60};
61
1/**
2 * Finds the minimum cost to connect all cities using Kruskal's algorithm with Union-Find
3 * @param n - Number of cities (1-indexed)
4 * @param connections - Array of connections where each connection is [city1, city2, cost]
5 * @returns Minimum cost to connect all cities, or -1 if impossible
6 */
7function minimumCost(n: number, connections: number[][]): number {
8 // Parent array for Union-Find data structure (0-indexed)
9 const parent: number[] = Array.from({ length: n }, (_, index) => index);
10
11 /**
12 * Find operation with path compression for Union-Find
13 * @param x - Node to find the root of
14 * @returns Root of the set containing x
15 */
16 const find = (x: number): number => {
17 if (parent[x] !== x) {
18 // Path compression: make every node point directly to root
19 parent[x] = find(parent[x]);
20 }
21 return parent[x];
22 };
23
24 // Sort connections by cost in ascending order (greedy approach)
25 connections.sort((a, b) => a[2] - b[2]);
26
27 let totalCost = 0;
28 let remainingComponents = n; // Track number of connected components
29
30 // Process each connection in order of increasing cost
31 for (const [city1, city2, cost] of connections) {
32 // Convert to 0-indexed and find roots
33 const root1 = find(city1 - 1);
34 const root2 = find(city2 - 1);
35
36 // Skip if cities are already connected (would create a cycle)
37 if (root1 === root2) {
38 continue;
39 }
40
41 // Union operation: connect the two components
42 parent[root1] = root2;
43 totalCost += cost;
44 remainingComponents--;
45
46 // If all cities are connected (only 1 component left), return result
47 if (remainingComponents === 1) {
48 return totalCost;
49 }
50 }
51
52 // Not all cities could be connected
53 return -1;
54}
55
Time and Space Complexity
Time Complexity: O(m × log m + m × α(n))
which simplifies to O(m × log m)
The time complexity is dominated by:
- Sorting the connections array:
O(m × log m)
wherem
is the number of edges (connections) - The for loop iterates through all
m
connections:O(m)
- Each iteration performs
find()
operations which takeO(α(n))
amortized time with path compression, whereα
is the inverse Ackermann function (effectively constant for practical purposes) - Overall:
O(m × log m + m × α(n)) ≈ O(m × log m)
since the sorting term dominates
Space Complexity: O(n)
The space complexity comes from:
- The parent array
p
which storesn
elements:O(n)
- The sorting operation uses
O(log m)
space for the recursive call stack in Python's Timsort - The recursion depth of
find()
can be at mostO(n)
in the worst case before path compression, but with path compression it becomes much smaller - Overall:
O(n)
as the parent array dominates the space usage
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Convert Between 1-indexed and 0-indexed
One of the most common mistakes is forgetting that cities are numbered from 1 to n, while array indices typically start from 0.
Incorrect Implementation:
def minimumCost(self, n: int, connections: List[List[int]]) -> int:
parent = list(range(n))
for city1, city2, cost in connections:
# WRONG: Using 1-indexed cities directly as array indices
root1 = find(city1) # This will cause IndexError when city1 = n
root2 = find(city2)
Why it fails: When accessing parent[n]
with a parent array of size n (indices 0 to n-1), you'll get an IndexError.
Correct Implementation:
for city1, city2, cost in connections: # Convert to 0-indexed before using as array indices city1_idx = city1 - 1 city2_idx = city2 - 1 root1 = find(city1_idx) root2 = find(city2_idx)
2. Incorrect Union Operation Without Finding Roots
Another frequent error is directly unioning the nodes instead of their roots, which doesn't properly merge the components.
Incorrect Implementation:
if find(city1_idx) != find(city2_idx): # WRONG: Connecting nodes directly instead of their roots parent[city1_idx] = city2_idx total_cost += cost num_components -= 1
Why it fails: This creates inconsistent parent relationships. Nodes in the same tree might not recognize they're connected because their roots weren't properly unified.
Correct Implementation:
root1 = find(city1_idx) root2 = find(city2_idx) if root1 != root2: # Connect the roots of the two components parent[root1] = root2 total_cost += cost num_components -= 1
3. Not Tracking Component Count Correctly
Some implementations try to count components at the end instead of tracking them during the process.
Incorrect Implementation:
def minimumCost(self, n: int, connections: List[List[int]]) -> int:
# ... union-find operations ...
# WRONG: Trying to count components at the end
components = len(set(find(i) for i in range(n)))
return total_cost if components == 1 else -1
Why it fails: This approach works but is inefficient and can be error-prone if the find function isn't implemented correctly or if path compression isn't applied consistently.
Correct Implementation:
num_components = n # Start with n separate components for city1, city2, cost in connections: # ... find roots ... if root1 != root2: parent[root1] = root2 total_cost += cost num_components -= 1 # Track as we merge if num_components == 1: # Early termination return total_cost return -1 # Not all cities connected
The early termination optimization also improves efficiency by avoiding processing unnecessary edges once all cities are connected.
Which algorithm should you use to find a node that is close to the root of the tree?
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
Minimum Spanning Tree Forests The Umbristan Department of Forestry UDF is tackling a rather difficult problem and the Umbristan government has assigned you as one of its best workers to go resolve the issue When you arrive you are quickly briefed on the problem at hand Inside the Umbristan National Park there exists an
Want a Structured Path to Master System Design Too? Don’t Miss This!