Start learning free

LESSON 487 OF 543 · GRAPHS

Min Cost to Connect All Points in Rust: Kruskal 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 27
    Ctrl + Enter to check

    RUST · GRAPHS

    Min Cost To Connect All Points in Rust, explained

    Problem overview

    The function min_cost_connect_points receives a vector of points, each an [x, y] pair. Connecting two points costs their Manhattan distance, |x1 - x2| + |y1 - y2|. The function returns the minimum total cost of connections that make all the points reachable from one another, directly or through other points.

    For [[0, 0], [1, 0], [0, 1], [10, 10]] the answer is 21. The cheapest connections are [0, 0] to [1, 0] and [0, 0] to [0, 1], 1 each. Joining [1, 0] and [0, 1] (cost 2) would only repeat a link that already exists through [0, 0]. Finally [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, weighted by the 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 join every point at the least total cost.

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

    Taking the cheapest edge between two groups is always safe. The finished tree must connect 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.

    To ask "are these two points in the same group?" quickly, the code uses union-find. Each group is a tree of parent links, 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. Merging by size and shortening the paths as they are walked keeps each lookup nearly constant.

    The sorted edges for the example above, as [cost, i, j], are:

    [1, 0, 1]   take, total 1
    [1, 0, 2]   take, total 2
    [2, 1, 2]   skip: points 1 and 2 are already connected through point 0
    [19, 1, 3]  take, total 21
    [19, 2, 3]
    [20, 0, 3]
    

    After the third edge is taken, used reaches n - 1 = 3 and the function returns 21 without looking at the last two edges.

    Prim's algorithm is another classic way to find a minimum spanning tree. It grows one tree from a starting point, which suits a graph where every pair is an edge. This lesson uses Kruskal's.

    Step by step

    1. Store the number of points in n.
    2. Create edges, an empty Vec<Vec<i32>>. Each edge is a vector of cost, i and j. The cost comes first because it is what the sort uses.
    3. Loop over i in 0..n. With a second loop inside it, the two indexes visit every pair.
    4. Loop over j in i + 1..n. 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 the Manhattan distance as the absolute x difference plus the absolute y difference. abs() is a method on the number, so it goes after the parenthesized difference.
    6. Push the edge as vec![cost, i as i32, j as i32]. A vector holds one type, so the indexes are cast to the cost's type.
    7. Sort the edges with sort_by_key and a closure returning edge[0]. This is the greedy order of Kruskal's algorithm.
    8. Create parent as a Vec<usize> holding 0 to n - 1, by collecting the range 0..n. Every point starts as the root of a group of one.
    9. Create size as n ones. For a root r, size[r] is the number of points in its group.
    10. Create total at 0, the cost of the edges taken so far.
    11. Create used at 0, the number of edges taken. A tree over n points has exactly n - 1 edges.
    12. Loop over &edges, cheapest first. For each edge, edge[0] is the cost and edge[1] and edge[2] are the points.
    13. Find the root of edge[1] with Self::find(&mut parent, edge[1] as usize), in a mutable root_a. The helper is written at the end.
    14. Find the root of edge[2] the same way, in root_b.
    15. Take the edge only if root_a != root_b. Equal roots mean the edge would close a cycle.
    16. Swap the roots if size[root_a] < size[root_b] with std::mem::swap, so that root_a is the root of the larger group. This is union by size.
    17. Set parent[root_b] to root_a, hanging the smaller tree under the larger one.
    18. Add size[root_b] to size[root_a]. The merged group has the points of both.
    19. Add edge[0] to total. This is the key line: the edge is the cheapest one left that joins two groups.
    20. Add 1 to used.
    21. Return total once used == n - 1. Every point is in one group, and the edges left could only close cycles.
    22. After the loop, return total. The loop ends here only for a single point: it has no edges and costs nothing.
    23. Write the helper find inside the impl, taking parent: &mut Vec<usize> and mut x: usize, returning the root of x's group.
    24. Loop while parent[x] != x, that is, while x is not a root. A loop needs no call stack, however long the walk.
    25. Point x at its grandparent: parent[x] = parent[parent[x]]. This is path halving: each node passed skips one level, so later walks are shorter.
    26. Move x up to parent[x]. It is the grandparent just assigned, so each turn climbs two levels.
    27. Return x, which is the root.

    The solution in Rust

    impl Solution {
        fn min_cost_connect_points(points: Vec<Vec<i32>>) -> i32 {
            let n = points.len();
            let mut edges: Vec<Vec<i32>> = Vec::new();
            for i in 0..n {
                for j in i + 1..n {
                    let cost = (points[i][0] - points[j][0]).abs() + (points[i][1] - points[j][1]).abs();
                    edges.push(vec![cost, i as i32, j as i32]);
                }
            }
            edges.sort_by_key(|edge| edge[0]);
            let mut parent: Vec<usize> = (0..n).collect();
            let mut size = vec![1; n];
            let mut total = 0;
            let mut used = 0;
            for edge in &edges {
                let mut root_a = Self::find(&mut parent, edge[1] as usize);
                let mut root_b = Self::find(&mut parent, edge[2] as usize);
                if root_a != root_b {
                    if size[root_a] < size[root_b] {
                        std::mem::swap(&mut root_a, &mut root_b);
                    }
                    parent[root_b] = root_a;
                    size[root_a] += size[root_b];
                    total += edge[0];
                    used += 1;
                    if used == n - 1 {
                        return total;
                    }
                }
            }
            total
        }
    
        fn find(parent: &mut Vec<usize>, mut x: usize) -> usize {
            while parent[x] != x {
                parent[x] = parent[parent[x]];
                x = parent[x];
            }
            x
        }
    }
    

    The Rust details worth a closer look:

    • The helper has no self. find is an associated function, so it is called as Self::find(...), with the path Self:: naming the type the impl block belongs to.
    • &mut parent. find changes the parent links while walking, so the vector is lent mutably. Inside find, mut x only lets the function reassign its own copy of the number. The caller's variable is untouched.
    • std::mem::swap(&mut root_a, &mut root_b) exchanges the two values through mutable references. That is why root_a and root_b are declared let mut.
    • Two integer types. Points and edges store indexes as i32 inside edges, so a vector can hold the cost and the two indexes together. Every use as an index goes through as usize. The vectors parent and size use usize, and used == n - 1 compares two usize values.
    • (0..n).collect() gathers the numbers of the range into a collection. The type written on parent, Vec<usize>, tells collect what to build.
    • sort_by_key sorts in place by the key that the closure returns for each element. It is a stable sort, so edges with equal cost keep their order. Any order among equal edges gives the same minimum total.
    • Overflow. A single cost is at most 4,000,000 and the total of a spanning tree stays far below the i32 limit of about 2.1 billion for 1,000 points. In the -O builds the lessons use, an overflow would wrap silently instead of panicking.

    Complexity

    Time: O(n² log n). There are about n² / 2 edges, and sorting them takes O(n² log n). The loop over the sorted edges makes two find calls per edge, each nearly constant time thanks to union by size and path halving, so it costs O(n²) times a very small factor, which the sort dominates.

    Space: O(n²). Storing every edge takes space proportional to the number of pairs. For 1,000 points that is just under half a million edges. parent and size add O(n).

    Edge cases and common mistakes

    • A single point. [[5, 5]] returns 0. There are no edges, the loop never runs, and the final total is 0.
    • Two points. [[0, 0], [3, 4]] returns 7, the Manhattan distance, not the straight-line distance of 5.
    • Negative coordinates. The absolute value handles them: [[3, 12], [-2, 5], [-4, 1]] returns 18.
    • Large coordinates. [[-1000000, -1000000], [1000000, 1000000]] returns 4000000. The difference of two coordinates is at most 2,000,000, which fits comfortably in an i32.
    • Skipping the cycle check. Taking the cheapest n - 1 edges without checking the groups can pick edges that connect the same points twice. In the example, the first three edges would give a total of 4 and leave [10, 10] out.
    • Forgetting to stop. The loop would still be correct without the used == n - 1 exit, since later edges are all skipped, but it would do needless work on a large input.
    • Comparing points instead of roots. Two points are connected when their roots are equal, not when they are equal or neighbours in the parent vector. Always call find on both.

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