DSA Graphs
Kruskal's Algorithm
A courier planner hands you a CSV of candidate roads and costs, not an adjacency matrix. Sort once. Keep a road only when it merges two components; that is Kruskal with .
plus builds an MST (or a ).
Algorithm journey
Sort edges
Nondecreasing weight.
Each vertex alone.
Scan lightest-first
Union if different components.
Stop at V−1 edges
Or exhaust edges for a forest.
What actually happens
Naive idea: sort edges and take the first V−1 of them. Breaks as soon as those edges form a cycle and leave another vertex isolated.
Better idea: take an edge iff its ends are in different DSU components. That is exactly cycle rejection guided by the cycle property.
The improved design still fails without path compression / union-by-rank on huge E (slow DSU), on directed inputs, or if you assume connectivity when the edge list describes several islands.
Invariant
The accepted edges form a forest contained in some MSF. Each accepted edge merges components and is a lightest edge across that merge cut among remaining candidates in sorted order.
How to choose Kruskal
1. Sparse edge list? Kruskal is the default engineering fit.
2. Dense matrix already in memory? Consider Prim O(V²).
3. Need a forest / offline edges? Kruskal handles disconnection cleanly.
Quantitative check
E=10⁶ edges: sorting dominates (~E log E ≈ 2×10⁷ comparisons). DSU work is comparatively tiny if you use rank + compression.
Interactive demo
Sorted Edges (by weight):
Connected Components:
Algorithm steps
Sort All Edges
Sort all edges in non-decreasing order of their weight. This is the most expensive step!
Initialize Union-Find
Create a Union-Find (Disjoint Set) data structure where each vertex starts in its own set.
Process Edges
For each edge (u, v) in sorted order: if u and v are in different sets (won't create cycle), add edge to MST and union the sets.
Stop When Complete
Stop when MST has V-1 edges. If edges are exhausted first, report the minimum spanning forest and its component count.
🔍 Did You Know? Union-Find Magic
The Union-Find data structure is the secret sauce of Kruskal's algorithm! It allows us to check if two vertices are in the same component and merge components in nearly O(1) time (amortized) using path compression and union by rank. Without it, Kruskal's would be much slower!
💻 Complete Python Implementation
Python
class UnionFind:
"""Union-Find (Disjoint Set) data structure with path compression and union by rank"""
def __init__(self, n):
self.parent = list(range(n)) # Each node is its own parent initially
self.rank = [0] * n # Rank for union by rank optimization
def find(self, x):
"""Find the root of x with path compression"""
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # Path compression
return self.parent[x]
def union(self, x, y):
"""Union two sets containing x and y"""
root_x = self.find(x)
root_y = self.find(y)
if root_x == root_y:
return False # Already in same set
# Union by rank: attach smaller tree under larger tree
if self.rank[root_x] < self.rank[root_y]:
self.parent[root_x] = root_y
elif self.rank[root_x] > self.rank[root_y]:
self.parent[root_y] = root_x
else:
self.parent[root_y] = root_x
self.rank[root_x] += 1
return True
def kruskals_mst(n, edges):
"""
Find Minimum Spanning Tree using Kruskal's Algorithm
Args:
n: Number of vertices (0 to n-1)
edges: List of edges [(u, v, weight), ...]
Returns:
List of edges in MST: [(u, v, weight), ...]
"""
# Step 1: Sort edges by weight
edges.sort(key=lambda x: x[2])
# Step 2: Initialize Union-Find
uf = UnionFind(n)
mst = []
total_weight = 0
# Step 3: Process edges in sorted order
for u, v, weight in edges:
# If u and v are in different components (no cycle)
if uf.union(u, v):
mst.append((u, v, weight))
total_weight += weight
# Stop when we have V-1 edges
if len(mst) == n - 1:
break
return mst, total_weight
# Example Usage
n = 5 # 5 vertices (0 to 4)
edges = [
(0, 1, 4),
(0, 2, 2),
(1, 2, 1),
(1, 3, 5),
(2, 3, 8),
(2, 4, 10),
(3, 4, 2)
]
mst, total_weight = kruskals_mst(n, edges)
print("MST Edges:", mst)
# Output: [(1, 2, 1), (0, 2, 2), (3, 4, 2), (1, 3, 5)]
print(f"Total MST Weight: {total_weight}")
# Output: Total MST Weight: 10Java
import java.util.*;
class Edge implements Comparable<Edge> {
int src, dest;
long weight;
public Edge(int src, int dest, long weight) {
this.src = src;
this.dest = dest;
this.weight = weight;
}
@Override
public int compareTo(Edge other) {
int byWeight = Long.compare(this.weight, other.weight);
if (byWeight != 0) return byWeight;
int bySource = Integer.compare(this.src, other.src);
return bySource != 0 ? bySource : Integer.compare(this.dest, other.dest);
}
}
class UnionFind {
int[] parent, rank;
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 0;
}
}
public int find(int i) {
if (parent[i] != i) {
parent[i] = find(parent[i]);
}
return parent[i];
}
public boolean union(int i, int j) {
int rootI = find(i);
int rootJ = find(j);
if (rootI != rootJ) {
if (rank[rootI] < rank[rootJ]) {
parent[rootI] = rootJ;
} else if (rank[rootI] > rank[rootJ]) {
parent[rootJ] = rootI;
} else {
parent[rootJ] = rootI;
rank[rootI]++;
}
return true;
}
return false;
}
}
public class Kruskals {
public static void kruskalsMST(int V, List<Edge> edges) {
Collections.sort(edges);
UnionFind uf = new UnionFind(V);
List<Edge> mst = new ArrayList<>();
long totalWeight = 0L;
for (Edge edge : edges) {
if (uf.union(edge.src, edge.dest)) {
mst.add(edge);
totalWeight = Math.addExact(totalWeight, edge.weight);
if (mst.size() == Math.max(0, V - 1)) break;
}
}
System.out.println("MST Edges:");
for (Edge edge : mst) {
System.out.println("(" + edge.src + ", " + edge.dest + ", " + edge.weight + ")");
}
System.out.println("Total MST Weight: " + totalWeight);
if (mst.size() != Math.max(0, V - 1)) {
System.out.println("No MST: returned a minimum spanning forest");
}
}
public static void main(String[] args) {
int V = 5;
List<Edge> edges = new ArrayList<>();
edges.add(new Edge(0, 1, 4));
edges.add(new Edge(0, 2, 2));
edges.add(new Edge(1, 2, 1));
edges.add(new Edge(1, 3, 5));
edges.add(new Edge(2, 3, 8));
edges.add(new Edge(2, 4, 10));
edges.add(new Edge(3, 4, 2));
kruskalsMST(V, edges);
// Output:
// MST Edges:
// (1, 2, 1)
// (0, 2, 2)
// (3, 4, 2)
// (1, 3, 5)
// Total MST Weight: 10
}
}🎯 Key Insight: Edge-Centric Greedy Approach
Unlike Prim's (which is vertex-centric), Kruskal's is edge-centric. It considers all edges globally and picks the smallest ones first, regardless of which vertices they connect. This makes it perfect for sparse graphs where sorting edges is cheaper than maintaining a priority queue of vertices!
⚡ Complexity Analysis
Time Complexity
- •O(E log E) overall
Dominated by sorting edges
- •O(E log V) upper-bound form
Because log E = O(log V) for a simple graph
- •O(E α(V)) for Union-Find
α is inverse Ackermann (nearly constant)
Space Complexity
- •O(V + E)
For storing edges and Union-Find structure
🔄 Kruskal's vs Prim's
| Aspect | Kruskal's | Prim's |
|---|---|---|
| Approach | Edge-centric (global) | Vertex-centric (local) |
| Data Structure | Union-Find | Priority Queue |
| Best For | Sparse graphs (few edges) | Dense graphs (many edges) |
| Time Complexity | O(E log E) | O(E log V) |
| Graph Representation | Edge list | Adjacency list |
✅ Pros & ❌ Cons
✅ Advantages
- ✓Better for sparse graphs (fewer edges)
- ✓Works on disconnected graphs (finds MST forest)
- ✓Sorting can use parallel or external-memory machinery
- ✓Works well with edge list representation
❌ Limitations
- ✗Requires sorting all edges (expensive for dense graphs)
- ✗Needs Union-Find implementation
- ✗Slower than Prim's for very dense graphs
- ✗Not suitable for adjacency matrix representation
💡 Pro Tip: When to Choose Kruskal's
Choose Kruskal's when you have a sparse graph (E is much less than V²) or when edges are already sorted/partially sorted. It's also great for finding MST in disconnected graphs, where it naturally produces a minimum spanning forest!
The forest invariant and correctness proof
Invariant
Chosen edges form an extendable forest
They are acyclic, and some minimum spanning forest contains every edge accepted so far.
Accept
Different DSU representatives
One current component defines a cut. The globally lightest eligible edge crossing it is safe by the cut property.
Reject
Same DSU representative
A path already connects the endpoints; adding this edge closes a cycle and cannot be required for connectivity.
e is absent from an optimal forest T that contains prior choices. Adding e to T creates a cycle containing an edge f that crosses the same component cut. Because edges are processed nondecreasingly, w(e) ≤ w(f); replacing f with e preserves optimality and the invariant.DSU mechanics: what makes cycle checks nearly constant
| Operation | Invariant | Optimization | Common bug |
|---|---|---|---|
find(x) | Returns the representative of x's component | Path compression/halving rewrites ancestors toward the root | Comparing immediate parents instead of representatives |
union(a,b) | Merges exactly two different components | Union-by-size attaches the smaller root below the larger root | Updating size on a non-root or decrementing count on a failed union |
| component count | Starts at V; each successful union subtracts one | Gives forest metadata without another traversal | Assuming it reaches one on disconnected input |
With both optimizations, any sequence of m operations on V elements costs O(m α(V)) amortized. That is not literally O(1), but the inverse Ackermann function is below five at practical scales. Sorting, not DSU, dominates Kruskal.
Worked trace: accept, reject, and merge
| Edge | Representatives before | Decision | Components after | Total |
|---|---|---|---|---|
| 1–2 (1) | different | accept | {0} {1,2} {3} {4} | 1 |
| 0–2 (2) | different | accept | {0,1,2} {3} {4} | 3 |
| 3–4 (2) | different | accept | {0,1,2} {3,4} | 5 |
| 0–1 (4) | same | reject cycle | unchanged | 5 |
| 1–3 (5) | different | accept; complete | {0,1,2,3,4} | 10 |
The rejected 0–1 edge exposes a frequent trace mistake: choosing the next cheapest weight is not enough. The endpoints must still belong to different components.
Production-safe Kruskal forest in Java
This version sorts a copy instead of mutating caller input, validates endpoints, uses path halving plus union-by-size, accumulates into long, and returns forest metadata rather than silently claiming success on a disconnected graph.
import java.util.*;
public final class KruskalMinimumSpanningForest {
public record Edge(int u, int v, long weight) {}
public record Result(
List<Edge> edges, long totalWeight,
int components, boolean connected) {}
public static Result build(int vertexCount, List<Edge> input) {
if (vertexCount < 0) {
throw new IllegalArgumentException("negative vertex count");
}
List<Edge> sorted = new ArrayList<>(input);
for (Edge edge : sorted) {
if (edge.u() < 0 || edge.u() >= vertexCount
|| edge.v() < 0 || edge.v() >= vertexCount) {
throw new IllegalArgumentException("vertex outside [0, V)");
}
}
sorted.sort(Comparator.comparingLong(Edge::weight)
.thenComparingInt(edge -> Math.min(edge.u(), edge.v()))
.thenComparingInt(edge -> Math.max(edge.u(), edge.v())));
Dsu dsu = new Dsu(vertexCount);
List<Edge> forest = new ArrayList<>();
long total = 0L;
// Do not stop at V-1 unless connectivity is guaranteed by contract.
for (Edge edge : sorted) {
if (!dsu.union(edge.u(), edge.v())) continue;
forest.add(edge);
total = Math.addExact(total, edge.weight());
}
if (forest.size() != vertexCount - dsu.components()) {
throw new AssertionError("forest edge-count invariant failed");
}
boolean connected = vertexCount == 0 || dsu.components() == 1;
return new Result(
List.copyOf(forest), total, dsu.components(), connected);
}
private static final class Dsu {
private final int[] parent;
private final int[] size;
private int components;
Dsu(int n) {
parent = new int[n];
size = new int[n];
components = n;
Arrays.setAll(parent, i -> i);
Arrays.fill(size, 1);
}
int find(int x) {
while (x != parent[x]) {
parent[x] = parent[parent[x]]; // path halving
x = parent[x];
}
return x;
}
boolean union(int a, int b) {
int rootA = find(a), rootB = find(b);
if (rootA == rootB) return false;
if (size[rootA] < size[rootB]) {
int temporary = rootA; rootA = rootB; rootB = temporary;
}
parent[rootB] = rootA;
size[rootA] += size[rootB];
components--;
return true;
}
int components() { return components; }
}
}Ties, uniqueness, and edge classification
Critical edge
Appears in every MST. A unique lightest edge across some cut is critical.
Pseudo-critical edge
Appears in at least one MST but not all; equal-weight alternatives commonly create this status.
Excluded edge
Appears in no MST, for example a uniquely heaviest edge on a cycle.
Distinct weights guarantee uniqueness, but repeated weights do not guarantee non-uniqueness. A deeper test processes one weight batch at a time: contract components formed by lighter edges, then inspect the equal-weight candidate edges between those components. Alternative acyclic ways to connect the same contracted region imply multiple optimal choices. Deterministic endpoint tie-breakers choose one reproducibly; they do not make it uniquely optimal.
FAANG-style variants and engineering choices
Recognize the transformation
- • “Connect every site for minimum total build cost” is MST; “minimum route from one site” is shortest path.
- • To form exactly
kclusters, stop when DSU haskcomponents, or remove thek−1largest MST edges. - • A virtual node can model per-vertex options such as building a well versus laying a pipe.
- • Sorting edges also powers offline connectivity-by-threshold questions, though not every DSU problem is an MST problem.
Choose around the data
- • Kruskal stores/sorts Θ(E) edges: excellent for sparse edge lists, expensive for dense complete graphs.
- • Matrix Prim's O(V²) often beats materializing and sorting Θ(V²) dense edges.
- • If edges live on disk, external merge sort can feed Kruskal in order; DSU remains in memory when V permits.
- • Parallel sorting is straightforward; concurrent unions require careful synchronization and are not “independent.”
long and avoid subtraction comparators.Complexity details & tie-breaking
Sorting dominates: O(E log E) comparisons. DSU with union-by-rank and path compression makes the union/find phase O(E α(V)), where α is the inverse Ackermann function, effectively constant for any realistic graph size.
Equal-weight edges can admit multiple valid MSTs; Kruskal's output depends on the comparator's tie-breaking. A stable sort alone is deterministic only when input order is deterministic. Different tied choices are valid only when they preserve connectivity and acyclicity, not merely because their weights match.
Short takeaway
Kruskal sorts edges and keeps those that merge Union-Find components. Sorting dominates time; DSU enforces acyclicity and yields an MST or minimum spanning forest.
Exercises
- Show a sorted edge list where taking the first V−1 edges without DSU fails. Which edge must be rejected?
- Prove in one sentence why an accepted Kruskal edge is a lightest edge across the cut of the two components it merges.
- E=2×10⁶, V=5×10⁵. Is Kruskal or heap Prim the better fit if the input is already an edge array?
Primary sources
- Kruskal, J. B. (1956): On the shortest spanning subtree of a graph.
- Cormen et al., CLRS: Kruskal's algorithm and the cycle property.
- Tarjan: Union-Find with path compression and union by rank.