Skip to main content

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.

A weighted undirected graph with 5 vertices and its minimum spanning tree highlighted in green
A weighted undirected graph with 5 vertices and its minimum spanning tree highlighted in green

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

  1. Generate all C(E, V-1) combinations of V-1 edges from E total edges.
  2. 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.
  3. 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_weight
import 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

  1. Sort all E edges by weight in non-decreasing order.
  2. Initialize an empty MST edge list and a counter edges_added = 0.
  3. 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).
  4. 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_weight
import 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:

  1. Sort all possible routes from cheapest to most expensive.
  2. 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.
  3. If they are already connected, skip this route (it would be redundant — a cycle).
  4. If they are NOT connected, lay this cable (it provides a new connection). This is a union operation.
  5. 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

  1. Sort all E edges by weight in non-decreasing order.
  2. Initialize a Union-Find (Disjoint Set) data structure with V elements.
  3. Initialize an empty MST edge list and a counter edges_added = 0.
  4. 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.
  5. 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_weight
import 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:

  1. Sorting edges: O(E log E). This is the dominant cost.
  2. 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).