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 forbidden/advance loops.

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 minimizes $\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). Running time: $O(m \log n)$ dominated by the sort, plus $O(m \cdot \alpha(n))$ amortized 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. 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 realize 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.

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.

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.

Constrained MST

A common side condition on MST is that a designated subset $M$ of edges must appear in the chosen spanning tree. Unlike the constrained-greedy programs in earlier chapters, the mandatory-edge requirement cannot simply be composed in conjunction with LLP-Kruskal: LLP-Kruskal's forbidden predicate for edge $e_j$ checks connectivity only through lower-indexed edges, so a naive $\&\&$ composition can accept a mandatory edge that closes a cycle with a mandatory edge committed later. Instead the two run in sequence: first commit every mandatory edge (reporting infeasible if $M$ contains a cycle), then run LLP-Kruskal on the full edge set starting from the resulting $G$ — its forbidden predicate now checks connectivity through all committed edges, mandatory and non-mandatory alike. This is equivalent to contracting $M$, computing an MST of the contracted graph, and uncontracting.

Mandatory edges

Phase 1 of the two-step procedure above: 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). Once this terminates, LLP-Kruskal is run on the full edge set starting from the $G$ it produced.

Time complexity: $O(m \log n)$, identical to LLP-Kruskal up to constant factors; cycle tests are amortized $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;
}

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]]$