Start learning free

LESSON 479 OF 535 · GRAPHS

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

Practice O(n2 log n) Read the full explanation

You are given a vector 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 29
    Ctrl + Enter to check

    C++ · GRAPHS

    Min Cost To Connect All Points in C++, explained

    Problem overview

    The function minCostConnectPoints receives a vector points where each point is 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 connects all the points, meaning you can get from any point to any other by following connections.

    For [[0, 0], [1, 0], [0, 1], [10, 10]] the answer is 21. The two cheapest connections, [0, 0] to [1, 0] and [0, 0] to [0, 1], cost 1 each. Connecting [1, 0] to [0, 1] (cost 2) would only join two points that are already linked. Then [10, 10] joins through [1, 0] at cost 9 + 10 = 19, for a total of 1 + 1 + 19.

    Approach: Kruskal's algorithm

    Treat every 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 all the points is a minimum spanning tree: n - 1 edges that connect everything with the least total cost, and no cycle.

    Kruskal's algorithm sorts the edges from cheapest to dearest and goes through them. It takes an edge when its two ends are still in different groups, which merges the two groups. If the ends are already in the same group, the edge would only close a cycle and add cost, so it is skipped. After n - 1 edges taken, everything is 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 swapping a dearer edge for the cheaper one would give a tree that costs no more. Sorting the edges takes O(n * n * log n), which is the time the statement allows, and storing them takes O(n * n) space.

    To ask "are these two points in the same group?" quickly, the code uses union-find. Each point has a parent, and a point that is its own parent is the root that names its group. Two points are in the same group exactly when they lead to the same root. Two tricks keep the trees shallow: union by size hangs the smaller tree under the larger, and path halving shortens paths while walking up.

    The six edges of the example above, as {cost, i, j} after sorting:

    {1,0,1} {1,0,2} {2,1,2} {19,1,3} {19,2,3} {20,0,3}

    Kruskal takes the first two, skips {2,1,2} because points 1 and 2 are already joined through point 0, takes {19,1,3}, and stops with three edges. The total is 1 + 1 + 19 = 21.

    Prim's algorithm is the other classic approach: it grows one tree from a starting point. This lesson uses Kruskal.

    Step by step

    1. Store the number of points in n.
    2. Create edges, a vector<array<int, 3>>. Each edge is three ints, {cost, i, j}, with the cost first because that is what the edges are sorted by. An array<int, 3> keeps its three numbers inside itself, so building and sorting half a million of them is faster than with small vectors.
    3. Loop over i, and inside it over j from i + 1. This lists each pair once and never pairs a point with itself.
    4. 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.
    5. Add {cost, i, j} to edges with push_back. The braces build the array in place.
    6. Sort edges by cost with sort and a lambda comparing first[0] < second[0]. The & parameters compare the edges where they are instead of copying them.
    7. Set up union-find. parent is a vector<int> of size n, and size is a vector<int> of n ones, the number of points in the group of each root.
    8. Make every point its own parent in a loop. Each point starts as the root of a group of one.
    9. Create total and used, both 0: the cost so far and the number of edges taken.
    10. Loop over the sorted edges with auto& edge. edge[0] is the cost, and edge[1] and edge[2] are the points.
    11. Find both roots, rootA and rootB, with the helper find, which is written at the end.
    12. If the roots differ, the edge joins two groups. Swap them if needed so that rootA has the larger size, then set parent[rootB] to rootA and add size[rootB] to size[rootA].
    13. Add edge[0] to total and increment used. This is the key line: the edge is the cheapest one left that joins two groups.
    14. When used == n - 1, return total. Every point is in one group, and the remaining edges could only close cycles.
    15. After the loop, return total. It is reached only with a single point, which has no edges and costs nothing.
    16. Write find. While parent[x] != x, set parent[x] to parent[parent[x]] and then move x to parent[x]. When the loop ends, x is the root.

    The solution in C++

    class Solution {
    public:
        int minCostConnectPoints(vector<vector<int>>& points) {
            int n = points.size();
            vector<array<int, 3>> edges;
            for (int i = 0; i < n; i++) {
                for (int j = i + 1; j < n; j++) {
                    int cost = abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]);
                    edges.push_back({cost, i, j});
                }
            }
            sort(edges.begin(), edges.end(), [](auto &first, auto &second) {
                return first[0] < second[0];
            });
            vector<int> parent(n);
            vector<int> size(n, 1);
            for (int i = 0; i < n; i++) {
                parent[i] = i;
            }
            int total = 0;
            int used = 0;
            for (auto& edge : edges) {
                int rootA = find(parent, edge[1]);
                int rootB = find(parent, edge[2]);
                if (rootA != rootB) {
                    if (size[rootA] < size[rootB]) {
                        swap(rootA, rootB);
                    }
                    parent[rootB] = rootA;
                    size[rootA] += size[rootB];
                    total += edge[0];
                    used++;
                    if (used == n - 1) {
                        return total;
                    }
                }
            }
            return total;
        }
    
        int find(vector<int>& parent, int x) {
            while (parent[x] != x) {
                parent[x] = parent[parent[x]];
                x = parent[x];
            }
            return x;
        }
    };
    

    The C++ details worth a closer look:

    • find is declared after it is used. Inside a class, a member function can call another member function that is written further down, because the compiler reads the whole class body before it checks the function bodies.
    • Reference parameter. find takes vector<int>& parent. Without the & each call would receive a copy of the vector, and the path halving would change the copy instead of the real one.
    • auto in a lambda. [](auto &first, auto &second) is a generic lambda, which works since C++14. The compiler deduces that both parameters are array<int, 3>.
    • Ties in the sort. Comparing only first[0] < second[0] orders edges by cost, and edges of equal cost can come in any order. That is fine here: the total of a minimum spanning tree does not depend on how ties are broken.
    • A local named size. The vector size hides the standard library's std::size function inside minCostConnectPoints. That works, but a name such as groupSize would be clearer.
    • int is enough. One edge costs at most 4,000,000, and with at most 1000 points the cheapest tree is far below the 32-bit limit. The tree takes only n - 1 edges, so the total stays small. Adding up every edge of the list would not fit.

    Complexity

    Time: O(n * n * log n). Building the edges takes O(n * n). Sorting about n * n / 2 edges takes O(n * n * log n). Each edge then makes two find calls, which cost almost constant time on average thanks to union by size and path halving.

    Space: O(n * n). The edge list holds one entry per pair of points, about 500,000 entries for 1000 points. parent and size add O(n).

    Edge cases and common mistakes

    • One point. [[5, 5]] returns 0: no edges exist, the loop does nothing and the final return total runs.
    • 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. The coordinates are subtracted before abs, and the differences fit in an int.
    • Skipping cycle edges. Without the rootA != rootB test, every edge would be added and the total would be the sum of all of them.
    • Counting edges. A tree on n points has n - 1 edges. Waiting for n edges never succeeds, so the loop would run through every remaining edge for nothing. The early return is an optimisation, but only when the count is right.
    • Pairing from 0. If j started at 0, every pair would be listed twice and each point would pair with itself at cost 0.
    • Passing parent by value. The code still compiles, but it is slow and path halving no longer changes the real vector.

    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.
    Keep going with C++500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free