Skip to content
CCC Python Course

Minimum spanning tree and Kruskal with DSU

Module
M6.3
Lesson
1 of 1
Reading time
5 min

In this lesson

  • Recognize when a minimum spanning tree solves a connectivity or infrastructure problem.
  • Implement Kruskal's algorithm sorting edges by weight.
  • Use a disjoint-set union (DSU) data structure to check connectivity and union components.
  • Optimize DSU with path compression and union by rank.

Many problems ask: connect all the items with minimum total cost. Build a network where every site can reach every other site, but use the fewest or cheapest links. This is the minimum spanning tree problem.

A tree is a connected graph with no cycles. A spanning tree of a graph touches every node exactly once through the edges. A minimum spanning tree has the smallest total edge weight among all spanning trees.

Kruskal's algorithm builds an MST by greedily adding the cheapest edge that does not create a cycle. It works because of a property called the cut property: if you partition the nodes into two groups, the cheapest edge crossing the partition is safe to include in any MST.

The challenge is checking whether adding an edge creates a cycle. You need to know which nodes are already connected through the edges you have chosen so far. A disjoint-set union (DSU), also called union-find, maintains this information efficiently.

Disjoint-set union

A DSU stores a collection of disjoint sets. Each element belongs to exactly one set. Two operations are central: find(x) returns the representative of the set containing x, and union(x, y) merges the sets containing x and y.

Two nodes are in the same connected component if find(x) == find(y). If they are not, you can add an edge between them without creating a cycle.

The simplest implementation uses a parent pointer. Each node points to its parent. The root of the tree points to itself. find(x) walks up the parent pointers until it reaches a root.

Python
def find(parent, x):    if parent[x] != x:        parent[x] = find(parent, parent[x])    return parent[x]

def union(parent, x, y):    root_x = find(parent, x)    root_y = find(parent, y)    if root_x != root_y:        parent[root_y] = root_x

find(x) uses path compression: after finding the root, it makes every node on the path point directly to the root. This keeps future find calls fast.

union(x, y) attaches one root to the other. To avoid creating very deep trees, union by rank attaches the root of the smaller tree to the larger one. With both optimizations, find and union take nearly constant time.

Kruskal's algorithm

examples/kruskal_mst.py
import sys

def find(parent, x):    if parent[x] != x:        parent[x] = find(parent, parent[x])    return parent[x]

def union(parent, rank, x, y):    root_x = find(parent, x)    root_y = find(parent, y)
    if root_x == root_y:        return False
    if rank[root_x] < rank[root_y]:        parent[root_x] = root_y    elif rank[root_x] > rank[root_y]:        parent[root_y] = root_x    else:        parent[root_y] = root_x        rank[root_x] += 1
    return True

def main() -> None:    input_data = sys.stdin.read().split()    if not input_data:        return
    idx = 0    n = int(input_data[idx])    m = int(input_data[idx + 1])    idx += 2
    edges = []    for _ in range(m):        u = int(input_data[idx])        v = int(input_data[idx + 1])        w = int(input_data[idx + 2])        idx += 3        edges.append((w, u, v))
    edges.sort()
    parent = list(range(n))    rank = [0] * n    total_weight = 0    edges_used = 0
    for w, u, v in edges:        if union(parent, rank, u, v):            total_weight += w            edges_used += 1            if edges_used == n - 1:                break
    if edges_used == n - 1:        sys.stdout.write(str(total_weight) + "\n")    else:        sys.stdout.write("-1\n")

if __name__ == "__main__":    main()

Input

4 5
0 1 2
1 2 3
0 2 4
2 3 1
1 3 5

Output

6
Kruskal's algorithm with DSU

The algorithm sorts edges by weight and processes them in order. For each edge, check if its endpoints are in different components using find. If they are, add the edge and merge the components. If they are already in the same component, skip the edge because adding it would create a cycle.

The result is a tree connecting all nodes with minimum total weight.

Why Kruskal's algorithm works

The cut property guarantees that Kruskal's greedy choice is safe. Partition the nodes into two groups: those already in the MST so far, and those not yet added. The cheapest edge crossing the partition between these two groups can appear in some MST. If it is not in an MST, you could swap it with the edge of equal or higher cost that the MST uses to cross the partition, creating an MST of equal or lower cost. So greedily adding the cheapest edge never closes off a path to the optimal solution.

The algorithm processes n - 1 edges: enough to connect n nodes into a tree. It rejects any edge that would create a cycle because its endpoints are already connected through earlier choices. This guarantees the result is acyclic and spanning.

The time complexity is O(m log m) for sorting edges, plus O(m⋅α(n))O(m \cdot \alpha(n)) for DSU operations, where α\alpha is the inverse Ackermann function (effectively constant). The total time is dominated by sorting.

A second example: highway network

A country has 6 cities and wants to connect them with highways. The cost is the total length of highways built. The practical distances are: cities 0-1 cost 5, 0-2 cost 8, 0-3 cost 6, 1-2 cost 3, 1-4 cost 7, 2-3 cost 4, 3-4 cost 2, and 3-5 cost 9.

Sort the edges by cost: (3-4, 2), (1-2, 3), (2-3, 4), (0-1, 5), (0-3, 6), (1-4, 7), (0-2, 8), (3-5, 9).

Process each edge in order. Initially, each city is its own component. Edge 3-4 costs 2: add it, merging 3 and 4. Edge 1-2 costs 3: add it, merging 1 and 2. Edge 2-3 costs 4: add it, merging 2 and 4 into 4. Edge 0-1 costs 5: add it, merging 0 and 4. Edge 0-3 costs 6: both endpoints are already in 4, so skip it. Edge 1-4 costs 7: skip (both in same component). Edge 0-2 costs 8: skip. Edge 3-5 costs 9: add it, merging 4 and 5. Total cost: 2 + 3 + 4 + 5 + 9 = 23. You have added 5 edges connecting 6 cities, so all are reachable.

Common mistakes

One mistake is to forget to update the parent correctly in union. If you set parent[root_x] = root_y, then later calls to find(x) will fail because the root has changed. You must always retrieve both roots first, check if they differ, and only then update the parent. Otherwise, find descends through an invalid path and returns an incorrect root.

Another mistake is to skip path compression in find. Without it, find becomes slow on deep trees. Consider a chain of 1,000 nodes where node i points to node i+1, and the last node points to itself. The first find call walks the entire chain, taking 1,000 steps. With path compression, that same call makes every node point directly to the root, so future calls take one step each. Even with union by rank, skipping compression allows deep trees to form in the worst case and degrades union-find toward O(n) per operation instead of nearly constant.

A third mistake is to check connectivity incorrectly. The condition if find(x) != find(y) means x and y are not yet in the same component, so it is safe to add an edge. If you check if x == y, you compare node ids instead of component representatives. This misses many edges that should be added because their endpoints are in different components.

Recap

A minimum spanning tree connects all nodes with minimum total edge weight and contains no cycles. Kruskal's algorithm builds an MST by sorting edges and greedily adding the cheapest edge that does not create a cycle. A disjoint-set union detects whether two nodes are already connected. With path compression and union by rank, DSU operations take nearly constant time per call, making Kruskal's algorithm efficient on large graphs.

Practice

Try these on the judges. Each link opens the problem on WMOJ or DMOJ.

  1. 2017 S4
    Minimum Cost Flow (opens on DMOJ in a new tab) DMOJ

    Build a minimum spanning tree while preferring to keep certain edges when costs tie.

    Why DMOJ: An older MST problem, with a DSU-based tie-breaking rule, that still makes good practice for this module.

  2. 2023 S4
    Minimum Cost Roads (opens on WMOJ in a new tab) WMOJ

    A subtask of this problem reduces to a plain minimum spanning tree.