Chapter 8. The Minimum Spanning Tree Problem
Classical sequential MST algorithms (Kruskal, Prim) and the union-find primitive.
This page: Classical forms. View LLP forms »
This page collects the classical / sequential implementations of the algorithms developed in this chapter. The lattice-linear (LLP) reformulations and chapter setup live on the LLP companion page.
Kruskal (sequential)
The classical edge-sorted scan with union-by-rank and path compression. $O(m \log n)$.
Time complexity: $O(m \log m)$ for the edge sort, $O(m \alpha(n))$ for the union-find work, where $n$ is the number of vertices and $m$ is the number of edges.
boolean[] mst(int n, int[] U, int[] V, int[] W) {
int m = U.length;
boolean[] inTree = new boolean[m];
int[] parent = new int[n];
int[] rank = new int[n];
forall i in [0..n-1] : parent[i] = i;
int chosen = 0;
int e = 0;
while (e < m && chosen < n - 1) {
int u = U[e];
int v = V[e];
int ru = root(parent, u);
int rv = root(parent, v);
if (ru != rv) {
inTree[e] = true;
chosen = chosen + 1;
if (rank[ru] < rank[rv]) { parent[ru] = rv; }
else if (rank[ru] > rank[rv]) { parent[rv] = ru; }
else { parent[rv] = ru; rank[ru] = rank[ru] + 1; }
};
e = e + 1;
};
return inTree;
}
int root(int[] parent, int x) {
if (parent[x] != x) { parent[x] = root(parent, parent[x]); };
return parent[x];
}
Prim (sequential)
Linear-scan Prim using a weight matrix; returns parent[], where
parent[v] is the predecessor of $v$ in the MST and is $-1$ for the root.
$O(n^2)$ — slower than the heap variant on sparse graphs but cache-friendly.
Time complexity: $O((n + m) \log n)$ with a binary heap, where $n$ is the number of vertices and $m$ is the number of edges.
int[] mst(int[][] w) {
int n = w.length;
int[] d = new int[n];
int[] parent = new int[n];
boolean[] fixed = new boolean[n];
forall i in [0..n-1] : { d[i] = 2147483647; parent[i] = -1; };
d[0] = 0;
int count = 0;
while (count < n) {
int v = -1;
int best = 2147483647;
int k = 0;
while (k < n) {
if (!fixed[k] && d[k] < best) { v = k; best = d[k]; };
k = k + 1;
};
if (v == -1) { return parent; };
fixed[v] = true;
count = count + 1;
k = 0;
while (k < n) {
if (!fixed[k] && w[v][k] != 2147483647 && w[v][k] < d[k]) {
d[k] = w[v][k];
parent[k] = v;
};
k = k + 1;
}
};
return parent;
}
Boruvka (sequential)
Grows many fragments at once: in each round, every current component finds its cheapest outgoing edge via a full edge scan, all such edges are added together, and components merge. Component identity is recomputed each round by a BFS over the tree edges chosen so far, labelling each vertex with the least-numbered vertex in its component. The number of components at least halves every round, so the algorithm halts within $O(\log n)$ rounds.
Time complexity: $O(m \log n)$ — $O(\log n)$ rounds, each doing $O(m)$ work to find every component's minimum outgoing edge.
boolean[] mst(int n, int[] U, int[] V, int[] W) {
int m = U.length;
boolean[] inTree = new boolean[m];
int[] cid = new int[n];
int treeEdges = 0;
while (treeEdges < n - 1) {
// Component ids: BFS in (V, inTree) gives every vertex the
// least-numbered vertex in its component.
components(n, U, V, inTree, cid);
int[] mwe = new int[n];
double[] dist = new double[n];
forall i in [0..n-1] : mwe[i] = -1;
forall i in [0..n-1] : dist[i] = infinity;
int e = 0;
while (e < m) {
int i = U[e];
int j = V[e];
if (cid[i] != cid[j]) {
if (W[e] < dist[cid[i]]) { dist[cid[i]] = W[e]; mwe[cid[i]] = e; };
if (W[e] < dist[cid[j]]) { dist[cid[j]] = W[e]; mwe[cid[j]] = e; };
};
e = e + 1;
};
forall i in [0..n-1] :
if (cid[i] == i && mwe[i] != -1 && !inTree[mwe[i]]) {
inTree[mwe[i]] = true;
treeEdges = treeEdges + 1;
};
};
return inTree;
}
// Aux: cid[v] := least-numbered vertex reachable from v in (V, inTree).
void components(int n, int[] U, int[] V, boolean[] inTree, int[] cid) {
boolean[] visited = new boolean[n];
forall i in [0..n-1] : visited[i] = false;
int i = 0;
while (i < n) {
if (!visited[i]) { bfs(i, i, n, U, V, inTree, visited, cid); };
i = i + 1;
};
}
// Aux: BFS from `start`, labelling every reached vertex with `root`.
void bfs(int start, int root, int n, int[] U, int[] V, boolean[] inTree, boolean[] visited, int[] cid) {
int[] queue = new int[n];
int head = 0;
int tail = 0;
queue[tail] = start; tail = tail + 1;
visited[start] = true;
cid[start] = root;
while (head < tail) {
int v = queue[head]; head = head + 1;
int e = 0;
while (e < U.length) {
if (inTree[e]) {
int u = neighborIf(U[e], V[e], v);
if (u != -1 && !visited[u]) {
visited[u] = true;
cid[u] = root;
queue[tail] = u; tail = tail + 1;
};
};
e = e + 1;
};
};
}
// Aux: if edge (a,b) touches v, return the other endpoint, else -1.
int neighborIf(int a, int b, int v) {
if (a == v) { return b; };
if (b == v) { return a; };
return -1;
}
UnionFind
The standard disjoint-set data structure with path compression in find
and union-by-rank in union. Both operations are amortised $O(\alpha(n))$
where $\alpha$ is the inverse Ackermann function.
Time complexity: $O(\alpha(n))$ amortised per find / union with path compression and union by rank — effectively constant for all practical $n$.
int find(int[] parent, int x) {
if (parent[x] != x) { parent[x] = find(parent, parent[x]); };
return parent[x];
}
boolean union(int[] parent, int[] rank, int x, int y) {
int rx = find(parent, x);
int ry = find(parent, y);
if (rx == ry) { return false; };
if (rank[rx] < rank[ry]) { parent[rx] = ry; }
else if (rank[rx] > rank[ry]) { parent[ry] = rx; }
else { parent[ry] = rx; rank[rx] = rank[rx] + 1; };
return true;
}