MST correctness
Why the greedy algorithms are right: the exchange argument, the cut argument, the safe-edge choice at every step, and a comparison of what each algorithm costs.
Learning goals
- State the optimality condition of a spanning tree in terms of cycles
- State it in terms of cuts
- Explain why each greedy step is safe
- Choose between Kruskal, heap Prim and array Prim from n and m
Prerequisites
A greedy algorithm that always grabs the cheapest option usually fails. Spanning trees are a famous case where it works, and the reason is two clean facts about exchanging one edge for another.
Exchange proof
**Exchange argument.** A spanning tree T is minimum if and only if, for every non-tree edge e, w(e) is at least the weight of every edge on the tree path between its ends. If some tree edge on that path were heavier than e, exchanging them would give a lighter spanning tree, a contradiction; and if no exchange helps, no sequence of exchanges does, because any spanning tree can be turned into any other by single swaps.
The check below is exactly that condition, and it is used as a test for any tree you want to verify. The *Cycle property* animation option runs it for each rejected edge of Kruskal's tree.
1static boolean isMinimumSpanningTree(int n, int[][] edges, int[] tree) { // tree = indexes of n - 1 edges2if (tree.length != n - 1) return false;3int[] parent = new int[n];4for (int i = 0; i < n; i++) parent[i] = i;5boolean[] inTree = new boolean[edges.length];6List<int[]>[] adj = new List[n];7for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();8for (int k : tree) {9if (inTree[k]) return false;10int a = find(parent, edges[k][0]), b = find(parent, edges[k][1]);11if (a == b) return false; // not even a tree12parent[a] = b;13inTree[k] = true;14adj[edges[k][0]].add(new int[]{edges[k][1], edges[k][2]});15adj[edges[k][1]].add(new int[]{edges[k][0], edges[k][2]});16}17for (int k = 0; k < edges.length; k++)18if (!inTree[k] && maxOnPath(adj, edges[k][0], edges[k][1], -1) > edges[k][2]) return false; // swapping k in gives a lighter tree19return true;20}21static int maxOnPath(List<int[]>[] tree, int u, int target, int from) {22if (u == target) return 0;23for (int[] e : tree[u]) {24if (e[0] == from) continue;25int sub = maxOnPath(tree, e[0], target, u);26if (sub >= 0) return Math.max(sub, e[1]);27}28return -1;29}30static int find(int[] p, int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }
MST properties and variations: cut, cycle, second best, critical edges
Interactive · edit the inputundirected graph · 5 vertices · 7 edges
side S
A cut splits the vertices into S = {0..1} and the rest. The edges with one end on each side cross the cut.
1static long secondBestMst(int n, int[][] edges) { // connected graph; Long.MAX_VALUE when no other spanning tree exists2int[][] sorted = edges.clone();3Arrays.sort(sorted, Comparator.comparingInt(e -> e[2]));4int[] parent = new int[n];5for (int i = 0; i < n; i++) parent[i] = i;6boolean[] inTree = new boolean[sorted.length];7List<int[]>[] tree = new List[n];8for (int i = 0; i < n; i++) tree[i] = new ArrayList<>();9long total = 0;10for (int i = 0; i < sorted.length; i++) {11int a = find(parent, sorted[i][0]), b = find(parent, sorted[i][1]);12if (a == b) continue;13parent[a] = b; inTree[i] = true; total += sorted[i][2];14tree[sorted[i][0]].add(new int[]{sorted[i][1], sorted[i][2]});15tree[sorted[i][1]].add(new int[]{sorted[i][0], sorted[i][2]});16}17long best = Long.MAX_VALUE;18for (int i = 0; i < sorted.length; i++) {19if (inTree[i]) continue;20int heaviest = maxOnPath(tree, sorted[i][0], sorted[i][1], -1); // the swap removes the heaviest tree edge on the cycle21best = Math.min(best, total - heaviest + sorted[i][2]);22}23return best;24}25static List<List<Integer>> criticalEdges(int n, int[][] edges) { // {critical, pseudo-critical} edge indexes26long base = mst(n, edges, -1, -1);27List<Integer> critical = new ArrayList<>(), pseudo = new ArrayList<>();28for (int i = 0; i < edges.length; i++) {29if (mst(n, edges, i, -1) > base) critical.add(i); // without it the tree is heavier or breaks30else if (mst(n, edges, -1, i) == base) pseudo.add(i); // forced in, still minimum31}32return List.of(critical, pseudo);33}
Live state
S
0..1
Execution trace (dry run table)Show 3 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | S=0..1 | A cut splits the vertices into S = {0..1} and the rest. The edges with one end on each side cross the cut. |
| 2 | L1 | crossing=3 | 3 edges cross the cut: 0-2:3, 1-2:1, 1-3:2. |
| 3 | L1 | return=1-2:1 tree contains it=yes | The cut property: the lightest crossing edge, 1-2:1, belongs to some minimum spanning tree. Reason: any tree must cross the cut somewhere; swapping a heavier crossing edge for this one keeps it a tree and cannot raise the weight. Kruskal's tree contains it. |
Cut proof
**Cut argument.** A spanning tree T is minimum if and only if every tree edge is a lightest edge crossing the cut it defines: deleting a tree edge splits T into two parts, and no edge of the graph joining the two parts may be lighter. If a lighter one existed, swapping it in would improve the tree.
Both conditions say the same thing from two sides: cycles ask about non-tree edges, cuts ask about tree edges. The function below tests every tree edge's cut.
1static boolean isMinimumByCuts(int n, int[][] edges, int[] tree) { // each tree edge must be a lightest edge across its own cut2if (tree.length != n - 1) return false;3int[] check = new int[n];4for (int i = 0; i < n; i++) check[i] = i;5boolean[] seen = new boolean[edges.length];6for (int k : tree) {7if (seen[k]) return false;8seen[k] = true;9int a = find(check, edges[k][0]), b = find(check, edges[k][1]);10if (a == b) return false;11check[a] = b;12}13for (int skip : tree) {14int[] parent = new int[n];15for (int i = 0; i < n; i++) parent[i] = i;16for (int k : tree) if (k != skip) parent[find(parent, edges[k][0])] = find(parent, edges[k][1]);17int side = find(parent, edges[skip][0]); // the component of one end after removing the edge18for (int k = 0; k < edges.length; k++) {19boolean x = find(parent, edges[k][0]) == side, y = find(parent, edges[k][1]) == side;20if (x != y && edges[k][2] < edges[skip][2]) return false; // a lighter edge crosses this cut21}22}23return true;24}25static int find(int[] p, int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }
MST properties and variations: cut, cycle, second best, critical edges
Interactive · edit the inputundirected graph · 5 vertices · 7 edges
side S
A cut splits the vertices into S = {0..1} and the rest. The edges with one end on each side cross the cut.
1static long secondBestMst(int n, int[][] edges) { // connected graph; Long.MAX_VALUE when no other spanning tree exists2int[][] sorted = edges.clone();3Arrays.sort(sorted, Comparator.comparingInt(e -> e[2]));4int[] parent = new int[n];5for (int i = 0; i < n; i++) parent[i] = i;6boolean[] inTree = new boolean[sorted.length];7List<int[]>[] tree = new List[n];8for (int i = 0; i < n; i++) tree[i] = new ArrayList<>();9long total = 0;10for (int i = 0; i < sorted.length; i++) {11int a = find(parent, sorted[i][0]), b = find(parent, sorted[i][1]);12if (a == b) continue;13parent[a] = b; inTree[i] = true; total += sorted[i][2];14tree[sorted[i][0]].add(new int[]{sorted[i][1], sorted[i][2]});15tree[sorted[i][1]].add(new int[]{sorted[i][0], sorted[i][2]});16}17long best = Long.MAX_VALUE;18for (int i = 0; i < sorted.length; i++) {19if (inTree[i]) continue;20int heaviest = maxOnPath(tree, sorted[i][0], sorted[i][1], -1); // the swap removes the heaviest tree edge on the cycle21best = Math.min(best, total - heaviest + sorted[i][2]);22}23return best;24}25static List<List<Integer>> criticalEdges(int n, int[][] edges) { // {critical, pseudo-critical} edge indexes26long base = mst(n, edges, -1, -1);27List<Integer> critical = new ArrayList<>(), pseudo = new ArrayList<>();28for (int i = 0; i < edges.length; i++) {29if (mst(n, edges, i, -1) > base) critical.add(i); // without it the tree is heavier or breaks30else if (mst(n, edges, -1, i) == base) pseudo.add(i); // forced in, still minimum31}32return List.of(critical, pseudo);33}
Live state
S
0..1
Execution trace (dry run table)Show 3 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | S=0..1 | A cut splits the vertices into S = {0..1} and the rest. The edges with one end on each side cross the cut. |
| 2 | L1 | crossing=3 | 3 edges cross the cut: 0-2:3, 1-2:1, 1-3:2. |
| 3 | L1 | return=1-2:1 tree contains it=yes | The cut property: the lightest crossing edge, 1-2:1, belongs to some minimum spanning tree. Reason: any tree must cross the cut somewhere; swapping a heavier crossing edge for this one keeps it a tree and cannot raise the weight. Kruskal's tree contains it. |
Greedy choice
Kruskal's invariant: the edges accepted so far are part of some MST. When the next edge e joins two components, take the cut that separates one of them from everything else. No accepted edge crosses it, and every edge lighter than e was already processed and is either accepted (inside a component) or rejected (inside a component), so e is a lightest crossing edge. By the cut property it can be added: the invariant holds again.
Prim's step is the same, with the cut between the current tree and the rest. The function below checks this safety claim on a concrete graph: for every accepted edge, no edge leaving that component is lighter.
1static boolean everyStepIsSafe(int n, int[][] edges) { // each Kruskal edge is a lightest edge leaving the component it grows2Integer[] order = new Integer[edges.length];3for (int i = 0; i < order.length; i++) order[i] = i;4Arrays.sort(order, Comparator.comparingInt(i -> edges[i][2]));5int[] parent = new int[n];6for (int i = 0; i < n; i++) parent[i] = i;7for (int k : order) {8int a = find(parent, edges[k][0]), b = find(parent, edges[k][1]);9if (a == b) continue;10for (int j = 0; j < edges.length; j++) { // the cut: a's component against everything else11boolean x = find(parent, edges[j][0]) == a, y = find(parent, edges[j][1]) == a;12if (x != y && edges[j][2] < edges[k][2]) return false;13}14parent[a] = b;15}16return true;17}18static int find(int[] p, int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }
Minimum spanning tree: Kruskal and Prim (lazy and eager)
Interactive · edit the inputundirected graph · 6 vertices · 8 edges
C0
C1
C2
C3
C4
C5
edges, lightest first
component leader of v
Sort the edges by weight (ties keep input order). Every vertex starts as its own component; an edge is taken when it joins two different components.
1static long kruskal(int n, int[][] edges) { // edges {u, v, w}; total weight of the minimum spanning forest2int[][] sorted = edges.clone();3Arrays.sort(sorted, Comparator.comparingInt(e -> e[2])); // lightest first4int[] parent = new int[n];5for (int i = 0; i < n; i++) parent[i] = i;6long total = 0;7for (int[] e : sorted) {8int a = find(parent, e[0]), b = find(parent, e[1]);9if (a == b) continue; // already connected: this edge would close a cycle10parent[a] = b;11total += e[2];12}13return total;14}15static int find(int[] p, int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }16static long primLazy(int n, List<int[]>[] adj, int s) { // adj[u] = {v, w}; weight of the tree of s's component17boolean[] in = new boolean[n];18PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]); // {vertex, weight of the edge that reaches it}19pq.add(new int[]{s, 0});20long total = 0;21while (!pq.isEmpty()) {22int[] top = pq.poll();23if (in[top[0]]) continue; // outdated: already in the tree24in[top[0]] = true;25total += top[1];26for (int[] e : adj[top[0]]) if (!in[e[0]]) pq.add(new int[]{e[0], e[1]});27}28return total;29}30static long primEager(int n, List<int[]>[] adj, int s) { // key[v] = cheapest edge from the tree to v31long INF = Long.MAX_VALUE / 2;32long[] key = new long[n];33Arrays.fill(key, INF);34key[s] = 0;35boolean[] in = new boolean[n];36long total = 0;37for (int step = 0; step < n; step++) {38int u = -1;39for (int v = 0; v < n; v++) if (!in[v] && key[v] < INF && (u == -1 || key[v] < key[u])) u = v;40if (u == -1) break; // the rest is unreachable41in[u] = true;42total += key[u];43for (int[] e : adj[u]) if (!in[e[0]] && e[1] < key[e[0]]) key[e[0]] = e[1]; // decrease key44}45return total;46}
Live state
n
6
edges
8
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | n=6 edges=8 | Sort the edges by weight (ties keep input order). Every vertex starts as its own component; an edge is taken when it joins two different components. |
| 2 | L11 | edge=1-2:1 total=1 | 1-2:1 joins two different components: add it to the tree. Total weight 1. |
| 3 | L11 | edge=1-3:2 total=3 | 1-3:2 joins two different components: add it to the tree. Total weight 3. |
| 4 | L11 | edge=0-2:3 total=6 | 0-2:3 joins two different components: add it to the tree. Total weight 6. |
| 5 | L9 | edge=0-1:4 | 0-1:4: 0 and 1 are already connected through the tree, so this edge would close a cycle. Reject it. |
| 6 | L9 | edge=2-3:4 | 2-3:4: 2 and 3 are already connected through the tree, so this edge would close a cycle. Reject it. |
| 7 | L11 | edge=2-4:5 total=11 | 2-4:5 joins two different components: add it to the tree. Total weight 11. |
| 8 | L11 | edge=4-5:6 total=17 | 4-5:6 joins two different components: add it to the tree. Total weight 17. |
| 9 | L9 | edge=3-4:7 | 3-4:7: 3 and 4 are already connected through the tree, so this edge would close a cycle. Reject it. |
| 10 | L13 | return=17 components=1 | The tree has 5 edges and total weight 17 (the smallest possible). |
Complexity comparison
Kruskal costs O(m log m) for the sort plus near-constant union-find work, and it needs an edge list. Prim with a binary heap costs O((n + m) log n) and needs an adjacency list. Prim with an array costs O(n²) and needs no heap. In theory O(m log n) and O(m log m) are the same, since log m <= 2 log n; in practice the constants differ.
As a rule: sparse graph or edges already sorted, Kruskal; dense graph, array Prim; in between, heap Prim. The helper below turns that rule into rough operation counts, a guide rather than a benchmark.
| Algorithm | Time | Input form | Best for |
|---|---|---|---|
| Kruskal | O(m log m) | edge list | sparse graphs, offline queries, edges sorted already |
| Prim with a binary heap | O((n + m) log n) | adjacency list | medium density |
| Prim with an array | O(n²) | matrix or list | dense graphs |
| Borůvka | O(m log n) | edge list | parallel and distributed settings |
1static String suggestMst(int n, int m) { // compares rough operation counts2double logM = Math.log(Math.max(m, 2)) / Math.log(2), logN = Math.log(Math.max(n, 2)) / Math.log(2);3double kruskal = m * logM;4double primHeap = (m + n) * logN;5double primDense = (double) n * n;6if (primDense <= kruskal && primDense <= primHeap) return "Prim, O(n^2) array version";7return kruskal <= primHeap ? "Kruskal" : "Prim with a heap";8}
Practice ladder
Now try it yourself
Easy
- Find if Path Exists in Graph
Medium
- Min Cost to Connect All Points
- Connecting Cities With Minimum Cost
Advanced
- Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree
- Checking Existence of Edge Length Limited Paths
Key takeaways
- A spanning tree is minimum iff no non-tree edge is lighter than an edge on its tree path (cycle condition).
- Equivalently, every tree edge is a lightest edge across the cut it defines (cut condition).
- Each Kruskal and Prim step takes a lightest edge across a cut that respects the tree so far: always safe.
- Kruskal for sparse graphs, array Prim for dense ones, heap Prim in between.