MST variations
Common variants of the MST problem: the second-best tree, edges that are critical or only pseudo-critical, maximum spanning trees, and updating a tree when an edge is added.
Learning goals
- Find the second-best spanning tree by one edge swap
- Classify each edge as critical, pseudo-critical or useless
- Compute a maximum spanning tree
- Update the minimum spanning tree when an edge is added or its weight changes
Prerequisites
Real questions go beyond one cheapest tree: what is the cheapest backup network? which cables can never be dropped? what happens when a new link appears? Each follows from the structure of the tree.
Second-best MST
The **second-best spanning tree** is the cheapest tree different from the minimum one. It always differs from the MST by a single swap: add a non-tree edge e, which creates a cycle, and remove the heaviest tree edge on that cycle. Try every non-tree edge and keep the smallest result: total - heaviestOnPath + w(e).
If the graph has several MSTs, the second-best has the same weight as the first. If the graph is already a tree, no other spanning tree exists. The *Second-best tree* option shows each swap candidate; each costs a path query, O(n) here.
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;14inTree[i] = true;15total += sorted[i][2];16tree[sorted[i][0]].add(new int[]{sorted[i][1], sorted[i][2]});17tree[sorted[i][1]].add(new int[]{sorted[i][0], sorted[i][2]});18}19long best = Long.MAX_VALUE;20for (int i = 0; i < sorted.length; i++) {21if (inTree[i]) continue;22int heaviest = maxOnPath(tree, sorted[i][0], sorted[i][1], -1); // the swap removes the heaviest tree edge on the cycle23best = Math.min(best, total - heaviest + sorted[i][2]);24}25return best;26}27static int maxOnPath(List<int[]>[] tree, int u, int target, int from) { // heaviest edge on the tree path, -1 if target is not below u28if (u == target) return 0;29for (int[] e : tree[u]) {30if (e[0] == from) continue;31int sub = maxOnPath(tree, e[0], target, u);32if (sub >= 0) return Math.max(sub, e[1]);33}34return -1;35}36static 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. |
Critical and pseudo-critical edges
An edge is **critical** if it is in every minimum spanning tree: removing it raises the minimum weight (or disconnects the graph). It is **pseudo-critical** if it is in some MST but not all: forcing it into the tree still gives the minimum weight, yet it is not required. The remaining edges are in no MST.
The direct method: compute the MST weight, then for each edge compute the best weight without it and, if that did not change, the best weight with it forced in. That is O(m) runs of Kruskal. The animation prints the verdict for every edge in input order.
1static List<List<Integer>> criticalEdges(int n, int[][] edges) { // {critical indexes, pseudo-critical indexes}2long base = mst(n, edges, -1, -1);3List<Integer> critical = new ArrayList<>(), pseudo = new ArrayList<>();4for (int i = 0; i < edges.length; i++) {5if (mst(n, edges, i, -1) > base) critical.add(i); // without it the tree is heavier or breaks6else if (mst(n, edges, -1, i) == base) pseudo.add(i); // forced in, still minimum7}8return List.of(critical, pseudo);9}10static long mst(int n, int[][] edges, int skip, int force) { // Kruskal without edge skip, edge force taken first; Long.MAX_VALUE when disconnected11Integer[] order = new Integer[edges.length];12for (int i = 0; i < order.length; i++) order[i] = i;13Arrays.sort(order, Comparator.comparingInt(i -> edges[i][2]));14int[] parent = new int[n];15for (int i = 0; i < n; i++) parent[i] = i;16long total = 0;17int used = 0;18if (force >= 0) {19parent[find(parent, edges[force][0])] = find(parent, edges[force][1]);20total += edges[force][2];21used++;22}23for (int k : order) {24if (k == skip || k == force) continue;25int a = find(parent, edges[k][0]), b = find(parent, edges[k][1]);26if (a == b) continue;27parent[a] = b;28total += edges[k][2];29used++;30}31return used == n - 1 ? total : Long.MAX_VALUE;32}33static 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. |
Maximum spanning tree
A **maximum spanning tree** keeps the same structure, but takes the heaviest edges. Sort in descending order and run Kruskal, or negate every weight and run the minimum version. The cut and cycle properties hold with the inequalities reversed.
A common use is to find the most reliable (or widest) connections, for example the network in which the weakest link is as strong as possible. The fourth option of the first animation shows a maximum spanning tree on the Kruskal graph.
1static long maximumSpanningForest(int n, int[][] edges) { // total weight of the heaviest spanning forest2int[][] sorted = edges.clone();3Arrays.sort(sorted, (a, b) -> Integer.compare(b[2], a[2])); // heaviest 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;10parent[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; }
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). |
MST under edge changes
If the MST is known and **a new edge** (u, v, w) is added, the new minimum tree is either the old tree or the old tree with one swap: if w is smaller than the heaviest edge on the tree path between u and v, replace that heaviest edge by the new one. This is O(n) instead of rerunning Kruskal.
If a **tree edge gets heavier** or a **non-tree edge gets lighter**, similar one-swap repairs apply (the first needs the lightest non-tree edge across the cut that the removed edge defines, the second the heaviest edge on the new edge's cycle). If a non-tree edge gets heavier or a tree edge lighter, nothing changes.
1static long mstAfterAddingEdge(int n, int[][] tree, int[] extra) { // tree: the n - 1 edges {u, v, w} of a minimum spanning tree2List<int[]>[] adj = new List[n];3for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();4long total = 0;5for (int[] e : tree) {6adj[e[0]].add(new int[]{e[1], e[2]});7adj[e[1]].add(new int[]{e[0], e[2]});8total += e[2];9}10int heaviest = maxOnPath(adj, extra[0], extra[1], -1);11return heaviest > extra[2] ? total - heaviest + extra[2] : total; // swap only when the newcomer is lighter12}13static int maxOnPath(List<int[]>[] tree, int u, int target, int from) {14if (u == target) return 0;15for (int[] e : tree[u]) {16if (e[0] == from) continue;17int sub = maxOnPath(tree, e[0], target, u);18if (sub >= 0) return Math.max(sub, e[1]);19}20return -1;21}
Practice ladder
Now try it yourself
Easy
- Find if Path Exists in Graph
Medium
- Min Cost to Connect All Points
- Number of Operations to Make Network Connected
Advanced
- Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree
- Checking Existence of Edge Length Limited Paths
Key takeaways
- Second-best tree: try each non-tree edge, remove the heaviest tree edge on its cycle, keep the best.
- Critical edges are in every MST; pseudo-critical edges are in some; the rest are in none.
- A maximum spanning tree is Kruskal with the order reversed.
- Adding an edge to an MST needs only one swap, found from the path maximum.