Disjoint set union

A disjoint set union (DSU) keeps elements split into groups (disjoint sets) that merge over time, and tells quickly whether two elements are in the same group. If the groups never change, one search finds them all, as in connected components.

It is for groups that only grow: roads between towns are built one at a time, and between the builds someone asks whether two towns are connected yet. A new depth-first search per question costs O(n + m); a DSU answers in practically constant time.

How it works

Each town has a parent, one town of its own group, drawn as an arrow. One town in each group is its own parent: the group's root. Following the arrows from any town ends at its root. The arrows are not roads; they only record the group.

Eight towns drawn as circles, each with an arrow to its parent: towns 2 and 3 point at town 1, town 4 points at town 3, and town 6 points at town 5; towns 1, 5, 7 and 8 are roots, drawn green with no arrow, and the path from town 4 through town 3 to town 1 is highlighted in blue.

Fig. 1. Each town points at a town of its group. Following the arrows from town 4 leads to town 1, the root.

Find and union

The first operation, find, follows the arrows from and returns its root. The second, union, finds the roots of and and, if they differ, points one at the other. Two towns are in the same group when find gives them the same root.

This fits only a relation that passes along: if is with and with , then is with . Friendship doesn't. A DSU also can't split a group when a road closes.

Union by size

If union always points the root of at the root of , the unions of town 1 with 2, 3, …, 8 build a chain, and with towns the finds walk about n^2 / 2 arrows.

The first rule is union by size. Keep the size of each group at its root, in sz, and point the root of the smaller group at the root of the larger one.

Two drawings of the same eight towns after the unions of town 1 with towns 2 to 8: on top, done in a fixed order, red arrows form one chain 1 to 2 to 3 and so on to 8, with town 8 as the green root; below, done by size, every town 2 to 8 has a blue arrow straight to town 1, the green root.

Fig. 2. The same seven unions, in a fixed order (top) and by size (bottom).

A town moves one arrow further from its root only when its group joins a group at least as large, so its group at least doubles. That happens at most \log_2 n times, and find is O(\log n).

Path compression

The second rule is path compression. When find has reached the root, it points every town it passed straight at the root. A recursive find does this in one line, writing the root into each parent on the way back.

Two drawings of the eight towns. On top, before find(1): a chain of arrows 1 to 2 to 3 and so on to 8, with town 8 as the green root. Below, after find(1): towns 1 to 6 each have a red arrow straight up to town 8, their old arrows along the row are faded grey, and town 7 keeps its arrow to 8.

Fig. 3. The chain before (top) and after (bottom) find(1).

A full run

Let's run a few unions on 8 towns, with gets in between, where get asks whether and are in one group.

A disjoint set union on 8 towns through the 9 operations, step 1.

Every town is its own root, in a group of size 1.

1/10

Fig. 4. Step through the operations with the buttons. Roots are green with their group size beside them. A blue arrow is new: one root now points at another. A red arrow was moved by path compression, and its old arrow is grey.

After the nine operations, towns 1 to 7 are one group with root 1, and town 8 is alone. The three gets answer NO, YES and NO. Path compression moved two arrows on the way: town 4's in get 4 2 and town 7's in get 7 8.

With both rules, operations on towns take O(m \cdot \alpha(n)) time, where is the inverse Ackermann function, at most 4 for any in a contest. The memory is two arrays of n + 1 numbers.

In C++

The standard library has no DSU. Two vectors hold the parent and the group size of each town.

The program reads n and m (1 \le n, m \le 5 \cdot 10^5), then m lines union u v or get u v. For each get it prints YES if u and v are in one group and NO otherwise. The run above has this input:

8 9
union 1 2
union 3 4
union 2 4
union 5 6
get 2 6
union 7 6
get 4 2
union 6 4
get 7 8
#include <iostream>
#include <string>
#include <vector>
using namespace std;

vector<int> parent, sz;

// find: the root of v's group
int find(int v) {
    if (parent[v] == v) return v;
    return parent[v] = find(parent[v]);   // path compression
}

// union: merge the groups of a and b; false if they were one group already
bool unite(int a, int b) {
    a = find(a);
    b = find(b);
    if (a == b) return false;
    if (sz[a] < sz[b]) swap(a, b);        // union by size: a is the larger root
    parent[b] = a;
    sz[a] += sz[b];
    return true;
}

int main() {
    int n, m;
    cin >> n >> m;
    parent.resize(n + 1);
    sz.assign(n + 1, 1);
    for (int v = 1; v <= n; v++) parent[v] = v;   // every town is its own group

    for (int i = 0; i < m; i++) {
        string op;
        int u, v;
        cin >> op >> u >> v;
        if (op == "union") {
            unite(u, v);
        } else {
            cout << (find(u) == find(v) ? "YES" : "NO") << '\n';
        }
    }
}

The size of the group of v is sz[find(v)]: only roots keep correct sizes. The number of groups is n minus the number of calls to unite that returned true.

Examples of use

Components while roads arrive

Some problems add roads one at a time and ask after each how many connected components there are, or how large the largest is. Count the groups as above and keep a running maximum of sz[find(u)]. A union that returns false means the new road closes a cycle.

Kruskal's algorithm

To connect all towns at the smallest total cost, Kruskal's algorithm sorts the roads by cost and takes each road whose ends are still in different groups, exactly when unite returns true. The sort, O(m \log m), sets the running time.

Deletions backwards

When roads close one at a time and all closures are known in advance, run them backwards. Start with the roads that never close and add the closed roads back with unite, the last closed first. Before a road goes back, the groups are those right after it closed, so record the answer then, and print the answers in reverse order at the end.

Common mistakes

  • Comparing or linking towns instead of their roots, as in parent[a] == parent[b], which can say NO for two towns of one group. Compare find(a) == find(b).

  • Merging a group with itself, which adds its size to sz a second time. Stop when the roots are equal, as if (a == b) return false does.

  • Reading the size at a town that is not a root, where sz is out of date. Read sz[find(v)].

  • Leaving out union by size, so the first recursive find on a chain of 5 \cdot 10^5 towns goes that many calls deep and can overflow the stack. With union by size it is at most 19 calls.

Similar structures

Depth-first search labels the components of a fixed graph in O(n + m) and can give a route; breadth-first search gives the shortest one. A DSU stores no roads, so it can do neither. An adjacency list beside it keeps the roads when they are needed.

Practice

Was this page helpful?