Kruskal's algorithm
Kruskal's algorithm sorts the edges by weight and adds each edge that joins two different components, using union-find to test; it runs in O(m log m) and handles disconnected graphs by returning a spanning forest.
Learning goals
- Sort edges and process them lightest first
- Use union-find to accept or reject each edge
- Describe how the forest of components grows
- Get a minimum spanning forest on a disconnected graph
Prerequisites
Kruskal's idea is the most direct one: look at the cheapest edge and take it unless it is useless. The cut property proves that this greedy choice is always safe.
Sort edges
Sort all edges by weight, smallest first. Java's Arrays.sort on objects is stable, so edges with equal weights stay in input order; with ties the resulting tree may differ between sorts, but the total weight never does. Sorting costs O(m log m) and dominates the running time.
To find a **maximum** spanning tree, sort heaviest first; nothing else changes. Step through the first animation option to see the sorted edge strip with each edge marked as taken or rejected.
1static int[][] sortedEdges(int[][] edges) { // lightest first; equal weights keep their input order2int[][] sorted = edges.clone();3Arrays.sort(sorted, Comparator.comparingInt(e -> e[2]));4return sorted;5}
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). |
DSU
Kruskal asks, for every edge, *are these two ends already connected through the tree built so far?* A disjoint-set union answers in nearly constant time: find returns the representative of a component, and union merges two components. An edge is accepted only when the two representatives differ, and accepting it merges them.
Path compression and union by size make the cost per operation almost constant, so the whole algorithm is dominated by sorting. The union-find on edges animation shows components, the first redundant edge and Kruskal.
1static class Dsu {2private final int[] parent, size;3int components;4Dsu(int n) {5parent = new int[n];6size = new int[n];7components = n;8for (int i = 0; i < n; i++) { parent[i] = i; size[i] = 1; }9}10int find(int x) {11while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } // path halving12return x;13}14boolean union(int a, int b) { // false when a and b were already together15a = find(a);16b = find(b);17if (a == b) return false;18if (size[a] < size[b]) { int t = a; a = b; b = t; } // attach the smaller tree under the larger19parent[b] = a;20size[a] += size[b];21components--;22return true;23}24}
Union-find on edges: components, redundant edge, Kruskal
Interactive · edit the inputdisjoint-set forest (arrow = parent, roots on top) · 6 nodes
edges in the order given
Start with n components, one per vertex. Every edge whose endpoints are in different sets merges two components (count - 1); an edge inside one set changes nothing.
1int countComponents(int n, int[][] edges) {2DSU d = new DSU(n); int comps = n;3for (int[] e : edges) if (d.union(e[0], e[1])) comps--; // a successful union merges two components4return comps;5}6int[] firstCycleEdge(int n, int[][] edges) { // the first edge whose endpoints are already connected7DSU d = new DSU(n);8for (int[] e : edges) if (!d.union(e[0], e[1])) return e;9return null;10}11long kruskal(int n, int[][] edges) { // edges are {a, b, w}12int[][] sorted = edges.clone();13Arrays.sort(sorted, Comparator.comparingInt(e -> e[2])); // stable: ties keep the input order14DSU d = new DSU(n); long total = 0;15for (int[] e : sorted) if (d.union(e[0], e[1])) total += e[2]; // keep an edge only if it joins two components16return total;17}
Live state
vertices
6
edges
5
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | vertices=6 edges=5 | Start with n components, one per vertex. Every edge whose endpoints are in different sets merges two components (count - 1); an edge inside one set changes nothing. |
| 2 | L3 | edge=0-1 components=5 | Edge 0-1 joins the sets of roots 0 and 1: 5 components left. |
| 3 | L3 | edge=1-2 components=4 | Edge 1-2 joins the sets of roots 0 and 2: 4 components left. |
| 4 | L3 | edge=3-4 components=3 | Edge 3-4 joins the sets of roots 3 and 4: 3 components left. |
| 5 | L3 | edge=2-0 root of a=0 root of b=0 | Edge 2-0: both ends have root 0, so they are already connected. Skip it. |
| 6 | L3 | edge=4-5 components=2 | Edge 4-5 joins the sets of roots 3 and 5: 2 components left. |
| 7 | L4 | return=2 | 2 connected components (provinces). The edges kept form a spanning forest with 4 edges. |
Forest growth
At the start there are n single-vertex trees. Each accepted edge joins two of them, so after k accepted edges there are n - k trees; the algorithm stops being able to accept anything once one tree remains. The picture is a **forest** whose trees merge, and the animation labels every vertex with its component leader.
Every accepted edge is the lightest edge leaving one of the two trees it joins (otherwise a lighter edge would have been processed and accepted earlier), which is the cut property applied to that tree. This is the proof sketch of correctness, expanded in the last lesson of the chapter.
1static List<int[]> kruskalEdges(int n, int[][] edges) { // the chosen edges in the order they are accepted2int[][] 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;6List<int[]> tree = new ArrayList<>();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;11tree.add(e);12if (tree.size() == n - 1) break; // a spanning tree is complete13}14return tree;15}16static 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). |
Disconnected minimum spanning forest
If the graph is not connected, Kruskal needs no change: it accepts edges until none remains that joins two components, which leaves one tree per component, a **minimum spanning forest**. The number of trees is n - (accepted edges).
Be careful with the early exit tree.size() == n - 1: it never triggers on a disconnected graph, which is correct. Prim, in contrast, grows only the component of its start vertex, so for a forest it must be started once in each component. See the two disconnected presets.
1static long[] forestSummary(int n, int[][] edges) { // {number of trees, edges chosen, total weight}2int[][] 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;6int chosen = 0;7long total = 0;8for (int[] e : sorted) {9int a = find(parent, e[0]), b = find(parent, e[1]);10if (a == b) continue;11parent[a] = b;12chosen++;13total += e[2];14}15return new long[]{n - chosen, chosen, total};16}17static 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). |
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
- Number of Operations to Make Network Connected
Advanced
- Optimize Water Distribution in a Village
- Remove Max Number of Edges to Keep Graph Fully Traversable
Key takeaways
- Kruskal: sort edges, take an edge when it joins two different components; O(m log m).
- Union-find answers the connectivity test in nearly constant time.
- After k accepted edges there are n - k trees; the total is the same for any tie-breaking.
- On a disconnected graph Kruskal returns a minimum spanning forest.