Minimum Spanning Trees
Connect every vertex of a weighted graph at the lowest total cost, greedily, with Kruskal's edge-sorting or Prim's tree-growing.
How It Works
A spanning tree of a connected, undirected graph is a set of its edges that reaches every vertex without forming a cycle. With V vertices it always has exactly V - 1 edges. When the edges have weights, the minimum spanning tree (MST) is the spanning tree with the smallest total weight: the cheapest way to connect everything, whether that's cables between buildings, roads between towns or links between points.
graph minimum spanning tree (0)---1---(1) (0)---1---(1) | \ | | | 4 3 2 4 2 | \ | | | (2)---5---(3)---6---(4) (2) (3)---6---(4)
Why does a greedy choice work? The cut property: split the vertices into any two groups, and the cheapest edge crossing between them belongs to some MST (to every MST, if all weights differ). If a spanning tree leaves that edge out, adding it creates a cycle, and the cycle has to cross the split a second time somewhere. Dropping that other crossing edge, which costs at least as much, gives a spanning tree that's no more expensive.
Both classic algorithms apply that rule over and over, in a different order:
- Kruskal's sorts all the edges and scans them cheapest first, keeping an edge whenever it joins two separate components. Union-find answers "separate?" almost instantly, so the sort dominates at O(E log E), which is the same as O(E log V) because E is less than V². It suits sparse graphs and inputs that arrive as an edge list.
- Prim's grows a single tree from a start vertex, always adding the cheapest edge that leaves the tree. With a heap of candidate edges it runs in O(E log V). With a plain array of distances it runs in O(V²), which wins on dense graphs where E is close to V².
Kruskal's Algorithm, Step by Step
Step 1 of 15
graph
edges
- Edge being checked, or nodes whose root just changed
- The edge's two ends, compared by root
- Kept in the tree
- Rejected: it would close a cycle
The edges arrive in no useful order. Every node starts as its own component, so each one is its own root.
All 15 steps as text
- The edges arrive in no useful order. Every node starts as its own component, so each one is its own root.
- Sort the edges by weight, cheapest first. Kruskal's tries them in this order and keeps each one that joins two separate components.
- The cheapest edge is
0-1at weight1.find(0) = 0andfind(1) = 1differ, so it would join two separate components. - Keep it and merge:
parent[0] = 1, so0now reports root1.totalis1. - Next is
1-3at weight2.find(1) = 1andfind(3) = 3differ, so it would join two separate components. - Keep it and merge:
parent[1] = 3, so0and1now report root3.totalis3. - Next is
0-3at weight3.find(0) = 3andfind(3) = 3: both ends already share a root. - Skip it: the kept edges already join
0and3along0-1-3, so0-3would close a cycle. Those edges came first in the sort, so none of them costs more. - Next is
0-2at weight4.find(0) = 3andfind(2) = 2differ, so it would join two separate components. - Keep it and merge:
parent[3] = 2, so0,1and3now report root2.totalis7. - Next is
2-3at weight5.find(2) = 2andfind(3) = 2: both ends already share a root. - Skip it: the kept edges already join
2and3along2-0-1-3, so2-3would close a cycle. Those edges came first in the sort, so none of them costs more. - Next is
3-4at weight6.find(3) = 2andfind(4) = 4differ, so it would join two separate components. - Keep it and merge:
parent[2] = 4, so0,1,2and3now report root4. That's4edges for5nodes, so stop. - Every node reports root
4, so the4kept edges form one spanning tree:1 + 2 + 4 + 6 = 13, the same total as the overview's tree.
When to Reach for It
- You must connect every node (cities, computers, points) at the lowest total cost.
- The answer is a set of edges with no cycles that reaches every vertex, and only the sum of their weights matters.
- Every pair of points can be linked, at a cost given by a formula. That's a complete graph, which points to Prim's O(V²).
- You need to minimize the most expensive single link used to connect everything. Every MST does that too.
Complexity
| Operation | Time | Space |
|---|---|---|
| Kruskal's (sort plus union-find) | O(E log E) | O(V + E) |
| Prim's with a binary heap | O(E log V) | O(V + E) |
| Prim's with an array (dense graphs) | O(V²) | O(V) |
The Template
Kruskal's Algorithm
Sort the edges by weight, then walk through them with a small disjoint set. An edge whose ends already share a root would close a cycle, so skip it. Otherwise keep it and merge the two components. Stop as soon as you have V - 1 edges, since no spanning tree has more.
edge weight action components afterward
0-1 1 take {0,1} {2} {3} {4}
1-3 2 take {0,1,3} {2} {4}
0-3 3 skip 0 and 3 already connected
0-2 4 take {0,1,2,3} {4}
2-3 5 skip 2 and 3 already connected
3-4 6 take {0,1,2,3,4}: 4 edges, doneEach file carries its own few-line disjoint set: a parent array and a find that uses path halving, pointing each node it passes at its grandparent. That's an iterative cousin of the path compression in Union-Find. There's no union by size: without it each find costs O(log V) amortized, so the finds total O(E log V), which on a connected graph is no more than the O(E log E) the sort already costs.
If the loop runs out of edges before it has V - 1, the graph isn't connected and has no spanning tree. What you get instead is a minimum spanning forest, one tree per component, as the last demo line shows.
Time O(E log E)Space O(V + E)
def kruskal(n: int, edges: list[list[int]]) -> list[list[int]]:
"""Minimum spanning tree of the nodes 0..n-1, as a list of [u, v, weight]
edges. A disconnected graph gets a spanning forest with fewer than n - 1 edges."""
parent = list(range(n)) # A small disjoint set: every node starts alone.
mst = []
# Cheapest edges first. The sort is stable, so equal weights keep their input order.
for u, v, weight in sorted(edges, key=lambda edge: edge[2]):
root_u, root_v = find(parent, u), find(parent, v)
if root_u == root_v:
continue # Already connected: this edge would close a cycle.
parent[root_u] = root_v # Merge the two components.
mst.append([u, v, weight])
if len(mst) == n - 1:
break # A spanning tree has exactly n - 1 edges.
return mst
def find(parent: list[int], x: int) -> int:
"""Root of x's set. Path halving: point each node at its grandparent on the way up."""
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
if __name__ == "__main__":
edges = [[2, 3, 5], [0, 1, 1], [3, 4, 6], [0, 3, 3], [1, 3, 2], [0, 2, 4]]
mst = kruskal(5, edges)
print(mst) # [[0, 1, 1], [1, 3, 2], [0, 2, 4], [3, 4, 6]]
print(sum(weight for _, _, weight in mst)) # 13
# Two separate pieces: only 2 edges for 4 nodes, so there's no spanning tree.
print(kruskal(4, [[0, 1, 3], [2, 3, 2]])) # [[2, 3, 2], [0, 1, 3]]// Minimum spanning tree of the nodes 0..n-1, as a list of [u, v, weight]
// edges. A disconnected graph gets a spanning forest with fewer than n - 1 edges.
export function kruskal(n: number, edges: number[][]): number[][] {
const parent = Array.from({ length: n }, (_, i) => i); // A small disjoint set: every node starts alone.
const mst: number[][] = [];
// Cheapest edges first. The sort is stable, so equal weights keep their input order.
const sorted = [...edges].sort((a, b) => a[2] - b[2]);
for (const [u, v, weight] of sorted) {
const rootU = find(parent, u);
const rootV = find(parent, v);
if (rootU === rootV) continue; // Already connected: this edge would close a cycle.
parent[rootU] = rootV; // Merge the two components.
mst.push([u, v, weight]);
if (mst.length === n - 1) break; // A spanning tree has exactly n - 1 edges.
}
return mst;
}
// Root of x's set. Path halving: point each node at its grandparent on the way up.
function find(parent: number[], x: number): number {
while (parent[x] !== x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
const edges = [[2, 3, 5], [0, 1, 1], [3, 4, 6], [0, 3, 3], [1, 3, 2], [0, 2, 4]];
const mst = kruskal(5, edges);
console.log(JSON.stringify(mst)); // [[0,1,1],[1,3,2],[0,2,4],[3,4,6]]
console.log(mst.reduce((total, edge) => total + edge[2], 0)); // 13
// Two separate pieces: only 2 edges for 4 nodes, so there's no spanning tree.
console.log(JSON.stringify(kruskal(4, [[0, 1, 3], [2, 3, 2]]))); // [[2,3,2],[0,1,3]]package main
import (
"cmp"
"fmt"
"slices"
)
// kruskal returns the minimum spanning tree of the nodes 0..n-1 as a list of
// {u, v, weight} edges. A disconnected graph gets a spanning forest with
// fewer than n - 1 edges.
func kruskal(n int, edges [][3]int) [][3]int {
parent := make([]int, n) // A small disjoint set: every node starts alone.
for i := range parent {
parent[i] = i
}
mst := [][3]int{}
// Cheapest edges first. The sort is stable, so equal weights keep their input order.
sorted := slices.Clone(edges)
slices.SortStableFunc(sorted, func(a, b [3]int) int { return cmp.Compare(a[2], b[2]) })
for _, edge := range sorted {
rootU, rootV := find(parent, edge[0]), find(parent, edge[1])
if rootU == rootV {
continue // Already connected: this edge would close a cycle.
}
parent[rootU] = rootV // Merge the two components.
mst = append(mst, edge)
if len(mst) == n-1 {
break // A spanning tree has exactly n - 1 edges.
}
}
return mst
}
// find returns the root of x's set. Path halving: point each node at its
// grandparent on the way up.
func find(parent []int, x int) int {
for parent[x] != x {
parent[x] = parent[parent[x]]
x = parent[x]
}
return x
}
func main() {
edges := [][3]int{{2, 3, 5}, {0, 1, 1}, {3, 4, 6}, {0, 3, 3}, {1, 3, 2}, {0, 2, 4}}
mst := kruskal(5, edges)
fmt.Println(mst) // [[0 1 1] [1 3 2] [0 2 4] [3 4 6]]
total := 0
for _, edge := range mst {
total += edge[2]
}
fmt.Println(total) // 13
// Two separate pieces: only 2 edges for 4 nodes, so there's no spanning tree.
fmt.Println(kruskal(4, [][3]int{{0, 1, 3}, {2, 3, 2}})) // [[2 3 2] [0 1 3]]
}using System;
using System.Collections.Generic;
using System.Linq;
public class Solution
{
// Minimum spanning tree of the nodes 0..n-1, as a list of [u, v, weight]
// edges. A disconnected graph gets a spanning forest with fewer than n - 1 edges.
public static List<int[]> Kruskal(int n, int[][] edges)
{
int[] parent = Enumerable.Range(0, n).ToArray(); // A small disjoint set: every node starts alone.
var mst = new List<int[]>();
// Cheapest edges first. OrderBy is stable, so equal weights keep their input order.
foreach (int[] edge in edges.OrderBy(e => e[2]))
{
int rootU = Find(parent, edge[0]), rootV = Find(parent, edge[1]);
if (rootU == rootV) continue; // Already connected: this edge would close a cycle.
parent[rootU] = rootV; // Merge the two components.
mst.Add(edge);
if (mst.Count == n - 1) break; // A spanning tree has exactly n - 1 edges.
}
return mst;
}
// Root of x's set. Path halving: point each node at its grandparent on the way up.
static int Find(int[] parent, int x)
{
while (parent[x] != x)
{
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
public static void Main()
{
int[][] edges = { new[] { 2, 3, 5 }, new[] { 0, 1, 1 }, new[] { 3, 4, 6 }, new[] { 0, 3, 3 }, new[] { 1, 3, 2 }, new[] { 0, 2, 4 } };
var mst = Kruskal(5, edges);
Console.WriteLine(Format(mst)); // [[0, 1, 1], [1, 3, 2], [0, 2, 4], [3, 4, 6]]
Console.WriteLine(mst.Sum(edge => edge[2])); // 13
// Two separate pieces: only 2 edges for 4 nodes, so there's no spanning tree.
Console.WriteLine(Format(Kruskal(4, new[] { new[] { 0, 1, 3 }, new[] { 2, 3, 2 } }))); // [[2, 3, 2], [0, 1, 3]]
}
static string Format(List<int[]> edges) =>
"[" + string.Join(", ", edges.Select(e => "[" + string.Join(", ", e) + "]")) + "]";
}import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class Solution {
// Minimum spanning tree of the nodes 0..n-1, as a list of [u, v, weight]
// edges. A disconnected graph gets a spanning forest with fewer than n - 1 edges.
static List<int[]> kruskal(int n, int[][] edges) {
int[] parent = new int[n]; // A small disjoint set: every node starts alone.
for (int i = 0; i < n; i++) parent[i] = i;
List<int[]> mst = new ArrayList<>();
// Cheapest edges first. The sort is stable, so equal weights keep their input order.
int[][] sorted = edges.clone();
Arrays.sort(sorted, Comparator.comparingInt(e -> e[2]));
for (int[] edge : sorted) {
int rootU = find(parent, edge[0]), rootV = find(parent, edge[1]);
if (rootU == rootV) continue; // Already connected: this edge would close a cycle.
parent[rootU] = rootV; // Merge the two components.
mst.add(edge);
if (mst.size() == n - 1) break; // A spanning tree has exactly n - 1 edges.
}
return mst;
}
// Root of x's set. Path halving: point each node at its grandparent on the way up.
static int find(int[] parent, int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
public static void main(String[] args) {
int[][] edges = {{2, 3, 5}, {0, 1, 1}, {3, 4, 6}, {0, 3, 3}, {1, 3, 2}, {0, 2, 4}};
List<int[]> mst = kruskal(5, edges);
System.out.println(format(mst)); // [[0, 1, 1], [1, 3, 2], [0, 2, 4], [3, 4, 6]]
System.out.println(mst.stream().mapToInt(edge -> edge[2]).sum()); // 13
// Two separate pieces: only 2 edges for 4 nodes, so there's no spanning tree.
System.out.println(format(kruskal(4, new int[][] {{0, 1, 3}, {2, 3, 2}}))); // [[2, 3, 2], [0, 1, 3]]
}
static String format(List<int[]> edges) {
return edges.stream().map(Arrays::toString).collect(Collectors.joining(", ", "[", "]"));
}
}Worked Problems
Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.
Problem
You're given points, where points[i] = [x, y] is a spot on a 2D grid. Linking two points costs their Manhattan distance, |x1 - x2| + |y1 - y2|. Return the smallest total cost of links that connects every point, so that any point can reach any other through them.
Example: [[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]] returns 20. One cheapest set of links is (0,0) to (2,2) for 4, (2,2) to (5,2) for 3, (5,2) to (7,0) for 4, and (2,2) to (3,10) for 9.
Show SolutionHide Solution
Approach
This is an MST on a complete graph: any two points can be linked, so n points have n(n - 1) / 2 edges. That count decides the algorithm. Kruskal's has to build and sort every one of them, which for 1,000 points is about 500,000 edges: O(n² log n) time and O(n²) memory. Prim's with a heap also pushes every edge through the heap, for O(n² log n).
The array version of Prim's needs no edge list at all. Keep dist[v], the cheapest link from the tree so far to each outside point. Each round, scan for the closest outside point u, add it to the tree and pay dist[u], then check whether u offers a cheaper link to each point still outside. That's n - 1 rounds of two O(n) scans: O(n²) time and O(n) memory, with each distance computed on the fly.
points: P0 (0,0) P1 (2,2) P2 (3,10) P3 (5,2) P4 (7,0)
add pay dist afterward: P1 P2 P3 P4
P0 0 4 13 7 7
P1 4 . 9 3 7
P3 3 . 9 . 4
P4 4 . 9 . .
P2 9 . . . .
20 in totalWhy does the plain scan beat a heap here? A heap makes finding the minimum cheap, but it charges O(log n) for every edge that might improve a distance, and in a complete graph that's all of them. The array charges O(1) per edge plus O(n) per round to find the minimum, so O(n²) in total, which is linear in the number of edges. When any edge could matter, you can't do better than looking at each one once.
The code starts the tree at point 0 and fills dist from it, so it never needs an infinity value.
Time O(n²)Space O(n)
def min_cost_connect_points(points: list[list[int]]) -> int:
"""Cheapest total Manhattan length of links that connect every point.
Prim's algorithm, array version: O(n²) with no edge list and no heap."""
n = len(points)
in_tree = [False] * n
in_tree[0] = True # Grow the tree from point 0.
# dist[v]: the cheapest link from the tree so far to point v.
dist = [manhattan(points[0], p) for p in points]
total = 0
for _ in range(n - 1):
# Take the closest point outside the tree. Without a heap, that's a plain scan.
u = -1
for v in range(n):
if not in_tree[v] and (u == -1 or dist[v] < dist[u]):
u = v
in_tree[u] = True
total += dist[u]
# The new point may offer a cheaper link to the points still outside.
for v in range(n):
if not in_tree[v]:
dist[v] = min(dist[v], manhattan(points[u], points[v]))
return total
def manhattan(a: list[int], b: list[int]) -> int:
return abs(a[0] - b[0]) + abs(a[1] - b[1])
if __name__ == "__main__":
print(min_cost_connect_points([[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]])) # 20
print(min_cost_connect_points([[3, 12], [-2, 5], [-4, 1]])) # 18
print(min_cost_connect_points([[0, 0]])) # 0// Cheapest total Manhattan length of links that connect every point.
// Prim's algorithm, array version: O(n²) with no edge list and no heap.
export function minCostConnectPoints(points: number[][]): number {
const n = points.length;
const inTree = new Array<boolean>(n).fill(false);
inTree[0] = true; // Grow the tree from point 0.
// dist[v]: the cheapest link from the tree so far to point v.
const dist = points.map((p) => manhattan(points[0], p));
let total = 0;
for (let added = 1; added < n; added++) {
// Take the closest point outside the tree. Without a heap, that's a plain scan.
let u = -1;
for (let v = 0; v < n; v++) {
if (!inTree[v] && (u === -1 || dist[v] < dist[u])) u = v;
}
inTree[u] = true;
total += dist[u];
// The new point may offer a cheaper link to the points still outside.
for (let v = 0; v < n; v++) {
if (!inTree[v]) dist[v] = Math.min(dist[v], manhattan(points[u], points[v]));
}
}
return total;
}
function manhattan(a: number[], b: number[]): number {
return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]);
}
console.log(minCostConnectPoints([[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]])); // 20
console.log(minCostConnectPoints([[3, 12], [-2, 5], [-4, 1]])); // 18
console.log(minCostConnectPoints([[0, 0]])); // 0package main
import "fmt"
// minCostConnectPoints returns the cheapest total Manhattan length of links
// that connect every point. Prim's algorithm, array version: O(n²) with no
// edge list and no heap.
func minCostConnectPoints(points [][]int) int {
n := len(points)
inTree := make([]bool, n)
inTree[0] = true // Grow the tree from point 0.
// dist[v]: the cheapest link from the tree so far to point v.
dist := make([]int, n)
for v, p := range points {
dist[v] = manhattan(points[0], p)
}
total := 0
for added := 1; added < n; added++ {
// Take the closest point outside the tree. Without a heap, that's a plain scan.
u := -1
for v := range n {
if !inTree[v] && (u == -1 || dist[v] < dist[u]) {
u = v
}
}
inTree[u] = true
total += dist[u]
// The new point may offer a cheaper link to the points still outside.
for v := range n {
if !inTree[v] {
dist[v] = min(dist[v], manhattan(points[u], points[v]))
}
}
}
return total
}
func manhattan(a, b []int) int {
return abs(a[0]-b[0]) + abs(a[1]-b[1])
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func main() {
fmt.Println(minCostConnectPoints([][]int{{0, 0}, {2, 2}, {3, 10}, {5, 2}, {7, 0}})) // 20
fmt.Println(minCostConnectPoints([][]int{{3, 12}, {-2, 5}, {-4, 1}})) // 18
fmt.Println(minCostConnectPoints([][]int{{0, 0}})) // 0
}using System;
public class Solution
{
// Cheapest total Manhattan length of links that connect every point.
// Prim's algorithm, array version: O(n²) with no edge list and no heap.
public static int MinCostConnectPoints(int[][] points)
{
int n = points.Length;
var inTree = new bool[n];
inTree[0] = true; // Grow the tree from point 0.
// dist[v]: the cheapest link from the tree so far to point v.
var dist = new int[n];
for (int v = 0; v < n; v++) dist[v] = Manhattan(points[0], points[v]);
int total = 0;
for (int added = 1; added < n; added++)
{
// Take the closest point outside the tree. Without a heap, that's a plain scan.
int u = -1;
for (int v = 0; v < n; v++)
{
if (!inTree[v] && (u == -1 || dist[v] < dist[u])) u = v;
}
inTree[u] = true;
total += dist[u];
// The new point may offer a cheaper link to the points still outside.
for (int v = 0; v < n; v++)
{
if (!inTree[v]) dist[v] = Math.Min(dist[v], Manhattan(points[u], points[v]));
}
}
return total;
}
static int Manhattan(int[] a, int[] b) => Math.Abs(a[0] - b[0]) + Math.Abs(a[1] - b[1]);
public static void Main()
{
Console.WriteLine(MinCostConnectPoints(new[] { new[] { 0, 0 }, new[] { 2, 2 }, new[] { 3, 10 }, new[] { 5, 2 }, new[] { 7, 0 } })); // 20
Console.WriteLine(MinCostConnectPoints(new[] { new[] { 3, 12 }, new[] { -2, 5 }, new[] { -4, 1 } })); // 18
Console.WriteLine(MinCostConnectPoints(new[] { new[] { 0, 0 } })); // 0
}
}public class Solution {
// Cheapest total Manhattan length of links that connect every point.
// Prim's algorithm, array version: O(n²) with no edge list and no heap.
static int minCostConnectPoints(int[][] points) {
int n = points.length;
boolean[] inTree = new boolean[n];
inTree[0] = true; // Grow the tree from point 0.
// dist[v]: the cheapest link from the tree so far to point v.
int[] dist = new int[n];
for (int v = 0; v < n; v++) dist[v] = manhattan(points[0], points[v]);
int total = 0;
for (int added = 1; added < n; added++) {
// Take the closest point outside the tree. Without a heap, that's a plain scan.
int u = -1;
for (int v = 0; v < n; v++) {
if (!inTree[v] && (u == -1 || dist[v] < dist[u])) u = v;
}
inTree[u] = true;
total += dist[u];
// The new point may offer a cheaper link to the points still outside.
for (int v = 0; v < n; v++) {
if (!inTree[v]) dist[v] = Math.min(dist[v], manhattan(points[u], points[v]));
}
}
return total;
}
static int manhattan(int[] a, int[] b) {
return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]);
}
public static void main(String[] args) {
System.out.println(minCostConnectPoints(new int[][] {{0, 0}, {2, 2}, {3, 10}, {5, 2}, {7, 0}})); // 20
System.out.println(minCostConnectPoints(new int[][] {{3, 12}, {-2, 5}, {-4, 1}})); // 18
System.out.println(minCostConnectPoints(new int[][] {{0, 0}})); // 0
}
}Common Pitfalls
- Confusing an MST with shortest paths. The MST minimizes the total of all its edges; the route it gives between two particular vertices can be far from the shortest.
- Forgetting the disconnected case. If Kruskal's ends with fewer than V - 1 edges there is no spanning tree, so return whatever the problem asks for (often
-1). - Building every edge of a complete graph for Kruskal's. With 1,000 points that's about 500,000 edges to store and sort, and Prim's array version needs neither.
- Running these algorithms on a directed graph. They assume undirected edges; the directed version (a minimum spanning arborescence) needs a different algorithm.
- In Prim's, choosing the next vertex without skipping the ones already in the tree, so a vertex gets added, and paid for, twice.
Practice
More problems to try on your own.