iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Union-Find

Union-Find (Disjoint Set Union) tracks groups of elements with two near-instant operations: find(x) returns the group representative, union(x, y) merges two groups. With path compression + union by rank, every operation is amortised O(α(n)) — effectively constant.

Implementation + Kruskal + connectivity

EXAMPLE
// 1) Core data structure
class UnionFind {
    constructor(n) {
        this.parent = Array.from({ length: n }, (_, i) => i);
        this.rank   = new Array(n).fill(0);
        this.count  = n;                                  // number of components
    }

    find(x) {
        // Path compression — every node on the path points directly at the root after this call
        while (this.parent[x] !== x) {
            this.parent[x] = this.parent[this.parent[x]];
            x = this.parent[x];
        }
        return x;
    }

    union(x, y) {
        const rx = this.find(x), ry = this.find(y);
        if (rx === ry) return false;                       // already in same set
        // Union by rank — attach the shallower tree under the deeper
        if (this.rank[rx] < this.rank[ry])        this.parent[rx] = ry;
        else if (this.rank[rx] > this.rank[ry])    this.parent[ry] = rx;
        else                                       { this.parent[ry] = rx; this.rank[rx]++; }
        this.count--;
        return true;
    }

    connected(x, y) { return this.find(x) === this.find(y); }
}

// 2) Demo
const uf = new UnionFind(8);
uf.union(0, 1); uf.union(1, 2); uf.union(3, 4); uf.union(5, 6);
uf.connected(0, 2);    // true
uf.connected(0, 4);    // false
uf.count;              // 5 components: {0,1,2} {3,4} {5,6} {7} … wait, count is 4 — 8 - 4 unions = 4

// 3) Counting connected components from an edge list
function componentCount(n, edges) {
    const uf = new UnionFind(n);
    for (const [a, b] of edges) uf.union(a, b);
    return uf.count;
}

componentCount(8, [[0,1],[1,2],[3,4],[5,6]]);    // 4 (one singleton: node 7)

// 4) Cycle detection in an undirected graph
function hasCycle(n, edges) {
    const uf = new UnionFind(n);
    for (const [a, b] of edges) {
        if (!uf.union(a, b)) return true;            // already connected -> adding this edge closes a cycle
    }
    return false;
}

// 5) Kruskal's algorithm — minimum spanning tree on a weighted graph
function mstWeight(n, edges) {
    // edges: [a, b, weight]
    const uf = new UnionFind(n);
    let weight = 0, used = 0;
    for (const [a, b, w] of edges.slice().sort((x, y) => x[2] - y[2])) {
        if (uf.union(a, b)) {
            weight += w;
            if (++used === n - 1) break;
        }
    }
    return used === n - 1 ? weight : Infinity;
}

// 6) Friend circles / number-of-islands (graph variants)
function friendCircles(M) {
    const n = M.length;
    const uf = new UnionFind(n);
    for (let i = 0; i < n; i++) {
        for (let j = i + 1; j < n; j++) {
            if (M[i][j]) uf.union(i, j);
        }
    }
    return uf.count;
}

// 7) Number of islands by union over grid neighbours
function numIslands(grid) {
    const R = grid.length, C = grid[0].length;
    const uf = new UnionFind(R * C);
    let zeros = 0;
    const id  = (r, c) => r * C + c;
    for (let r = 0; r < R; r++) {
        for (let c = 0; c < C; c++) {
            if (grid[r][c] === '0') { zeros++; continue; }
            if (r > 0 && grid[r - 1][c] === '1') uf.union(id(r, c), id(r - 1, c));
            if (c > 0 && grid[r][c - 1] === '1') uf.union(id(r, c), id(r, c - 1));
        }
    }
    return uf.count - zeros;        // subtract the singleton water cells
}

// 8) Dynamic connectivity (offline)
// Process a stream of operations:
//   union(a, b)
//   query(a, b) -> are they connected?
// Easy: just run union and find as they arrive.
// Watch out for OFFLINE deletes (edges removed) — that's a different beast (link-cut trees or LCT).

// 9) Accounts merge — group accounts by shared email
function accountsMerge(accounts) {
    const emailToIdx = new Map();
    const uf = new UnionFind(accounts.length);
    accounts.forEach(([name, ...emails], i) => {
        for (const e of emails) {
            if (emailToIdx.has(e)) uf.union(i, emailToIdx.get(e));
            else                    emailToIdx.set(e, i);
        }
    });
    const byRoot = new Map();
    emailToIdx.forEach((idx, email) => {
        const r = uf.find(idx);
        if (!byRoot.has(r)) byRoot.set(r, new Set());
        byRoot.get(r).add(email);
    });
    return [...byRoot].map(([root, set]) => [accounts[root][0], ...[...set].sort()]);
}

// 10) Weighted union-find — track relative offsets between nodes
class WeightedUF {
    constructor(n) {
        this.parent = Array.from({ length: n }, (_, i) => i);
        this.weight = new Array(n).fill(0);      // weight[x] = offset from x to parent
    }
    find(x) {
        if (this.parent[x] === x) return x;
        const root = this.find(this.parent[x]);
        this.weight[x] += this.weight[this.parent[x]];
        this.parent[x] = root;
        return root;
    }
    union(x, y, diff) {                          // y - x = diff
        const rx = this.find(x), ry = this.find(y);
        if (rx === ry) return this.weight[y] - this.weight[x] === diff;
        this.parent[rx] = ry;
        this.weight[rx] = diff + this.weight[y] - this.weight[x];
        return true;
    }
}

// 11) Complexity
//   Without compression / rank: O(n) per op worst case
//   With path compression + union by rank: O(α(n)) per op amortised — α grows < 5 for any conceivable n

// 12) When NOT to use union-find
//   • You need to SPLIT groups dynamically — union-find doesn't support undo or split cleanly
//   • You need to enumerate members in a group — store a separate list keyed by root
//   • Online deletions of edges — too expensive without specialised data structures
//   • You need shortest paths — use BFS / Dijkstra

// 13) Common bugs
//   • Forgetting to do path compression in find() → O(n) per op on adversarial inputs
//   • Updating parent[x] without re-running find on the children → stale pointers in iteration
//   • Using object literals for parent map with non-integer keys → use Map for clarity
//   • Counting components by uniqueing parent[i] WITHOUT calling find — gives the wrong answer
//   • Forgetting to decrement count on a successful union

Why it matters

Union-Find with path compression and union by rank is the workhorse for connectivity, Kruskal’s MST, friend circles, and accounts-merge. The operations are practically constant time, but make sure both optimisations are present — without compression, adversarial inputs collapse to O(n) per call.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
class DSU {
    constructor(n) { this.p = [...Array(n).keys()]; this.r = Array(n).fill(0); }
    find(x) { return this.p[x] === x ? x : (this.p[x] = this.find(this.p[x])); }
    union(a, b) {
        a = this.find(a); b = this.find(b);
        if (a === b) return false;
        if (this.r[a] < this.r[b]) [a, b] = [b, a];
        this.p[b] = a;
        if (this.r[a] === this.r[b]) this.r[a]++;
        return true;
    }
}
Try it Yourself »

Exercise

Operation that joins two sets.

dsu. (a, b)

Discussion

Loading…