Start learning free

LESSON 480 OF 536 · GRAPHS

Min Cost to Connect All Points in Python: Kruskal and Union-Find

Practice O(n2 log n) Read the full explanation

You are given a list points of n points on a grid, where points[i] = [xi, yi].

Connecting two points [xi, yi] and [xj, yj] costs their Manhattan distance, |xi - xj| + |yi - yj|: the distance along the grid lines, the way a taxi drives through city blocks.

Return the minimum total cost to connect all the points. The points are all connected when you can get from any point to any other by following connections.

 

Example 1:

Input: points = [[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]]
Output: 20
Explanation: Four connections join the five points for the least total cost:
    [0, 0] - [2, 2] costs 2 + 2 = 4
    [2, 2] - [5, 2] costs 3 + 0 = 3
    [5, 2] - [7, 0] costs 2 + 2 = 4
    [2, 2] - [3, 10] costs 1 + 8 = 9
The total is 4 + 3 + 4 + 9 = 20.

Example 2:

Input: points = [[3, 12], [-2, 5], [-4, 1]]
Output: 18
Explanation: [-2, 5] - [-4, 1] costs 2 + 4 = 6 and [3, 12] - [-2, 5] costs 5 + 7 = 12,
18 in all. Connecting [3, 12] to [-4, 1] instead would cost 7 + 11 = 18 on its own.

Example 3:

Input: points = [[0, 0], [1, 0], [0, 1], [10, 10]]
Output: 21
Explanation: The two cheapest connections are [0, 0] - [1, 0] and [0, 0] - [0, 1], 1 each.
The next cheapest, [1, 0] - [0, 1] (cost 2), joins two points that are already connected
through [0, 0], so it would only add cost. Then [10, 10] joins through [1, 0], at cost
9 + 10 = 19. The total is 1 + 1 + 19 = 21.

 

Constraints:

  • 1 <= points.length <= 1000
  • -106 <= xi, yi <= 106
  • All the points are distinct.

Note:

Your algorithm should run in O(n2 log n) time and use O(n2) extra space.

The tutorial uses Kruskal's algorithm. Think of every pair of points as an edge of a graph, whose cost is the two points' Manhattan distance: there are n · (n - 1) / 2 edges. The cheapest way to connect all the points is a minimum spanning tree: n - 1 edges that connect every point, with the least total cost. Kruskal's algorithm sorts the edges from the cheapest up and takes each edge whose two ends are still in different groups, which joins the two groups into one. An edge whose ends are already in the same group would only close a cycle, so it is skipped. Union-find, as in Number of Connected Components in an Undirected Graph, tells in almost constant time whether two points are in the same group. After n - 1 edges every point is in one group, and the algorithm can stop.

Taking the cheapest edge between two groups is always safe: the finished tree has to join those two groups somehow, and if it used a dearer edge there, swapping in the cheaper one would give a tree that costs no more. Sorting the n2 edges takes O(n2 log n) time, and keeping them takes O(n2) space.

Prim's algorithm is the other classic way: it grows a single tree from one point, each time adding the cheapest edge from the tree to a point outside it, with a heap as in Dijkstra's algorithm or, on a graph where every pair is an edge like this one, with an O(n2) scan.

STEP 1 OF 27
    Ctrl + Enter to check

    PYTHON · GRAPHS

    Min Cost To Connect All Points in Python, explained

    Problem overview

    The function minCostConnectPoints(points) receives a list of n points, each an [x, y] pair. Connecting two points costs their Manhattan distance, |x1 - x2| + |y1 - y2|, the distance along grid lines. The function returns the least total cost that makes all the points connected, meaning you can get from any point to any other by following connections. There are up to 1000 points, with coordinates between -1,000,000 and 1,000,000, all distinct.

    For [[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]] the answer is 20: four connections costing 4, 3, 4 and 9. For [[3, 12], [-2, 5], [-4, 1]] it is 18, and for [[0, 0], [1, 0], [0, 1], [10, 10]] it is 21.

    Approach: Kruskal's algorithm

    Think of every pair of points as an edge of a graph, with the Manhattan distance as its cost. There are n · (n - 1) / 2 edges, which is 499,500 for 1000 points. Connecting all the points as cheaply as possible means choosing n - 1 edges that join everything with the least total cost: a minimum spanning tree. Any more edges would only close a cycle and add cost.

    Kruskal's algorithm builds it greedily. Sort all the edges from cheapest to dearest, then go through them: take an edge if its two ends are in different groups, which joins the two groups into one, and skip it if both ends are already in the same group. After n - 1 edges taken, everything is one group and the work is done.

    Taking the cheapest edge between two groups is always safe. The finished tree has to join those two groups somehow, and if it used a dearer edge there, swapping in the cheaper one gives a tree that costs no more.

    To tell quickly whether two points are in the same group, the lesson uses union-find. Every point has a parent; a point that is its own parent is the root, and the root names the group. Two points are in one group exactly when they lead to the same root. Merging two groups hangs one root under the other.

    The third example shows the skipping. The sorted edges, as (cost, i, j), are:

    [(1, 0, 1), (1, 0, 2), (2, 1, 2), (19, 1, 3), (19, 2, 3), (20, 0, 3)]
    

    Going through them:

    (1, 0, 1)   roots 0 1   take
    (1, 0, 2)   roots 0 2   take
    (2, 1, 2)   roots 0 0   skip
    (19, 1, 3)  roots 0 3   take
    (19, 2, 3)  roots 0 0   skip
    (20, 0, 3)  roots 0 0   skip
    

    Points 1 and 2 are already joined through point 0, so the edge of cost 2 is skipped. The total is 1 + 1 + 19 = 21. (The function itself returns as soon as n - 1 edges are taken, so it stops after the edge (19, 1, 3).)

    The cost is O(n² log n) time, from sorting the edges, and O(n²) space, for keeping them. Prim's algorithm is the other classic approach: it grows one tree from a starting point, always adding the cheapest edge to a new point, and on a graph where every pair is an edge it can run in O(n²) with a plain scan. This lesson uses Kruskal.

    Step by step

    1. Store n = len(points). Kruskal works on edges, so the next steps list every possible connection with its cost.
    2. Create an empty list edges. Each edge will be a tuple (cost, i, j). The cost comes first because the edges get sorted by it.
    3. Loop i over range(n). Together with the next loop, this visits every pair of points.
    4. Loop j over range(i + 1, n) inside it. Starting at i + 1 lists each pair once and never pairs a point with itself, since connecting i to j costs the same as j to i.
    5. Compute cost. It is abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]). The absolute value turns each difference into a distance, whichever point lies further left or lower.
    6. Append (cost, i, j) to edges. The inner parentheses make the tuple and the outer ones belong to append.
    7. Sort with edges.sort(). Tuples compare by their first element first, so the edges end up ordered by cost. Ties are broken by i and j, which doesn't change the total.
    8. Create parent = list(range(n)). Every point is its own parent, so every point starts as the root of a group of one.
    9. Create size = [1] * n. size[r] counts the points in the group whose root is r. Only the roots' entries matter; they decide which tree goes under which.
    10. Start total = 0. It is the cost of the edges taken so far.
    11. Start used = 0. It counts the edges taken. A tree over n points has exactly n - 1 edges.
    12. Loop over edges, cheapest first, with edge. edge[0] is the cost, and edge[1] and edge[2] are the two points.
    13. Find rootA = self.find(parent, edge[1]). The helper find walks up from a point to its root. Assume it exists for now; it is written at the end.
    14. Find rootB = self.find(parent, edge[2]). Two points are in the same group exactly when they lead to the same root.
    15. If rootA != rootB, the edge joins two groups, so take it. Equal roots mean the points are already connected, and the edge would only close a cycle.
    16. Make rootA the root of the larger group. If size[rootA] < size[rootB], swap them with rootA, rootB = rootB, rootA. This is union by size: the smaller tree always goes under the larger. A point moves one level deeper only when its group at least doubles, so no path to a root gets longer than log2(n) steps. The right side of a tuple assignment is computed first, so the swap needs no third variable.
    17. Merge: parent[rootB] = rootA. This line is back at the indentation of the short if, not inside it. Every point of the smaller group now reaches rootA through rootB.
    18. size[rootA] += size[rootB]. The merged group has the points of both. size[rootB] is never read again, because rootB is no longer a root.
    19. total += edge[0]. This is the key line: the edge is the cheapest one left that joins two groups, so it belongs in the network.
    20. used += 1. One more edge in the network.
    21. If used == n - 1, return total. Each edge taken joined two groups into one, so after n - 1 of them the original n groups are one. The edges left could only close cycles.
    22. After the loop, return total. The loop only gets here when there is one point: it has no edges, and one point costs nothing to connect. With two or more points the function always returns from inside the loop, because every pair is an edge.
    23. Write the helper find(self, parent, x) in the class. It returns the root of x's group. A list is passed as the same object, so changes that find makes to parent are seen by minCostConnectPoints.
    24. Loop while parent[x] != x. The root is the only point that is its own parent. A loop needs no call stack, however long the walk.
    25. Set parent[x] = parent[parent[x]]. This is path halving: each point we pass skips a level, so later walks from here take about half the steps. Together with union by size, a call costs O(α(n)) on average, which is nearly constant.
    26. Set x = parent[x]. It moves up to the grandparent it now points to, so each turn climbs two levels.
    27. Return x. The loop stops exactly at the root.

    The solution in Python

    class Solution:
        def minCostConnectPoints(self, points: List[List[int]]) -> int:
            n = len(points)
            edges = []
            for i in range(n):
                for j in range(i + 1, n):
                    cost = abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1])
                    edges.append((cost, i, j))
            edges.sort()
            parent = list(range(n))
            size = [1] * n
            total = 0
            used = 0
            for edge in edges:
                rootA = self.find(parent, edge[1])
                rootB = self.find(parent, edge[2])
                if rootA != rootB:
                    if size[rootA] < size[rootB]:
                        rootA, rootB = rootB, rootA
                    parent[rootB] = rootA
                    size[rootA] += size[rootB]
                    total += edge[0]
                    used += 1
                    if used == n - 1:
                        return total
            return total
    
        def find(self, parent: List[int], x: int) -> int:
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x
    

    Python details:

    • Sorting tuples. edges.sort() sorts the list in place and returns None, so edges = edges.sort() would throw the list away. Tuples compare element by element, which is why the cost comes first.
    • Calling the helper. find is a method, so inside the class it is called as self.find(...), with parent passed explicitly.
    • Tuple swap. rootA, rootB = rootB, rootA exchanges two variables without a temporary one.
    • Exact arithmetic. Manhattan distances of up to 4,000,000 are summed over up to 999 edges, and Python integers never overflow.

    Complexity

    Listing the edges takes O(n²), and sorting n · (n - 1) / 2 of them takes O(n² log n). The union-find work is nearly constant per edge, so it is dominated by the sort. The time is O(n² log n), and the list of edges makes the space O(n²).

    Edge cases and common mistakes

    • One point. minCostConnectPoints([[5, 5]]) is 0: there are no edges, and the loop is skipped.
    • Two points. [[0, 0], [3, 4]] costs 7, the Manhattan distance (3 + 4), not the straight-line distance of 5.
    • Negative coordinates. The abs calls make the sign of the difference irrelevant, as in the second example.
    • Taking every edge. Adding an edge whose ends share a root closes a cycle and adds cost. The root comparison is what prevents it.
    • Comparing the points instead of the roots. Two points can be connected without being each other's parent. Always compare roots.
    • Linking each point to its nearest neighbour. That can leave separate groups, and adding up those costs is not the answer.

    Testing

    minCostConnectPoints([[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]]) -> 20
    minCostConnectPoints([[3, 12], [-2, 5], [-4, 1]])               -> 18
    minCostConnectPoints([[0, 0], [1, 0], [0, 1], [10, 10]])        -> 21
    minCostConnectPoints([[5, 5]])                                  -> 0
    minCostConnectPoints([[0, 0], [3, 4]])                          -> 7
    
    Keep going with Python500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free