Facebook Pixel

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.

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

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:

  1. Sort all connections by cost (cheapest first)
  2. Go through connections one by one
  3. For each connection, check if it connects two different groups of cities
  4. If yes, use this connection and merge the groups
  5. 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 Evaluator

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

  1. Sorting the connections array: O(m × log m) where m is the number of edges (connections)
  2. The for loop iterates through all m connections: O(m)
  3. Each iteration performs find() operations which take O(α(n)) amortized time with path compression, where α is the inverse Ackermann function (effectively constant for practical purposes)
  4. 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:

  1. The parent array p which stores n elements: O(n)
  2. The sorting operation uses O(log m) space for the recursive call stack in Python's Timsort
  3. The recursion depth of find() can be at most O(n) in the worst case before path compression, but with path compression it becomes much smaller
  4. 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.

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

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More