A Systematic Approach to Algorithms

Vijay K. Garg · The University of Texas at Austin

Chapter 8. The Minimum Spanning Tree Problem

Edge-inclusion lattices, the cut property, and three classical greedy algorithms recast as LLP fixed points.

This page: LLP forms. View classical forms »

Setting

Given a connected, weighted, undirected graph $G = (V, E, w)$ with $n = |V|$ vertices and $m = |E|$ edges, a minimum spanning tree (MST) is a spanning tree $T \subseteq E$ that minimises $\sum_{e \in T} w(e)$. When edge weights are distinct the MST is unique. The chapter develops three classical algorithms — Kruskal, Prim, Borůvka — each captured by a different greedy invariant on the same edge-inclusion lattice.

Key properties

Two structural lemmas drive every MST algorithm:

Kruskal's Algorithm

Sort edges in non-decreasing weight order. Maintain a union-find structure. Scan the sorted list: for each edge $(u, v)$, if $u$ and $v$ are in different components, accept the edge and merge the components; otherwise reject (it would close a cycle). Sequential running time: $O(m \log n)$ dominated by the sort, plus $O(m \cdot \alpha(n))$ amortised for the union-find operations.

Prim's Algorithm

Pick a starting vertex $v_0$. Maintain a "fragment" containing $v_0$ and a priority queue of crossing edges keyed on weight. Repeatedly extract the lightest crossing edge $(u, v)$ with $u \in F$, $v \notin F$ — add $v$ to $F$ and add $(u, v)$ to the MST. Repeat until $F = V$. With a binary heap, runs in $O((n + m) \log n)$; with a Fibonacci heap, $O(m + n \log n)$. Note the structural similarity to Dijkstra's shortest-path algorithm — same control structure, different relaxation rule.

Borůvka's Algorithm

The oldest of the three (1926) and the most parallel-friendly: every fragment picks its lightest outgoing edge simultaneously, then all picked edges are added at once, fragments are merged, and the process repeats. Each round halves the fragment count, so $O(\log n)$ rounds suffice. Sequential running time: $O(m \log n)$. Parallel variants run in $O(\log n)$ rounds with $O(m)$ work per round on a CRCW PRAM.

The LLP perspective

All three algorithms admit lattice-linear formulations on the edge-inclusion lattice $\{0, 1\}^m$ or, for Prim, on a "chosen edge per vertex" lattice:

LLP-Borůvka is exactly the pointer-jumping kernel; the surrounding loop (alternating between picking minimum-outgoing edges and contracting fragments to stars) lives in the caller. The book develops the full algorithm in detail.

LLP-Kruskal

Edge-inclusion lattice with union-find. Auxiliary find and union methods realise iterative path-halving and root-linking; the parent scratch array is supplied by the caller.

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[] LLPKruskal(int[] u, int[] v, int[] parent) {
  boolean[] C = false;
  forbidden (j) : !C[j] && find(u[j], parent) != find(v[j], parent)  =>
    advance : {
      C[j] = true;
      union(u[j], v[j], parent);
    };
  return C;
}

int find(int x, int[] parent) {
  while (parent[x] != x) {
    parent[x] = parent[parent[x]];
    x = parent[x];
  };
  return x;
}

void union(int a, int b, int[] parent) {
  int ra = find(a, parent);
  int rb = find(b, parent);
  if (ra != rb) {
    parent[ra] = rb;
  }
}

LLP-Prim

Each non-root vertex's chosen edge is captured in $C[j]$ and $\text{parent}[j]$. Auxiliary minCrossCut, argMinCrossCut, globalMinCrossCut, and propagateFixed implement the cross-cut machinery. The forbidden predicate uses the global minimum cross-cut to ensure correctness on multi-vertex frontiers.

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.

double[] LLPPrim(int[] parent, boolean[] fixed,
                 double[][] W, double[] C, int root) {
  forbidden (j) : !fixed[j]
               && argMinCrossCut(j) >= 1
               && minCrossCut(j) <= globalMinCrossCut()
               && C[j] < minCrossCut(j)  =>
    advance : {
      int i = argMinCrossCut(j);
      parent[j] = i;
      C[j] = W[i][j];
      propagateFixed();
    };
  return C;
}

double minCrossCut(int j) {
  double best = infinity;
  int i = 1;
  while (i <= n) {
    if (fixed[i] && W[i][j] < best) { best = W[i][j]; };
    i = i + 1;
  };
  return best;
}

int argMinCrossCut(int j) {
  int besti = 0 - 1;
  double best = infinity;
  int i = 1;
  while (i <= n) {
    if (fixed[i] && W[i][j] < best) {
      best = W[i][j];
      besti = i;
    };
    i = i + 1;
  };
  return besti;
}

double globalMinCrossCut() {
  double best = infinity;
  int j = 1;
  while (j <= n) {
    if (!fixed[j]) {
      double m = minCrossCut(j);
      if (m < best) { best = m; };
    };
    j = j + 1;
  };
  return best;
}

void propagateFixed() {
  boolean changed = true;
  while (changed) {
    changed = false;
    int j = 1;
    while (j <= n) {
      if (!fixed[j] && fixed[parent[j]]) {
        fixed[j] = true;
        changed = true;
      };
      j = j + 1;
    };
  }
}

LLP-Boruvka

The pointer-jumping kernel that contracts a rooted forest into a rooted star in $O(\log n)$ rounds. Forbidden when $G[j] \ne G[G[j]]$ (i.e. $j$ is not yet pointing to its component root); advance is one pointer-jump.

Time complexity: $O((n + m) \log n)$ across $O(\log n)$ rounds of pointer-jumping, where $n$ is the number of vertices and $m$ is the number of edges.

void LLPBoruvka(int[] G) {
  forbidden (j) : G[j] != G[G[j]]  =>
    advance : G[j] = G[G[j]];
}

Constrained MST

A common side condition on MST is that a designated subset $M$ of edges must appear in the chosen spanning tree. The mandatory-edge requirement is itself a one-rule LLP program operating on the same edge-inclusion lattice $\{0, 1\}^m$ used by LLP-Kruskal, and is composed via the Lattice-Linear Language's predicate-conjunction operator $\&\&$. The problem is feasible iff $M$ is acyclic.

Mandatory edges

Forbidden when an edge $e_j \in M$ has not yet been committed ($G[j] = \text{false}$); the advance commits it unless doing so would close a cycle with an already-committed mandatory edge, in which case the program halts ``infeasible'' (returns null). Composed with LLP-Kruskal: $[\,\textsf{LLP-Kruskal}\,\&\&\,\textsf{Mandatory}\,]$.

Time complexity: $O(m \log n)$, identical to LLP-Kruskal up to constant factors; cycle tests are amortised $O(\alpha(n))$ via union-find.

boolean[] MandatoryEdges(int[] u, int[] v, boolean[] M, int[] parent) {
  boolean[] G = false;
  forbidden (j) : M[j] && !G[j] =>
    advance : {
      if (find(u[j], parent) == find(v[j], parent)) return null;
      G[j] = true;
      union(u[j], v[j], parent);
    };
  return G;
}

MandatoryMST

Mandatory-edge constraint for minimum spanning trees: every mandatory edge must be included. Forbidden when a mandatory edge $j$ is not yet selected; the advance includes it (or returns null if it would create a cycle).

// Mandatory-edge constraint for MST: mandatory edges must be included.
// M[j] is true iff edge j is mandatory.

class MandatoryMST {
  void MandatoryMST(boolean[] M, int[] u, int[] v, int[] parent, boolean[] G) {
    forbidden (j) : M[j] && !G[j] =>
      advance : if (find(u[j], parent) == find(v[j], parent)) {
                  return null;
                } else {
                  G[j] = true;
                  union(u[j], v[j], parent);
                };
  }

  int find(int x, int[] parent) {
    while (parent[x] != x) {
      parent[x] = parent[parent[x]];
      x = parent[x];
    };
    return x;
  }

  void union(int a, int b, int[] parent) {
    int ra = find(a, parent);
    int rb = find(b, parent);
    if (ra != rb) {
      parent[ra] = rb;
    }
  }
}

LLP predicates for this chapter

Each LLP program in this chapter defines a state vector $G$ and a forbidden predicate; the algorithm runs until no $j$ is forbidden. The table below lists, for each algorithm, what $G[i]$ represents and the negation of the forbidden clause from the matching .llp source.

Algorithm $G[j]$ Negation of the forbidden clause
LLP-Kruskal $C[e]$ — edge $e$ is in the tree $\forall e = (u, v):\ C[e] \,\vee\, \mathrm{find}(u) = \mathrm{find}(v)$
LLP-Prim $C[j]$ — weight of $j$'s chosen edge $\forall j:\ \mathrm{fixed}[j] \,\vee\, \mathrm{argMinCrossCut}(j) < 1 \,\vee\, \mathrm{minCrossCut}(j) > \mathrm{globalMinCrossCut}() \,\vee\, C[j] \geq \mathrm{minCrossCut}(j)$
LLP-Borůvka $G[j]$ — parent pointer of vertex $j$ $\forall j:\ G[j] = G[G[j]]$