Problem overview
minCostConnectPoints(points) gets an array of [x, y] points. Connecting two points costs their Manhattan distance, |x1 - x2| + |y1 - y2|: the distance along grid lines, like a taxi driving through city blocks. The function returns the least total cost that makes every point reachable from every other by following connections.
For [[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]] the answer is 20: [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. That is four connections for five points, 4 + 3 + 4 + 9 = 20. A single point costs 0.
Approach
Treat every pair of points as an edge of a graph, with the Manhattan distance as its cost. You need the cheapest set of edges that connects all the points. That set is a minimum spanning tree: it has exactly n - 1 edges, and any more would close a cycle that only adds cost.
Kruskal's algorithm builds it greedily:
- List all the edges and sort them from the cheapest.
- Go through them in that order. If the edge's two points are in different groups, take it: the two groups become one. If they are already in the same group, skip it, since it would close a cycle.
- Stop after
n - 1edges, when every point is in one group.
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 gives 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 parent links, and the root, the point that is its own parent, names the group.
There are n · (n - 1) / 2 edges, and sorting them takes O(n² log n) time, which is fine for up to 1000 points. Prim's algorithm is the other classic way to build the tree, growing it from one point, but this lesson uses Kruskal.
Step by step
- Size.
val n = points.size. - The edge list.
mutableListOf<IntArray>(). Each edge is anIntArrayofcost,iandj, with the cost first because that is what the edges are sorted by. - All pairs.
igoes over0 until nandjoveri + 1 until n. Starting ati + 1lists each pair once and never pairs a point with itself. - Cost and edge. The cost is
Math.absof the x difference plusMath.absof the y difference. AddintArrayOf(cost, i, j)toedges. - Sort.
edges.sortBy { it[0] }sorts the list in place, cheapest first, by the first number of each edge. - Union-find arrays.
parent = IntArray(n) { it }makes every point its own root, andsize = IntArray(n) { 1 }counts the points in the group of each root. - Totals.
var totalis the cost so far andvar usedthe number of edges taken. - Go through the edges. For each
edge, findrootAofedge[1]androotBofedge[2]with the helperfind. They arevars because they may be swapped. - Different roots? If
rootA != rootB, the edge joins two groups. Equal roots mean the points are already connected, and the edge is skipped. - Union by size. If
size[rootA] < size[rootB], swap the two roots throughtemp, sorootAis the root of the larger group. Setparent[rootB] = rootAand addsize[rootB]tosize[rootA]. Hanging the smaller tree under the larger keeps the trees shallow. - Count it. This is the key line:
total += edge[0], since the edge is the cheapest left that joins two groups. Thenused++. - Stop early. When
used == n - 1, all points are in one group: returntotal. - After the loop, return
total. This is reached only with a single point: no edges, cost 0. - The helper
find. It copiesnodeinto avar xand, whileparent[x] != x, pointsxto its grandparent and moves up to it. It returnsx, the root.
The solution in Kotlin
class Solution {
fun minCostConnectPoints(points: Array<IntArray>): Int {
val n = points.size
val edges = mutableListOf<IntArray>()
for (i in 0 until n) {
for (j in i + 1 until n) {
val cost = Math.abs(points[i][0] - points[j][0]) + Math.abs(points[i][1] - points[j][1])
edges.add(intArrayOf(cost, i, j))
}
}
edges.sortBy { it[0] }
val parent = IntArray(n) { it }
val size = IntArray(n) { 1 }
var total = 0
var used = 0
for (edge in edges) {
var rootA = find(parent, edge[1])
var rootB = find(parent, edge[2])
if (rootA != rootB) {
if (size[rootA] < size[rootB]) {
val temp = rootA
rootA = rootB
rootB = temp
}
parent[rootB] = rootA
size[rootA] += size[rootB]
total += edge[0]
used++
if (used == n - 1) {
return total
}
}
}
return total
}
fun find(parent: IntArray, node: Int): Int {
var x = node
while (parent[x] != x) {
parent[x] = parent[parent[x]]
x = parent[x]
}
return x
}
}
Kotlin details worth knowing:
- Lambdas with
it. InIntArray(n) { it }, the block runs once per index anditis that index. Inedges.sortBy { it[0] },itis one edge, and the list is sorted by the number the block returns. - Parameters are read-only.
findcannot changenode, so it copies it into thevar x. - Arrays are passed by reference. When
findwrites toparent, the caller sees the change. That is how path halving shortens later lookups. - Swapping two variables. With a
val tempand two assignments, the oldrootAis not lost. - A loop instead of recursion.
findwalks up withwhile, so a long chain cannot overflow the call stack. Math.absis Java'sMath.abs, which Kotlin can call directly.kotlin.math.absworks the same way on anInt.
Tracing an example
For [[0, 0], [1, 0], [0, 1], [10, 10]], the edges sorted by cost, each 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: total 2. The edge [2, 1, 2] links two points that are already in one group, so it is skipped. The edge [19, 1, 3] joins the point [10, 10] and makes the total 21. That is the third edge taken, n - 1 = 3, so the function returns 21.
Complexity
Time: there are about n² / 2 edges. Building them is O(n²), sorting them is O(n² log n), and the union-find calls cost almost constant time each (the cost grows like the inverse Ackermann function), so the sort dominates: O(n² log n).
Space: the edge list holds about n² / 2 small arrays, which is O(n²). For 1000 points that is 499,500 edges.
Edge cases and common mistakes
- One point. There are no edges, the loop never runs and the function returns 0.
- Two points. One edge, whose cost is the Manhattan distance:
[[0, 0], [3, 4]]gives 7. - Negative coordinates. The differences can be negative, so the absolute value is needed:
[[3, 12], [-2, 5], [-4, 1]]gives 18. - Counting skipped edges. Only add the cost when the roots differ. Adding every cheap edge gives a total that is too high.
- Using Euclidean distance. The cost is the Manhattan distance, with no square root.
- Large coordinates. A single difference can be as big as 2,000,000, so one distance is up to 4,000,000, which still fits easily in an
Int. For the two points[-1000000, -1000000]and[1000000, 1000000]the answer is 4000000.
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
