A Systematic Approach to Algorithms

Vijay K. Garg · The University of Texas at Austin

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;
}

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;
}