Disjoint Set Union (Union-Find)

Disjoint Set Union (DSU)

Read: ~30 min Intermediate 7 practice problems Live code runner

1. The Big Idea — "Are These Two Things Connected?"

DSU (also called Union-Find) is the simplest data structure on Earth and yet one of the most powerful. It maintains a partition of n elements into disjoint groups and answers two questions blazingly fast:

  • find(x) — to which group does x belong?
  • union(x, y) — merge the groups of x and y.
DSU বা Union-Find হলো পৃথিবীর সবচেয়ে সহজ অথচ সবচেয়ে শক্তিশালী data structure-গুলোর একটি। এটি n-টি element-কে কয়েকটি disjoint দলে ভাগ করে রাখে এবং দুটি প্রশ্নের অবিশ্বাস্য দ্রুত উত্তর দেয়: (১) find(x) — x কোন দলে আছে? (২) union(x, y) — x ও y-এর দল দুটিকে এক করে দাও।
Key insight DSU operations প্রায় O(α(n)) — যেটি বাস্তবে সব সময় ৫-এর কম।

With union-by-rank + path compression, every operation runs in inverse Ackermann time α(n) — a function so slow-growing that for any n ≤ 10600, α(n) ≤ 5.

2. Each Group Is a Tree

We store each group as a rooted tree; the root's name is the "group ID". Every element points to its parent, and the root points to itself. find(x) walks up parent pointers; union(x, y) attaches one root under the other.

প্রতিটি দলকে একটি গাছের মতো ভাবুন — গাছের root-ই দলটির পরিচয়। প্রতিটি element তার parent-কে চেনে, এবং root নিজেকেই তার parent বলে রাখে। find(x) root পর্যন্ত উঠে যায়; union(x, y) দুটি গাছের root-কে একটিকে অন্যটির নিচে জুড়ে দেয়।
Before find(7) — tall chain 1 2 4 5 7 After find(7) — path compressed 1 2 4 5 7 Path compression flattens the tree on every find. Future calls are O(1). Figure 22.1 — Path compression-এর আগে ও পরে DSU-এর অবস্থা।

3. Two Heuristics That Change Everything

HeuristicWhat it doesEffect
Union by rank/sizeAlways attach the shorter (smaller) tree under the taller (larger) one.Tree height stays O(log n).
Path compressionDuring find, repoint every visited node directly to the root.Amortizes future finds to O(α).
Both together—Each operation is essentially O(α(n)).
About α(n) Tarjan (1975) proved that any sequence of m operations on n elements takes O(m·α(n)) time. α(n) — the inverse Ackermann function — grows so slowly it stays under 5 for any n that fits in the observable universe.

Tarjan-এর প্রমাণ: m-টি operation ও n-টি element-এর মোট সময় O(m·α(n))। α(n) এত ধীরে বাড়ে যে দৃশ্যমান মহাবিশ্বে ফিট হওয়া যেকোনো n-এর জন্য α(n) ≤ ৫।

4. Live Code — DSU with Rank + Path Compression

dsu.cpp
#include <bits/stdc++.h>
using namespace std;

struct DSU {
    vector<int> par, rnk;
    int components;

    DSU(int n) : par(n), rnk(n, 0), components(n) {
        iota(par.begin(), par.end(), 0);  // par[i] = i
    }

    int find(int x) {
        while (par[x] != x) {
            par[x] = par[par[x]];     // path compression (halving)
            x = par[x];
        }
        return x;
    }

    bool unite(int x, int y) {
        x = find(x); y = find(y);
        if (x == y) return false;
        if (rnk[x] < rnk[y]) swap(x, y);
        par[y] = x;
        if (rnk[x] == rnk[y]) ++rnk[x];
        --components;
        return true;
    }

    bool connected(int x, int y) { return find(x) == find(y); }
};

int main() {
    DSU d(7);                 // 7 cities: 0..6
    d.unite(0, 1);            // Dhaka — Gazipur
    d.unite(1, 2);            // Gazipur — Narsingdi
    d.unite(3, 4);            // Chittagong — Cox's Bazar

    cout << "0 & 2 connected? " << d.connected(0, 2) << "\n";
    cout << "0 & 3 connected? " << d.connected(0, 3) << "\n";
    cout << "Components = " << d.components << "\n";

    d.unite(2, 4);            // link the two regions
    cout << "After uniting 2 & 4 → components = " << d.components << "\n";
    return 0;
}
par[i] array-তে আমরা প্রতিটি element-এর parent রাখি। শুরুতে সবাই নিজেই নিজের parent। find-এ path halving ব্যবহার করেছি (একটি লাইনে: par[x] = par[par[x]]) — এটি classic full compression-এর মতই দ্রুত কিন্তু লেখা সহজ।

5. Live Code — Connected Components of an Undirected Graph

For every edge, call unite(u, v). After processing all edges, the number of distinct roots equals the number of connected components.

components.cpp
#include <bits/stdc++.h>
using namespace std;

struct DSU {
    vector<int> p, r;
    DSU(int n) : p(n), r(n, 0) { iota(p.begin(), p.end(), 0); }
    int find(int x) { return p[x] == x ? x : p[x] = find(p[x]); }
    bool unite(int a, int b) {
        a = find(a); b = find(b);
        if (a == b) return false;
        if (r[a] < r[b]) swap(a, b);
        p[b] = a;
        if (r[a] == r[b]) ++r[a];
        return true;
    }
};

int main() {
    int n = 8;
    vector<pair<int,int>> edges = {{0,1},{1,2},{3,4},{5,6},{6,7}};
    DSU d(n);
    for (auto& [u, v] : edges) d.unite(u, v);

    int comps = 0;
    for (int i = 0; i < n; ++i) if (d.find(i) == i) ++comps;
    cout << "Connected components = " << comps << "\n";
    return 0;
}
Where DSU shines Kruskal MST, Boruvka, dynamic connectivity, offline range-color queries, DSU on tree, and even compiler register allocation use DSU as a first-class citizen.

6. Practice Problems

নিচের প্রতিটি সমস্যায় DSU-এর একটি বাস্তব প্রয়োগ আছে। প্রতিটির সাথে চালানোযোগ্য C++ কোড দেওয়া।
  1. Number of Provinces — given an n×n adjacency matrix, count connected groups.
    n×n adjacency matrix থেকে connected group-এর সংখ্যা বের করুন।
    Show Answer
    provinces.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int p[200];
    int f(int x) { return p[x] == x ? x : p[x] = f(p[x]); }
    int main() {
        vector<vector<int>> M = {{1,1,0},{1,1,0},{0,0,1}};
        int n = M.size();
        iota(p, p + n, 0);
        for (int i = 0; i < n; ++i)
            for (int j = i + 1; j < n; ++j)
                if (M[i][j]) p[f(i)] = f(j);
        int ans = 0;
        for (int i = 0; i < n; ++i) if (f(i) == i) ++ans;
        cout << ans << "\n";
    }
  2. Redundant Connection — given an undirected graph that is a tree plus one extra edge, find the extra edge.
    একটি tree-এর সাথে অতিরিক্ত একটি edge যোগ করা আছে — সেই অতিরিক্ত edge-টি বের করুন।
    Show Answer
    redundant.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int p[1010];
    int f(int x) { return p[x] == x ? x : p[x] = f(p[x]); }
    int main() {
        vector<pair<int,int>> e = {{1,2},{2,3},{3,4},{1,4},{1,5}};
        iota(p, p + 1010, 0);
        for (auto& [u, v] : e) {
            if (f(u) == f(v)) { cout << u << " " << v << "\n"; return 0; }
            p[f(u)] = f(v);
        }
    }
  3. Accounts Merge — merge accounts that share at least one email; output unified email list per person.
    যেসব account-এ অন্তত একটি email common, সেগুলো merge করুন এবং প্রতি ব্যক্তির জন্য combined email list বের করুন।
    Show Answer
    accounts.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int p[5000];
    int f(int x){return p[x]==x?x:p[x]=f(p[x]);}
    int main() {
        vector<vector<string>> A = {
            {"Arif","a@x","a@y"},
            {"Arif","a@y","a@z"},
            {"Mim","m@x"}
        };
        iota(p, p + 5000, 0);
        unordered_map<string,int> idx;
        unordered_map<string,string> owner;
        for (int i = 0; i < (int)A.size(); ++i) {
            for (int j = 1; j < (int)A[i].size(); ++j) {
                if (!idx.count(A[i][j])) { idx[A[i][j]] = idx.size(); owner[A[i][j]] = A[i][0]; }
                p[f(idx[A[i][1]])] = f(idx[A[i][j]]);
            }
        }
        map<int, set<string>> g;
        for (auto& [e, i] : idx) g[f(i)].insert(e);
        for (auto& [k, s] : g) {
            cout << owner[*s.begin()] << ": ";
            for (auto& e : s) cout << e << " ";
            cout << "\n";
        }
    }
  4. Minimum Cost to Connect Cities — Kruskal's MST using DSU.
    শহরগুলো connect করার সর্বনিম্ন খরচ — Kruskal MST।
    Show Answer
    kruskal.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int p[100];
    int f(int x){return p[x]==x?x:p[x]=f(p[x]);}
    int main() {
        int n = 4;
        vector<tuple<int,int,int>> e = {{1,0,1},{4,0,2},{2,1,2},{3,1,3},{5,2,3}};
        sort(e.begin(), e.end());
        iota(p, p + n, 0);
        int cost = 0;
        for (auto& [w, u, v] : e) {
            if (f(u) != f(v)) { p[f(u)] = f(v); cost += w; }
        }
        cout << "MST cost = " << cost << "\n";
    }
  5. Smallest Equivalent String — given pairs of equivalent characters, output the lexicographically smallest version of a string.
    কিছু character-জোড়া equivalent দেওয়া আছে; একটি string-এর lexicographically smallest version বের করুন।
    Show Answer
    eqstr.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int p[26];
    int f(int x){return p[x]==x?x:p[x]=f(p[x]);}
    void u(int a,int b){a=f(a);b=f(b);if(a==b)return;if(a<b)p[b]=a;else p[a]=b;}
    int main() {
        string s1 = "parker", s2 = "morris", base = "parser";
        iota(p, p + 26, 0);
        for (int i = 0; i < (int)s1.size(); ++i) u(s1[i] - 'a', s2[i] - 'a');
        for (char& c : base) c = f(c - 'a') + 'a';
        cout << base << "\n";
    }
  6. Satisfiability of Equality Equations — given equations like "a==b" and "b!=c", say whether all are satisfiable.
    "a==b" ও "b!=c" এর মতো সমীকরণ দেওয়া আছে — সবগুলো একসাথে সঙ্গতিপূর্ণ কিনা বলুন।
    Show Answer
    equations.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int p[26];
    int f(int x){return p[x]==x?x:p[x]=f(p[x]);}
    int main() {
        vector<string> E = {"a==b","b==c","a!=c"};
        iota(p, p + 26, 0);
        for (auto& e : E) if (e[1] == '=') p[f(e[0] - 'a')] = f(e[3] - 'a');
        for (auto& e : E)
            if (e[1] == '!' && f(e[0] - 'a') == f(e[3] - 'a')) {
                cout << "unsatisfiable\n"; return 0;
            }
        cout << "satisfiable\n";
    }
  7. Number of Islands II — given a grid and a stream of "add land" operations, output the island count after each.
    গ্রিডে একটির পর একটি ভূমি যোগ হচ্ছে — প্রতিবার যোগের পর island-এর সংখ্যা প্রিন্ট করুন।
    Show Answer
    islands2.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int p[10000];
    int f(int x){return p[x]==x?x:p[x]=f(p[x]);}
    int main() {
        int R = 3, C = 3;
        vector<int> land(R*C, 0);
        iota(p, p + R*C, 0);
        vector<pair<int,int>> ops = {{0,0},{0,1},{1,2},{2,1}};
        int cnt = 0, dr[] = {-1,1,0,0}, dc[] = {0,0,-1,1};
        for (auto& [r, c] : ops) {
            int id = r*C + c;
            if (land[id]) { cout << cnt << " "; continue; }
            land[id] = 1; ++cnt;
            for (int k = 0; k < 4; ++k) {
                int nr = r + dr[k], nc = c + dc[k];
                if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
                int nid = nr*C + nc;
                if (land[nid] && f(id) != f(nid)) { p[f(id)] = f(nid); --cnt; }
            }
            cout << cnt << " ";
        }
        cout << "\n";
    }

Summary — Module 22

DSU stores each set as a tree, exposes find and unite, and with the two heuristics (union-by-rank + path compression) every operation runs in nearly constant time. It is the secret weapon behind Kruskal's MST, dynamic connectivity, and dozens of offline graph problems.

DSU প্রতিটি দলকে একটি গাছ হিসেবে রাখে; দুটি heuristic যোগ করলে প্রতিটি operation প্রায় constant সময়ে শেষ হয়। Kruskal MST থেকে শুরু করে dynamic connectivity, offline range query — সবখানেই DSU কাজে লাগে।

Next Module → Segment Trees & Fenwick (BIT) — competitive programming-এর সর্বশক্তিশালী জোড়া অস্ত্র।