Start learning free

LESSON 484 OF 540 · GRAPHS

Min Cost to Connect All Points in Go: Kruskal's with Union-Find

Practice O(n2 log n) Read the full explanation

You are given an array 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 30
    Ctrl + Enter to check

    GO · GRAPHS

    Min Cost To Connect All Points in Go, explained

    Problem overview

    The function minCostConnectPoints receives a slice points, where each point is an [x, y] pair. Connecting two points costs their Manhattan distance, |x1 - x2| + |y1 - y2|: the distance along the grid lines. The function returns the least total cost that connects all the points, meaning you can get from any point to any other by following connections.

    For [[0,0],[2,2],[3,10],[5,2],[7,0]] the answer is 20, from four connections: [0,0]-[2,2] costs 4, [2,2]-[5,2] costs 3, [5,2]-[7,0] costs 4 and [2,2]-[3,10] costs 9. For [[3,12],[-2,5],[-4,1]] the answer is 18, and for a single point it is 0, because there is nothing to connect.

    Approach: a minimum spanning tree with Kruskal's algorithm

    Think of each pair of points as an edge of a graph, with the Manhattan distance as its cost. There are n · (n - 1) / 2 edges. The cheapest way to connect everything is a minimum spanning tree: n - 1 edges that join all points with the least total cost. A tree has no cycles, and any extra edge would only add cost.

    Kruskal's algorithm builds it greedily. Sort all edges from cheapest to dearest. Go through them in that order and take an edge whenever its two ends are in different groups, which merges the two groups. If both ends are already in the same group, the edge would close a cycle, so skip it. After n - 1 edges everything is in one group, and the loop can stop.

    Taking the cheapest edge between two groups is always safe: the finished tree has to join those two groups somehow, and swapping a dearer connection for the cheaper one would give a tree that costs no more.

    To ask "are these two points in the same group?" quickly, use union-find. Each group is a tree of points. Every point links to a parent, and the root, which is its own parent, names the group. Two points are in the same group exactly when they lead to the same root. With union by size and path halving, one lookup costs almost constant time.

    Example 3, [[0,0],[1,0],[0,1],[10,10]], shows the idea. The six edge costs sorted are 1, 1, 2, 19, 19 and 20. The two edges of cost 1 join [0,0] with [1,0] and with [0,1]. The edge of cost 2, between [1,0] and [0,1], joins two points that are already connected through [0,0], so it is skipped. The next edge of cost 19 attaches [10,10]. That is three edges, n - 1, and the total is 1 + 1 + 19 = 21.

    Prim's algorithm is the other classic way to build a minimum spanning tree. It grows one tree from a starting point, but this lesson uses Kruskal's.

    Step by step

    1. Store the point count as n := len(points).
    2. Create edges as [][]int{}. Each edge will be []int{cost, i, j}, with the cost first because that is the value the edges are sorted by.
    3. Loop over i from 0 to n - 1.
    4. Loop over j from i + 1 to n - 1. This lists each pair once and never pairs a point with itself, because connecting i to j costs the same as j to i.
    5. Compute cost as abs of the x difference plus abs of the y difference. The absolute value makes each difference a distance, whichever point is further left or lower. The lesson provides an integer abs, since math.Abs only works on float64.
    6. Append the edge and store the result back in edges.
    7. Sort edges by cost with sort.Slice, passing a function that says whether edges[i][0] < edges[j][0].
    8. Create parent as a slice of n ints. parent[x] will hold the parent of point x.
    9. Create size the same way. size[r] counts the points in the group whose root is r.
    10. Loop over every point to initialise both slices.
    11. Set parent[i] = i. A point that is its own parent is a root, so at the start every point is a group of one.
    12. Set size[i] = 1. make filled the sizes with zeros, so they must be set.
    13. Declare total := 0, the cost of the edges taken so far.
    14. Declare used := 0, the number of edges taken. A tree over n points has exactly n - 1.
    15. Loop over edges with range, cheapest first. edge[0] is the cost, and edge[1] and edge[2] are the two points.
    16. Find the root of edge[1] with find(parent, edge[1]) and keep it in rootA. The helper is written at the end.
    17. Find the root of edge[2] in rootB.
    18. If rootA != rootB, the edge joins two groups, and the network takes it. Equal roots mean the edge would close a cycle.
    19. Make rootA the root of the larger group. If size[rootA] < size[rootB], swap the two with rootA, rootB = rootB, rootA. This is union by size: the smaller tree goes under the larger one, which keeps every path short.
    20. Set parent[rootB] = rootA to merge the groups.
    21. Add size[rootB] to size[rootA]. The merged group has the points of both.
    22. Add edge[0] to total. This is the key line: the edge is the cheapest one left that joins two groups.
    23. Add 1 to used.
    24. Return total when used == n - 1. Every point is in one group, and the remaining edges could only close cycles.
    25. Return total after the loop. That is reached only with a single point: there are no edges, and the cost is 0. With two or more points the function always returns from inside the loop, since every pair is an edge.
    26. Write find(parent []int, x int) int below the main function. It returns the root of x's group.
    27. Loop while parent[x] != x. A Go for with only a condition works like a while, and a loop needs no call stack however long the walk is.
    28. Point x to its grandparent: parent[x] = parent[parent[x]]. This is path halving, and later walks from here take about half the steps.
    29. Move up with x = parent[x].
    30. Return x, the root.

    The solution in Go

    func minCostConnectPoints(points [][]int) int {
        n := len(points)
        edges := [][]int{}
        for i := 0; i < n; i++ {
            for j := i + 1; j < n; j++ {
                cost := abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1])
                edges = append(edges, []int{cost, i, j})
            }
        }
        sort.Slice(edges, func(i, j int) bool {
            return edges[i][0] < edges[j][0]
        })
        parent := make([]int, n)
        size := make([]int, n)
        for i := 0; i < n; i++ {
            parent[i] = i
            size[i] = 1
        }
        total := 0
        used := 0
        for _, edge := range edges {
            rootA := find(parent, edge[1])
            rootB := 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++
                if used == n - 1 {
                    return total
                }
            }
        }
        return total
    }
    
    func find(parent []int, x int) int {
        for parent[x] != x {
            parent[x] = parent[parent[x]]
            x = parent[x]
        }
        return x
    }
    

    The Go details worth a closer look:

    • Edges as []int. Each edge is a slice of three ints. A struct would be clearer in a larger program, but a slice keeps the sort code short here.
    • sort.Slice. It takes the slice and a function that receives two positions and reports whether the element at the first should come before the one at the second. It is not stable: edges of equal cost can end up in either order. That is fine, because every minimum spanning tree has the same total cost.
    • Swapping in one line. rootA, rootB = rootB, rootA evaluates the whole right side before it assigns, so no temporary variable is needed.
    • Slices share their memory. find receives parent as a slice, which points to the same underlying array. When find halves a path, minCostConnectPoints sees the change. Assigning to the parameter x, in contrast, changes only find's own copy of the number.
    • Iteration, not recursion. find uses a loop, so a long chain of parents can't overflow the call stack.
    • int is 64 bits. Coordinates reach 1,000,000 in size, so one cost is at most 4,000,000 and the total stays far below the limit.

    Complexity

    Time: O(n² log n). Listing the edges takes O(n²), and sorting n · (n - 1) / 2 of them takes O(n² log n). The loop makes two find calls per edge, each close to constant time with union by size and path halving, so the loop is O(n²) up to that tiny factor and the sort dominates. For 1000 points there are 499,500 edges.

    Space: O(n²). The edge list holds every pair. parent and size need only O(n).

    Edge cases and common mistakes

    • A single point. [[5, 5]] returns 0: there are no edges, and the loop is skipped.
    • Two points. [[0, 0], [3, 4]] returns 7, the Manhattan distance, not the straight-line 5.
    • Far-apart points. [[-1000000, -1000000], [1000000, 1000000]] returns 4000000, which fits easily in an int.
    • Points on a line. [[0,0],[0,1],[0,2],[0,3]] returns 3: the tree chains neighbours.
    • Using math.Abs. It takes and returns float64, so using it on ints needs conversions in both directions. A small integer abs avoids that.
    • Taking edges without the group check. Some cheap edges close a cycle. In example 3 the edge of cost 2 joins two points that are already connected, and taking it would only add cost to the total.
    • Counting an edge twice. Starting j at 0 instead of i + 1 adds self-edges of cost 0 and every pair twice. The self-edges are harmless to the answer, because both ends share a root, but they double the list and the sorting work.
    • Stopping at n edges. A tree has n - 1. Checking used == n - 1 stops the loop at the right time.

    Testing

    • [[0,0],[2,2],[3,10],[5,2],[7,0]] gives 20.
    • [[3,12],[-2,5],[-4,1]] gives 18.
    • [[0,0],[1,0],[0,1],[10,10]] gives 21.
    • [[5,5]] gives 0.
    • [[0,0],[3,4]] gives 7.
    • [[-1000000,-1000000],[1000000,1000000]] gives 4000000.
    • [[0,0],[0,1],[0,2],[0,3]] gives 3.
    Keep going with Go500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free