Kruskal's Algorithm
Description
Given a weighted, connected, undirected graph with V vertices and E edges, find the Minimum Spanning Tree (MST).
A spanning tree of a graph is a subgraph that includes all V vertices and exactly V - 1 edges, forming a tree (connected with no cycles). A minimum spanning tree is the spanning tree whose total edge weight (sum of all edge weights) is the smallest possible among all spanning trees of the graph.
Kruskal's Algorithm is a classic greedy algorithm that builds the MST by processing edges in order of increasing weight, adding each edge to the MST as long as it does not create a cycle.
Return the set of edges in the MST and its total weight.

Examples
Example 1
Input:
V = 4, E = 5
Edges: (0,1,10), (0,2,6), (0,3,5), (1,3,15), (2,3,4)
Format: (u, v, weight)
Output:
MST edges: (2,3,4), (0,3,5), (0,1,10)
Total weight: 19
Explanation:
We need exactly V - 1 = 3 edges to connect all 4 vertices. The MST is formed by choosing the three edges with the smallest weights that do not create a cycle:
- Edge (2,3) with weight 4 — connects vertices 2 and 3.
- Edge (0,3) with weight 5 — connects vertex 0 to the component {2,3}.
- Edge (0,1) with weight 10 — connects vertex 1 to the component {0,2,3}.
Edge (0,2) with weight 6 is skipped because adding it would create a cycle (0-2-3-0 are already connected). Edge (1,3) with weight 15 is also skipped for the same reason.
Total weight = 4 + 5 + 10 = 19.
Example 2
Input:
V = 3, E = 3
Edges: (0,1,1), (1,2,2), (0,2,3)
Output:
MST edges: (0,1,1), (1,2,2)
Total weight: 3
Explanation:
With 3 vertices, we need exactly 2 edges. Sort by weight: (0,1,1), (1,2,2), (0,2,3).
- Take (0,1,1) — no cycle.
- Take (1,2,2) — no cycle. All 3 vertices connected.
- Skip (0,2,3) — would form cycle 0-1-2-0.
Total weight = 1 + 2 = 3.
Example 3
Input:
V = 2, E = 1
Edges: (0,1,7)
Output:
MST edges: (0,1,7)
Total weight: 7
Explanation:
With only 2 vertices and 1 edge, the MST is simply that single edge. This is the smallest possible connected graph.
Constraints
- 2 ≤ V ≤ 10^5 (number of vertices)
- V - 1 ≤ E ≤ V × (V - 1) / 2 (number of edges; at least V-1 to ensure connectivity)
- 1 ≤ weight ≤ 10^6 (edge weights are positive integers)
- The graph is connected (an MST always exists)
- The graph is undirected (edge (u,v) is the same as edge (v,u))
- No self-loops or parallel edges
Editorial
Brute Force
Intuition
The most basic approach to finding a Minimum Spanning Tree is to examine all possible spanning trees and select the one with the minimum total weight.
A spanning tree of a graph with V vertices contains exactly V - 1 edges chosen from the available E edges. So we can enumerate all possible combinations of V - 1 edges from E edges, check if each combination forms a valid spanning tree (i.e., it connects all vertices and contains no cycles), and track the one with the minimum total weight.
Think of it like a road planner who has a map of all possible roads between cities. To connect all cities with the cheapest road network (no redundant connections), the planner could list every possible set of V - 1 roads, verify each set actually connects all cities, and then pick the cheapest valid set.
This is conceptually simple but computationally catastrophic for any non-trivial graph.
Step-by-Step Explanation
Let's trace with V = 4, E = 5, Edges: (0,1,10), (0,2,6), (0,3,5), (1,3,15), (2,3,4).
We need to choose 3 edges (V - 1 = 3) from 5 edges. There are C(5,3) = 10 possible combinations.
Step 1: Enumerate all C(5,3) = 10 subsets of 3 edges.
Step 2: Check each subset:
- {(0,1,10), (0,2,6), (0,3,5)} — Connects all? Yes (0 connects to 1,2,3). Cycle? No. Weight = 21. ✓ Valid spanning tree.
- {(0,1,10), (0,2,6), (1,3,15)} — Connects all? Yes. Cycle? No. Weight = 31. ✓ Valid.
- {(0,1,10), (0,2,6), (2,3,4)} — Connects all? Yes. Cycle? No. Weight = 20. ✓ Valid.
- {(0,1,10), (0,3,5), (1,3,15)} — Connects all? 0-1, 0-3, 1-3. Vertex 2 missing! ✗ Not spanning.
- {(0,1,10), (0,3,5), (2,3,4)} — Connects all? Yes. Cycle? No. Weight = 19. ✓ Valid.
- {(0,1,10), (1,3,15), (2,3,4)} — Connects all? Yes. Cycle? No. Weight = 29. ✓ Valid.
- {(0,2,6), (0,3,5), (1,3,15)} — Connects all? Yes. Cycle? No. Weight = 26. ✓ Valid.
- {(0,2,6), (0,3,5), (2,3,4)} — Connects all? 0-2, 0-3, 2-3. Forms cycle 0-2-3-0! ✗ Has cycle (and vertex 1 missing).
- {(0,2,6), (1,3,15), (2,3,4)} — Connects all? Yes. Cycle? No. Weight = 25. ✓ Valid.
- {(0,3,5), (1,3,15), (2,3,4)} — Connects all? Yes. Cycle? No. Weight = 24. ✓ Valid.
Step 3: Among valid spanning trees, minimum weight is 19 from {(0,1,10), (0,3,5), (2,3,4)}.
Step 4: Return MST with total weight 19.
Algorithm
- Generate all C(E, V-1) combinations of V-1 edges from E total edges.
- For each combination:
a. Check if the V-1 edges connect all V vertices (form a connected subgraph).
b. Check if there are no cycles (a connected graph with V-1 edges on V vertices is automatically cycle-free, so checking connectivity suffices).
c. If valid, compute total weight. - Return the combination with the minimum total weight.
Code
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
struct Edge {
int u, v, weight;
};
// Check if selected edges form a spanning tree
bool isSpanningTree(vector<Edge>& selected, int V) {
// BFS/DFS to check connectivity
vector<vector<int>> adj(V);
for (auto& e : selected) {
adj[e.u].push_back(e.v);
adj[e.v].push_back(e.u);
}
vector<bool> visited(V, false);
vector<int> stack = {0};
visited[0] = true;
int count = 1;
while (!stack.empty()) {
int node = stack.back();
stack.pop_back();
for (int neighbor : adj[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
count++;
stack.push_back(neighbor);
}
}
}
return count == V;
}
pair<vector<Edge>, int> bruteForceMST(
vector<Edge>& edges, int V) {
int E = edges.size();
int target = V - 1;
vector<Edge> bestMST;
int bestWeight = INT_MAX;
// Generate all combinations of (V-1) edges
vector<int> indices(target);
for (int i = 0; i < target; i++)
indices[i] = i;
while (true) {
vector<Edge> selected;
int totalWeight = 0;
for (int idx : indices) {
selected.push_back(edges[idx]);
totalWeight += edges[idx].weight;
}
if (isSpanningTree(selected, V)
&& totalWeight < bestWeight) {
bestWeight = totalWeight;
bestMST = selected;
}
// Next combination
int i = target - 1;
while (i >= 0 && indices[i] == E - target + i)
i--;
if (i < 0) break;
indices[i]++;
for (int j = i + 1; j < target; j++)
indices[j] = indices[j-1] + 1;
}
return {bestMST, bestWeight};
}from itertools import combinations
from collections import deque
def is_spanning_tree(selected_edges, V):
"""Check if selected edges connect all V vertices."""
adj = [[] for _ in range(V)]
for u, v, w in selected_edges:
adj[u].append(v)
adj[v].append(u)
visited = [False] * V
visited[0] = True
queue = deque([0])
count = 1
while queue:
node = queue.popleft()
for neighbor in adj[node]:
if not visited[neighbor]:
visited[neighbor] = True
count += 1
queue.append(neighbor)
return count == V
def brute_force_mst(edges, V):
best_weight = float('inf')
best_mst = []
# Try all combinations of V-1 edges
for combo in combinations(edges, V - 1):
if is_spanning_tree(combo, V):
total = sum(w for _, _, w in combo)
if total < best_weight:
best_weight = total
best_mst = list(combo)
return best_mst, best_weightimport java.util.*;
class BruteForceMST {
static boolean isSpanningTree(
int[][] selected, int V) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < V; i++)
adj.add(new ArrayList<>());
for (int[] e : selected) {
adj.get(e[0]).add(e[1]);
adj.get(e[1]).add(e[0]);
}
boolean[] visited = new boolean[V];
visited[0] = true;
Queue<Integer> queue = new LinkedList<>();
queue.add(0);
int count = 1;
while (!queue.isEmpty()) {
int node = queue.poll();
for (int nb : adj.get(node)) {
if (!visited[nb]) {
visited[nb] = true;
count++;
queue.add(nb);
}
}
}
return count == V;
}
static int bruteForceMST(
int[][] edges, int V) {
int E = edges.length;
int target = V - 1;
int bestWeight = Integer.MAX_VALUE;
// Generate all combinations using
// recursive backtracking
int[] indices = new int[target];
for (int i = 0; i < target; i++)
indices[i] = i;
while (true) {
int[][] selected = new int[target][];
int total = 0;
for (int i = 0; i < target; i++) {
selected[i] = edges[indices[i]];
total += edges[indices[i]][2];
}
if (isSpanningTree(selected, V)
&& total < bestWeight) {
bestWeight = total;
}
int i = target - 1;
while (i >= 0
&& indices[i] == E - target + i)
i--;
if (i < 0) break;
indices[i]++;
for (int j = i + 1; j < target; j++)
indices[j] = indices[j-1] + 1;
}
return bestWeight;
}
}Complexity Analysis
Time Complexity: O(C(E, V-1) × V)
We generate all possible combinations of V-1 edges from E total edges. The number of combinations C(E, V-1) can be astronomically large — it grows exponentially. For each combination, we check connectivity using BFS/DFS which takes O(V + V-1) = O(V) time.
For a dense graph where E ≈ V², C(V², V) is enormous. Even for modest inputs like V = 20 and E = 50, C(50, 19) ≈ 10^13 — completely infeasible.
Space Complexity: O(V + E)
We store the adjacency list for connectivity checking (O(V + E)) and the current combination of edges (O(V)).
Why This Approach Is Not Efficient
The brute force approach has exponential time complexity. The number of edge combinations C(E, V-1) grows explosively with graph size. For a graph with just 20 vertices and 50 edges, there are approximately 10^13 possible spanning tree candidates to check — this would take years on modern hardware.
The fundamental flaw is that we treat each subset of edges independently, without leveraging any structure. We re-check connectivity from scratch for every combination, and we have no way to prune obviously bad candidates early.
Key Insight: Instead of trying all possible trees, what if we built the MST one edge at a time? Kruskal's key observation is that if we process edges in order of increasing weight and greedily include each edge that does not create a cycle, the result is guaranteed to be an MST. This is called the greedy choice property of the MST problem.
The remaining question is: how do we efficiently detect whether adding an edge creates a cycle?
Better Approach - Kruskal's with DFS Cycle Detection
Intuition
Kruskal's algorithm works by a greedy strategy: sort all edges by weight, then iterate through them from lightest to heaviest. For each edge, check if adding it to the current MST would create a cycle. If not, include it. Stop when we have V - 1 edges.
Why does this greedy approach work? Consider the lightest edge in the entire graph — it must be part of at least one MST (this is called the cut property). By always choosing the lightest available edge that does not form a cycle, we are guaranteed to build a minimum spanning tree.
In this version, we detect cycles using DFS (Depth-First Search). Before adding edge (u, v), we run DFS from u in the partially built MST to see if v is already reachable. If v is reachable, adding (u, v) would create a cycle, so we skip it. If v is not reachable, we safely add the edge.
This is correct but not optimal — each DFS traversal can take O(V) time, and we might need to do it for every edge.
Step-by-Step Explanation
Let's trace with V = 4, E = 5, Edges: (0,1,10), (0,2,6), (0,3,5), (1,3,15), (2,3,4).
Step 1: Sort edges by weight: (2,3,4), (0,3,5), (0,2,6), (0,1,10), (1,3,15).
Step 2: MST = {}, edges_added = 0. Process edge (2,3,4).
- DFS from 2: can we reach 3 in the current MST? MST is empty, so no.
- No cycle → add edge. MST = {(2,3,4)}, edges_added = 1.
Step 3: Process edge (0,3,5).
- DFS from 0: can we reach 3? MST has only (2,3), and 0 is isolated. No.
- No cycle → add edge. MST = {(2,3,4), (0,3,5)}, edges_added = 2.
Step 4: Process edge (0,2,6).
- DFS from 0: can we reach 2? Follow 0→3 (via edge (0,3,5)), then 3→2 (via edge (2,3,4)). YES! 2 is reachable.
- Cycle detected → skip this edge.
Step 5: Process edge (0,1,10).
- DFS from 0: can we reach 1? Follow 0→3→2, but 1 is not in {0, 2, 3}. No.
- No cycle → add edge. MST = {(2,3,4), (0,3,5), (0,1,10)}, edges_added = 3.
Step 6: edges_added = 3 = V - 1. MST is complete! Total weight = 4 + 5 + 10 = 19.
Kruskal's with DFS Cycle Detection — Watch Kruskal's algorithm process edges from lightest to heaviest, using DFS to check for cycles before adding each edge to the MST.
Algorithm
- Sort all E edges by weight in non-decreasing order.
- Initialize an empty MST edge list and a counter edges_added = 0.
- For each edge (u, v, w) in sorted order:
a. Run DFS/BFS from u in the current MST to check if v is reachable.
b. If v is NOT reachable: add edge (u, v, w) to MST, increment edges_added.
c. If v IS reachable: skip this edge (it would create a cycle).
d. If edges_added == V - 1: stop (MST is complete). - Return MST edges and total weight.
Code
#include <vector>
#include <algorithm>
using namespace std;
struct Edge {
int u, v, weight;
};
bool canReach(int src, int dest, int V,
vector<vector<pair<int,int>>>& adj) {
vector<bool> visited(V, false);
vector<int> stack = {src};
visited[src] = true;
while (!stack.empty()) {
int node = stack.back();
stack.pop_back();
if (node == dest) return true;
for (auto [nb, w] : adj[node]) {
if (!visited[nb]) {
visited[nb] = true;
stack.push_back(nb);
}
}
}
return false;
}
pair<vector<Edge>, int> kruskalWithDFS(
vector<Edge>& edges, int V) {
sort(edges.begin(), edges.end(),
[](const Edge& a, const Edge& b) {
return a.weight < b.weight;
});
vector<vector<pair<int,int>>> adj(V);
vector<Edge> mst;
int totalWeight = 0;
for (auto& e : edges) {
if (mst.size() == V - 1) break;
if (!canReach(e.u, e.v, V, adj)) {
mst.push_back(e);
totalWeight += e.weight;
adj[e.u].push_back({e.v, e.weight});
adj[e.v].push_back({e.u, e.weight});
}
}
return {mst, totalWeight};
}from collections import defaultdict
def can_reach(src, dest, adj):
"""DFS to check if dest is reachable from src."""
visited = set()
stack = [src]
visited.add(src)
while stack:
node = stack.pop()
if node == dest:
return True
for neighbor, _ in adj[node]:
if neighbor not in visited:
visited.add(neighbor)
stack.append(neighbor)
return False
def kruskal_with_dfs(edges, V):
# Sort edges by weight
edges.sort(key=lambda e: e[2])
adj = defaultdict(list)
mst = []
total_weight = 0
for u, v, w in edges:
if len(mst) == V - 1:
break
# DFS cycle detection
if not can_reach(u, v, adj):
mst.append((u, v, w))
total_weight += w
adj[u].append((v, w))
adj[v].append((u, w))
return mst, total_weightimport java.util.*;
class KruskalDFS {
static boolean canReach(int src, int dest,
List<List<int[]>> adj, int V) {
boolean[] visited = new boolean[V];
Deque<Integer> stack = new ArrayDeque<>();
stack.push(src);
visited[src] = true;
while (!stack.isEmpty()) {
int node = stack.pop();
if (node == dest) return true;
for (int[] nb : adj.get(node)) {
if (!visited[nb[0]]) {
visited[nb[0]] = true;
stack.push(nb[0]);
}
}
}
return false;
}
static int kruskalWithDFS(
int[][] edges, int V) {
Arrays.sort(edges,
(a, b) -> a[2] - b[2]);
List<List<int[]>> adj = new ArrayList<>();
for (int i = 0; i < V; i++)
adj.add(new ArrayList<>());
int totalWeight = 0;
int edgesAdded = 0;
for (int[] e : edges) {
if (edgesAdded == V - 1) break;
if (!canReach(e[0], e[1], adj, V)) {
totalWeight += e[2];
edgesAdded++;
adj.get(e[0]).add(
new int[]{e[1], e[2]});
adj.get(e[1]).add(
new int[]{e[0], e[2]});
}
}
return totalWeight;
}
}Complexity Analysis
Time Complexity: O(E log E + E × V)
Sorting E edges takes O(E log E). For each of the E edges, we potentially run a DFS on the partially built MST, which can traverse up to V-1 edges and V vertices. So cycle detection across all edges costs O(E × V).
Total: O(E log E + E × V). Since E can be up to V², this becomes O(V² log V + V³) for dense graphs.
Space Complexity: O(V + E)
We maintain an adjacency list for the partial MST (up to V-1 edges) and the DFS visited array of size V.
Why This Approach Is Not Efficient
Kruskal's with DFS cycle detection is O(E log E + E × V). For a dense graph with V = 10^5 vertices, E can be up to ~5 × 10^9, and E × V becomes ~5 × 10^14 — far too slow.
The bottleneck is the cycle detection step. Every time we consider an edge (u, v), we run a full DFS to check if u and v are already connected in the partial MST. This DFS may traverse the entire MST each time.
Key Insight: The cycle detection question — "are u and v already in the same connected component?" — is exactly the problem that the Disjoint Set (Union-Find) data structure solves in near-constant time. Instead of running DFS for each edge, we can use Union-Find:
- find(u) == find(v) → they are in the same component → adding this edge would create a cycle.
- find(u) ≠ find(v) → they are in different components → safe to add.
- After adding the edge, call union(u, v) to merge their components.
With Union by Rank and Path Compression, each find and union operation takes O(α(V)) ≈ O(1) amortized time, replacing the O(V) DFS with a near-constant-time operation.
Optimal Approach - Kruskal's with Union-Find
Intuition
Kruskal's algorithm with Union-Find is the classic, optimal implementation. The core idea remains the same: sort edges by weight and greedily add the lightest edge that does not form a cycle. The innovation is replacing the expensive DFS cycle check with an ultra-efficient Union-Find data structure.
Imagine you are a network engineer connecting offices across a city. You have a list of all possible cable routes with their costs. Your strategy:
- Sort all possible routes from cheapest to most expensive.
- Go through the list. For each route, ask: "Are these two offices already connected through the routes we have already laid?" This is a find query.
- If they are already connected, skip this route (it would be redundant — a cycle).
- If they are NOT connected, lay this cable (it provides a new connection). This is a union operation.
- Stop when all offices are connected (V - 1 cables laid).
The Union-Find structure makes Step 2 nearly instant. Instead of tracing through the existing network to check connectivity (which DFS does), Union-Find maintains a compact representation where checking connectivity is one near-constant-time lookup.
The total time is dominated by the initial sort: O(E log E). Everything after that is nearly linear.
Step-by-Step Explanation
Let's trace with V = 4, E = 5, Edges: (0,1,10), (0,2,6), (0,3,5), (1,3,15), (2,3,4).
Step 1: Sort edges by weight: (2,3,4), (0,3,5), (0,2,6), (0,1,10), (1,3,15).
Step 2: Initialize Union-Find: parent = [0,1,2,3], rank = [0,0,0,0].
Step 3: Process edge (2,3,4). find(2)=2, find(3)=3. Different roots → no cycle. Union(2,3). Add to MST.
- parent = [0,1,2,2], rank = [0,0,1,0]. MST weight = 4. Edges = 1.
Step 4: Process edge (0,3,5). find(0)=0, find(3)=2. Different roots → no cycle. Union(0,2). Add to MST.
- parent = [0,1,0,2], rank = [1,0,1,0] — Wait, rank[0]=0 and rank[2]=1, so 0 attaches under 2.
- Actually: find(0)=0 (rank 0), find(3)→parent[3]=2→parent[2]=2, root=2 (rank 1). rank[2] > rank[0], so parent[0]=2.
- parent = [2,1,2,2], rank = [0,0,1,0]. MST weight = 9. Edges = 2.
Step 5: Process edge (0,2,6). find(0)→parent[0]=2→root=2. find(2)=2. Same root (both 2)! CYCLE → skip.
Step 6: Process edge (0,1,10). find(0)→parent[0]=2→root=2. find(1)=1. Different roots → no cycle. Union(2,1). Add to MST.
- rank[2]=1 > rank[1]=0, so parent[1]=2.
- parent = [2,2,2,2], rank = [0,0,1,0]. MST weight = 19. Edges = 3.
Step 7: edges = 3 = V - 1. MST complete! Stop. Total weight = 19.
Kruskal's Algorithm with Union-Find — Building the MST — Watch how Kruskal's algorithm processes edges from lightest to heaviest, using Union-Find for near-instant cycle detection to build the minimum spanning tree.
Algorithm
- Sort all E edges by weight in non-decreasing order.
- Initialize a Union-Find (Disjoint Set) data structure with V elements.
- Initialize an empty MST edge list and a counter edges_added = 0.
- For each edge (u, v, w) in sorted order:
a. If find(u) ≠ find(v) → they are in different components:- Add (u, v, w) to the MST.
- Call union(u, v) to merge their components.
- Increment edges_added.
b. If find(u) == find(v) → same component → skip (would create cycle).
c. If edges_added == V - 1 → MST is complete → stop.
- Return the MST edges and total weight.
Code
#include <vector>
#include <algorithm>
using namespace std;
class UnionFind {
vector<int> parent, rank_;
public:
UnionFind(int n) {
parent.resize(n);
rank_.resize(n, 0);
for (int i = 0; i < n; i++)
parent[i] = i;
}
int find(int x) {
if (parent[x] != x)
parent[x] = find(parent[x]);
return parent[x];
}
bool unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return false;
if (rank_[rx] < rank_[ry]) swap(rx, ry);
parent[ry] = rx;
if (rank_[rx] == rank_[ry]) rank_[rx]++;
return true;
}
};
struct Edge {
int u, v, weight;
};
pair<vector<Edge>, int> kruskal(
vector<Edge>& edges, int V) {
// Step 1: Sort edges by weight
sort(edges.begin(), edges.end(),
[](const Edge& a, const Edge& b) {
return a.weight < b.weight;
});
// Step 2: Initialize Union-Find
UnionFind uf(V);
vector<Edge> mst;
int totalWeight = 0;
// Step 3: Process edges greedily
for (auto& e : edges) {
if (mst.size() == V - 1) break;
// If u and v are in different
// components, add edge
if (uf.unite(e.u, e.v)) {
mst.push_back(e);
totalWeight += e.weight;
}
}
return {mst, totalWeight};
}class UnionFind:
def __init__(self, n: int):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x: int) -> int:
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x: int, y: int) -> bool:
rx, ry = self.find(x), self.find(y)
if rx == ry:
return False
if self.rank[rx] < self.rank[ry]:
rx, ry = ry, rx
self.parent[ry] = rx
if self.rank[rx] == self.rank[ry]:
self.rank[rx] += 1
return True
def kruskal(edges, V):
# Step 1: Sort edges by weight
edges.sort(key=lambda e: e[2])
# Step 2: Initialize Union-Find
uf = UnionFind(V)
mst = []
total_weight = 0
# Step 3: Process edges greedily
for u, v, w in edges:
if len(mst) == V - 1:
break
# If u and v are in different
# components, add edge
if uf.union(u, v):
mst.append((u, v, w))
total_weight += w
return mst, total_weightimport java.util.*;
class UnionFind {
private 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;
}
public int find(int x) {
if (parent[x] != x)
parent[x] = find(parent[x]);
return parent[x];
}
public boolean union(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return false;
if (rank[rx] < rank[ry]) {
int tmp = rx; rx = ry; ry = tmp;
}
parent[ry] = rx;
if (rank[rx] == rank[ry]) rank[rx]++;
return true;
}
}
class Kruskal {
public static int kruskal(
int[][] edges, int V) {
// Step 1: Sort edges by weight
Arrays.sort(edges,
(a, b) -> a[2] - b[2]);
// Step 2: Initialize Union-Find
UnionFind uf = new UnionFind(V);
int totalWeight = 0;
int edgesAdded = 0;
// Step 3: Process edges greedily
for (int[] e : edges) {
if (edgesAdded == V - 1) break;
if (uf.union(e[0], e[1])) {
totalWeight += e[2];
edgesAdded++;
}
}
return totalWeight;
}
}Complexity Analysis
Time Complexity: O(E log E) or equivalently O(E log V)
The algorithm has two main phases:
- Sorting edges: O(E log E). This is the dominant cost.
- Processing edges with Union-Find: We iterate through at most E edges. Each edge requires one find and potentially one union operation, each costing O(α(V)) amortized time. Total: O(E × α(V)) ≈ O(E).
Combined: O(E log E + E × α(V)) = O(E log E).
Since E ≤ V × (V-1)/2, we have log E ≤ 2 log V, so O(E log E) = O(E log V). Both expressions are commonly used.
For V = 10^5 and E = 10^5, this is roughly 10^5 × 17 ≈ 1.7 × 10^6 operations — very fast.
Space Complexity: O(V + E)
We store the edge list (O(E)), the Union-Find parent and rank arrays (O(V)), and the MST edge list (O(V)). Total: O(V + E).