Problem overview
The method MinCostConnectPoints receives points, a jagged array of [x, y] pairs. Connecting two points costs their Manhattan distance, |x1 - x2| + |y1 - y2|: the distance along grid lines. The method returns the least total cost that connects all the points, meaning you can get from any point to any other by following connections. There are between 1 and 1000 distinct points, with coordinates up to a million in absolute value.
In the first example, {{0,0},{2,2},{3,10},{5,2},{7,0}}, four connections are enough: (0,0) to (2,2) costs 4, (2,2) to (5,2) costs 3, (5,2) to (7,0) costs 4 and (2,2) to (3,10) costs 9, so the answer is 20. In the third example, {{0,0},{1,0},{0,1},{10,10}}, the answer is 21: two connections of cost 1 and one of cost 19. A connection between (1,0) and (0,1) costs 2 but would only link two points that are already linked through (0,0).
The lesson asks for O(n² log n) time and O(n²) extra space.
Approach: Kruskal's algorithm
Treat every pair of points as an edge of a graph, with the Manhattan distance as its cost. That makes n · (n - 1) / 2 edges. The cheapest way to connect everything is a minimum spanning tree: n - 1 edges that join every point, with the lowest total cost. Fewer edges can't connect n points, and more would close a cycle that only adds cost.
Kruskal's algorithm sorts the edges from cheapest to dearest and takes each edge whose two ends are still in different groups, merging the groups. An edge whose ends are already in the same group is skipped. Taking the cheapest edge between two groups is always safe: the finished tree has to join those groups somehow, and swapping a dearer edge for the cheaper one never raises the total.
The question "are these two points in the same group?" is answered by 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. After n - 1 edges are taken, every point is in one group and the algorithm can stop.
Sorting about n²/2 edges takes O(n² log n), and keeping them takes O(n²) space. Prim's algorithm is another classic approach, growing one tree from a single point. This lesson uses Kruskal's.
Step by step
- Store
points.Lengthinn. - Create
edges, aList<int[]>. Each edge is an array{ cost, i, j }, with the cost first because that is what the edges are sorted by. - Loop over every pair. The outer loop runs
ifrom 0 ton - 1and the inner one runsjfromi + 1. Starting ati + 1lists each pair once and never pairs a point with itself. - Compute the cost as
Math.Absof the x difference plusMath.Absof the y difference, and add the edge to the list. - Sort the edges with
edges.Sortand a lambda that returnsfirst[0] - second[0]. A negative result puts the first edge in front. Costs are at most 4,000,000, so the subtraction can't overflow. - Set up union-find. Create
parentandsize, bothintarrays of lengthn. Then loop over the points and setparent[i]toi(every point is its own root) andsize[i]to 1. - Create
totalandused, both 0: the cost so far and the number of edges taken. - Loop over
edges, cheapest first. Find the roots ofedge[1]andedge[2]withFind, which is written at the end, and store them inrootAandrootB. - If the roots differ, the edge joins two groups. Make
rootAthe root of the larger group by swapping the two with a tuple assignment whensize[rootA] < size[rootB]. This is union by size. - Merge: set
parent[rootB]torootA, addsize[rootB]tosize[rootA], addedge[0]tototaland add one toused. - Stop early. When
used == n - 1, returntotal. The remaining edges could only close cycles. - Return
totalafter the loop. With a single point there are no edges, and the cost is 0. With two or more points the loop always returns from inside, since every pair is an edge. - Write
Find. Whileparent[x] != x, setparent[x]toparent[parent[x]]and thenxtoparent[x]. Returnx.
The solution in C#
class Solution {
public int MinCostConnectPoints(int[][] points) {
int n = points.Length;
List<int[]> edges = new List<int[]>();
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int cost = Math.Abs(points[i][0] - points[j][0]) + Math.Abs(points[i][1] - points[j][1]);
edges.Add(new int[] { cost, i, j });
}
}
edges.Sort((first, second) => first[0] - second[0]);
int[] parent = new int[n];
int[] size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
int total = 0;
int used = 0;
foreach (int[] edge in edges) {
int rootA = Find(parent, edge[1]);
int 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;
}
public int Find(int[] parent, int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
}
The C# details worth knowing:
- Sorting with a lambda.
List<T>.Sortaccepts a comparison, here(first, second) => first[0] - second[0]. It sorts the list in place and returns nothing. The subtraction trick is safe only because the values are small. With larger ones it can wrap: with variables2000000000and-2000000000, an uncheckedintsubtraction gives-294967296, which compares the wrong way.first[0].CompareTo(second[0])has no such risk. - Tuple swap.
(rootA, rootB) = (rootB, rootA);swaps two variables without a temporary. Afterint a = 1, b = 2;the same statement leavesaas 2 andbas 1. - Arrays are references.
Findreceives the caller'sparentarray itself, so the path halving it performs is visible to the caller. The parameterxis anintand is copied, so changing it insideFindleaves the caller's variable alone. - Iteration, not recursion.
Findis a loop, so a long chain of parents can't overflow the call stack. - Arrays of arrays. The input is a jagged
int[][], read aspoints[i][0]andpoints[i][1]. Each edge is also a smallint[], which costs an object per edge: with 1000 points there are 499,500 of them. - Reused names in loops. Both
forloops declare their ownint i, which is allowed because each is scoped to its loop.
Complexity
Time: O(n² log n). Building the edges is O(n²). Sorting about n²/2 edges takes O(n² log n). With union by size and path halving, each Find costs almost constant time, so scanning the edges adds only about O(n²). A run on 1000 random points finishes well within a second.
Space: O(n²). The edge list holds one three-element array per pair. The parent and size arrays add O(n).
Edge cases and common mistakes
- A single point.
{{5,5}}returns 0. No edges exist, so the loop is skipped andtotalis returned as is. - Two points.
{{0,0},{1000000,-1000000}}returns 2000000: one edge, with the full Manhattan distance. - Negative coordinates. The absolute values handle them. In the second example, the edge between (-2,5) and (-4,1) costs 2 + 4 = 6.
- Collinear points.
{{0,0},{0,1},{0,2},{0,3}}returns 3, three edges of cost 1. - Using Euclidean distance. The cost is the Manhattan distance, with no square roots.
- Taking every edge. Without the root comparison, the algorithm would add the cost of all the edges, including those that close cycles.
- Listing edges twice. Starting
jat 0 instead ofi + 1includes each pair twice and pairs each point with itself, which doubles the sort work. - Merging roots incorrectly. Setting the parent of a point instead of its root leaves the rest of its group behind. The merge changes
parent[rootB], notparent[edge[2]]. - Sorting the other way. Taking the dearest edges first gives a spanning tree, but not the cheapest one.
Testing
{{0,0},{2,2},{3,10},{5,2},{7,0}}gives20.{{3,12},{-2,5},{-4,1}}gives18.{{0,0},{1,0},{0,1},{10,10}}gives21.{{5,5}}gives0.{{0,0},{1000000,-1000000}}gives2000000.{{-1000000,-1000000},{1000000,1000000}}gives4000000.{{0,0},{0,1},{0,2},{0,3}}gives3.
