Start learning free

LESSON 480 OF 536 · GRAPHS

Min Cost to Connect All Points in JavaScript: Kruskal's Algorithm

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 27
    Ctrl + Enter to check

    JAVASCRIPT · GRAPHS

    Min Cost To Connect All Points in JavaScript, explained

    Problem overview

    The method minCostConnectPoints(points) receives an array of n points, each an [x, y] pair. Connecting two points costs their Manhattan distance, |x1 - x2| + |y1 - y2|: the distance along grid lines, like a taxi driving through city blocks. The method returns the minimum total cost of connections that link all the points, so that you can get from any point to any other by following connections.

    For points = [[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]] the answer is 20: the connections [0,0]-[2,2] (cost 4), [2,2]-[5,2] (3), [5,2]-[7,0] (4) and [2,2]-[3,10] (9) add up to 20. For [[3, 12], [-2, 5], [-4, 1]] the answer is 18, and for [[0, 0], [1, 0], [0, 1], [10, 10]] it is 21.

    Approach: sort all the connections, take the cheap useful ones

    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. 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. A network with fewer edges leaves a point cut off, and one with more has a cycle that only adds cost.

    Kruskal's algorithm builds it greedily. Sort the edges from the cheapest up. Go through them in that order and take an edge only if its 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 close a cycle, so it is skipped. After n - 1 edges taken, 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 for that, swapping in the cheaper one would give a tree that costs no more.

    To ask whether two points are in the same group, the lesson uses union-find. Each point links to a parent, and the root, the one point that is its own parent, names the group. Two points are in the same group exactly when they lead to the same root.

    For the third example, 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]]
    

    The first two edges join points 0, 1 and 2, for 2 in all. The edge [2, 1, 2] links two points that are already connected through point 0, so it is skipped. The edge [19, 1, 3] joins the point [10, 10], and that was the third edge taken, so used reaches n - 1 = 3. The total is 1 + 1 + 19 = 21.

    Sorting about n²/2 edges takes O(n² log n) time and keeping them takes O(n²) space. Prim's algorithm, which grows one tree from a starting point, is the other classic way to solve this problem.

    Step by step

    1. Store n, the number of points.
    2. Create edges, an empty array. Each edge will be [cost, i, j]. The cost comes first because it is what the edges are sorted by.
    3. Loop over the points with i, and inside it loop with j from i + 1. Starting at i + 1 lists each pair once and never pairs a point with itself.
    4. Compute the cost as Math.abs of the x difference plus Math.abs of the y difference. The absolute value makes each difference a distance, whichever point is further left or lower.
    5. Push [cost, i, j] onto edges.
    6. Sort the edges by cost with edges.sort((first, second) => first[0] - second[0]). A negative result puts first before second.
    7. Set up union-find: parent built with Array.from({length: n}, (_, i) => i), so every point starts as its own root, and size as n ones, the number of points in the group of each root.
    8. Declare total and used at 0 with let: the cost so far and the number of edges taken.
    9. Loop over the sorted edges with for...of.
    10. Find both roots: rootA = this.find(parent, edge[1]) and rootB = this.find(parent, edge[2]), declared with let because they may be swapped.
    11. Skip equal roots. Only when rootA !== rootB does the edge join two groups.
    12. Union by size: if size[rootA] < size[rootB], swap the two roots with a destructuring assignment, so that rootA is the root of the larger group.
    13. Merge: set parent[rootB] to rootA, and add size[rootB] to size[rootA].
    14. Count the edge: add edge[0] to total and do used++.
    15. Stop early: once used === n - 1, return total. The edges left could only close cycles.
    16. Return total after the loop. It is reached only for a single point, which has no edges and costs nothing.
    17. Write the helper find(parent, x) inside the class: while parent[x] !== x, set parent[x] to parent[parent[x]] and then x to parent[x]. Then return x.

    The solution in JavaScript

    class Solution {
        minCostConnectPoints(points) {
            const n = points.length;
            const edges = [];
            for (let i = 0; i < n; i++) {
                for (let j = i + 1; j < n; j++) {
                    const cost = Math.abs(points[i][0] - points[j][0]) + Math.abs(points[i][1] - points[j][1]);
                    edges.push([cost, i, j]);
                }
            }
            edges.sort((first, second) => first[0] - second[0]);
            const parent = Array.from({length: n}, (_, i) => i);
            const size = new Array(n).fill(1);
            let total = 0;
            let used = 0;
            for (const edge of edges) {
                let rootA = this.find(parent, edge[1]);
                let rootB = this.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;
        }
    
        find(parent, x) {
            while (parent[x] !== x) {
                parent[x] = parent[parent[x]];
                x = parent[x];
            }
            return x;
        }
    }
    

    The JavaScript details worth knowing:

    • sort needs a comparator. With no function, sort compares elements as strings: [10, 9, 1].sort() gives [1, 10, 9]. For edges, which are arrays, it compares their text forms, so [[10,0,1],[9,1,2],[1,2,3]].sort() gives [[1,2,3],[10,0,1],[9,1,2]]. The comparator first[0] - second[0] returns a negative, zero or positive number, which is all sort needs. Subtracting is safe here because the costs are small integers.
    • Swapping with destructuring. [rootA, rootB] = [rootB, rootA] builds an array of the two values in the other order and unpacks it into the variables, with no temporary. That is why rootA and rootB are let.
    • Array.from with a function. Array.from({length: n}, (_, i) => i) calls the function once per index. The first parameter is the element, which does not exist here, so it is named _ and ignored.
    • Methods called through this. find is a method of the class, so minCostConnectPoints calls it as this.find(...). A bare find(...) would look for a variable named find and fail.
    • Arrays are shared, not copied. find receives the same parent array, so its changes (path halving) are visible to the caller.
    • Path halving. Each step points x at its grandparent, so later walks from the same point take about half the steps. The loop needs no recursion, so there is no call stack to overflow however long the walk is. Together with union by size, each call is almost constant time.

    Complexity

    Time: O(n² log n). Building the edges takes O(n²). Sorting them takes O(n² log n), which dominates. The loop over the edges makes two find calls per edge, each nearly constant time.

    Space: O(n²). The edges array holds n · (n - 1) / 2 entries, about half a million for 1,000 points. parent and size add only O(n).

    Edge cases and common mistakes

    • A single point. [[5, 5]] returns 0. There are no edges, so the loop does nothing and total stays 0. used === n - 1 would be 0 === 0, but it is never tested because no edge exists.
    • Two points. [[0, 0], [1, 1]] returns 2. [[-1000000, -1000000], [1000000, 1000000]] returns 4000000, the largest possible single edge.
    • Negative coordinates. The absolute value makes every difference non-negative, so the order of the points never matters.
    • Sorting without a comparator. The edges would be ordered as text, so a cost of 10 would come before 9, and the greedy choice would be wrong.
    • Skipping the root check. Taking every cheap edge, including ones inside a group, adds cycles and a larger total.
    • Forgetting this. before find. The call fails with a ReferenceError, because no variable named find exists.
    • Comparing points instead of roots. Two points in the same group can have different parents. Only their roots tell whether they are connected.

    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], [1, 1]] gives 2.
    • [[-1000000, -1000000], [1000000, 1000000]] gives 4000000.
    Keep going with JavaScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free