πŸ“– Chapter 5.6: Union-Find (Disjoint Set Union)

⏱ Estimated reading time: 60 minutes | Difficulty: 🟑 Medium


Prerequisites

Before studying this chapter, make sure you understand:

  • Arrays and functions (Chapter 2.3)
  • Basic graph concepts β€” nodes, edges, connectivity (Chapter 5.1)

🎯 Learning Goals

After completing this chapter, you will be able to:

  1. Implement Union-Find with path compression + union by size in O(Ξ±(n))
  2. Use Union-Find to check graph connectivity and detect cycles
  3. Implement weighted Union-Find to solve difference/relation problems
  4. Use bipartite Union-Find to solve multi-relation grouping problems
  5. Independently solve 10 practice problems from basic to challenge level

5.6.1 Starting from a Real Problem

Problem: Network Connectivity

You manage a data center network of N servers (numbered 1~N). Engineers gradually establish direct links (undirected) between pairs of servers. You need to answer at any time: Can server A and server B communicate with each other?

Initial state: each server is isolated
                1    2    3    4    5

Add link (1,2): 1β€”β€”2    3    4    5
Add link (3,4): 1β€”β€”2    3β€”β€”4    5
Add link (2,3): 1β€”β€”2β€”β€”3β€”β€”4    5

Query (1,4): can communicate βœ“ (1β†’2β†’3β†’4)
Query (1,5): cannot communicate βœ— (5 is isolated)

Bottleneck of naive approaches:

ApproachQuery TimeMerge Time
Brute-force BFS/DFSO(N+M)O(1)
Maintain group[] arrayO(1)O(N) (need to update all)
Union-FindO(Ξ±(N)) β‰ˆ O(1)O(Ξ±(N)) β‰ˆ O(1)

When N and M reach 10^5 and operations are interleaved, Union-Find is the only practical choice.


5.6.2 Core Idea: Represent a Set as a Tree

Key insight: Organize servers in the same connected component into a tree. The tree's root node serves as the "representative" of that set.

  • Check if two servers are connected: see if they are in the same tree (same root)
  • Merge two connected components: point one tree's root to the other tree's root

Use a pa[] (parent) array to represent this forest:

Union-Find forest construction: structure after three unions

Key observations:

  • Each unite operation connects one tree's root to another tree's root, not two arbitrary nodes directly
  • unite(2, 3) actually executes pa[find(3)] ← find(2), i.e., pa[3] ← 1. So after the merge, 4 still hangs under 3 (not directly under 1)
  • To make 4 connect directly to root 1, we need path compression (Section 5.6.4)

5.6.3 Two Core Operations

Find (Find the Root)

Climb up the pa[] pointers to find the root:

int find(int x) {
    while (pa[x] != x)
        x = pa[x];
    return x;  // when pa[x] == x, x is the root
}

Check if A and B are connected: find(A) == find(B)

Unite (Merge Two Sets)

Connect the roots of two trees:

void unite(int x, int y) {
    int rx = find(x);
    int ry = find(y);
    if (rx != ry)
        pa[rx] = ry;  // attach tree of rx under ry
}

5.6.4 Optimization 1: Path Compression

Problem: If we always attach new trees under old trees, the tree may degenerate into a long chain:

1 ← 2 ← 3 ← 4 ← 5 ← 6

find(1) requires 5 steps, time O(N)

Path compression: During find(x), directly connect all nodes on the path to the root.

Union-Find Path Compression Before and After

int find(int x) {
    // If x is not the root, recursively find root
    // Then set x's parent directly to root ("flatten")
    return pa[x] == x ? x : pa[x] = find(pa[x]);
}

5.6.5 Optimization 2: Union by Size

Problem: If we always attach large trees under small trees, the large tree gets taller and find gets slower.

Union by size: Attach the small tree under the large tree, guaranteeing tree height ≀ O(log N).

Union by Size: Small Tree Under Large Tree

πŸ“„ Complete C++ implementation
struct DSU {
    vector<int> pa, sz;   // sz[i] = total nodes in tree rooted at i
    int groups;           // current number of connected components
    
    explicit DSU(int n) : pa(n + 1), sz(n + 1, 1), groups(n) {
        iota(pa.begin(), pa.end(), 0);  // pa[i] = i
    }
    
    // Find root with path compression
    int find(int x) {
        return pa[x] == x ? x : pa[x] = find(pa[x]);
    }
    
    // Union by size; returns true if they were in different sets (merge happened)
    bool unite(int x, int y) {
        x = find(x); y = find(y);
        if (x == y) return false;       // already in same set
        if (sz[x] < sz[y]) swap(x, y); // x is the larger tree
        pa[y] = x;                      // attach small tree under large tree
        sz[x] += sz[y];
        groups--;
        return true;
    }
    
    // Check if connected
    bool connected(int x, int y) { return find(x) == find(y); }
    
    // Size of the component containing x
    int size(int x) { return sz[find(x)]; }
};

Complexity:

OptimizationPer Operation
NoneO(N)
Path compression onlyAmortized O(Ξ±(N))
Union by size onlyO(log N)
Both together (recommended)Amortized O(Ξ±(N)) β‰ˆ O(1)

Ξ±(N) is the inverse Ackermann function, which grows extremely slowly: Ξ±(10^80) < 5. In practice, treat it as a constant.


5.6.6 Back to Network Connectivity: Complete Code

Now let's solve the opening problem completely with Union-Find:

πŸ“„ Complete solution for network connectivity
#include <bits/stdc++.h>
using namespace std;

struct DSU {
    vector<int> pa, sz;
    int groups;
    explicit DSU(int n) : pa(n + 1), sz(n + 1, 1), groups(n) {
        iota(pa.begin(), pa.end(), 0);
    }
    int find(int x) { return pa[x] == x ? x : pa[x] = find(pa[x]); }
    bool unite(int x, int y) {
        x = find(x); y = find(y);
        if (x == y) return false;
        if (sz[x] < sz[y]) swap(x, y);
        pa[y] = x; sz[x] += sz[y]; groups--;
        return true;
    }
    bool connected(int x, int y) { return find(x) == find(y); }
};

int main() {
    int n, q;
    cin >> n >> q;
    DSU dsu(n);
    
    while (q--) {
        int op, a, b;
        cin >> op >> a >> b;
        if (op == 1) {
            // Add link
            if (dsu.unite(a, b))
                cout << "New link: " << a << " - " << b << "\n";
            else
                cout << "Already connected, no new link needed\n";
        } else {
            // Query
            cout << (dsu.connected(a, b) ? "Can communicate" : "Cannot communicate") << "\n";
        }
    }
    return 0;
}

Example trace:

Input: 5 6
       1 1 2    β†’ add link 1-2, new (not connected before)
       1 3 4    β†’ add link 3-4, new
       1 2 3    β†’ add link 2-3, new
       2 1 4    β†’ query 1 and 4 β†’ Can communicate (1β†’2β†’3β†’4)
       2 1 5    β†’ query 1 and 5 β†’ Cannot communicate (5 is isolated)
       1 1 4    β†’ add link 1-4, already connected (no new link)

5.6.7 Advanced: Weighted Union-Find

Problem Introduction

There are N students. The teacher tells you: "Student B is D cm taller than student A (i.e., height[B] - height[A] = D)."

You need to answer:

  1. How many cm taller is B than A?
  2. Does some piece of information contradict previous information?

Naive approach: Model with a graph, but each query requires BFS traversal, O(N) per query is too slow.

Weighted Union-Find idea: Store "the height difference from each node to its root dist[x]" at each node. Queries directly use dist subtraction.

Weighted Union-Find dist[] Diagram

Core Design

  • dist[x] = height[x] - height[find(x)] (x's height minus root's height)
  • During path compression, accumulate dist along the path, connecting x directly to root:
Before compression: x β†’ p β†’ root
  dist[x] = height[x] - height[p]
  dist[p] = height[p] - height[root]

After compression: x β†’ root
  new dist[x] should = height[x] - height[root]
                     = dist[x] + dist[p]
int find(int x) {
    if (pa[x] == x) return x;
    int root = find(pa[x]);
    dist[x] += dist[pa[x]];  // accumulate path weights during compression
    pa[x] = root;
    return root;
}

Computing New Edge Weight During Union

"Declare height[y] - height[x] = d":

πŸ“„ Derivation of new edge weight
We have: dist[x] = height[x] - height[rx]
         dist[y] = height[y] - height[ry]

If we attach ry under rx, we need dist[ry] to satisfy:
    height[ry] - height[rx] = ?
    
From height[y] - height[x] = d:
    (dist[y] + height[ry]) - (dist[x] + height[rx]) = d
    height[ry] - height[rx] = d + dist[x] - dist[y]

Therefore dist[ry] = d + dist[x] - dist[y]
πŸ“„ Complete C++ implementation
// Complete Weighted Union-Find
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
int pa[MAXN], sz_arr[MAXN];
long long dist[MAXN];  // dist[x] = height[x] - height[find(x)]

void init(int n) {
    for (int i = 1; i <= n; i++) { pa[i] = i; sz_arr[i] = 1; dist[i] = 0; }
}

int find(int x) {
    if (pa[x] == x) return x;
    int root = find(pa[x]);
    dist[x] += dist[pa[x]];
    pa[x] = root;
    return root;
}

// Declare height[y] - height[x] = d
// Returns true = no contradiction; false = contradicts known info
bool add_info(int x, int y, long long d) {
    int rx = find(x), ry = find(y);
    long long dx = dist[x], dy = dist[y];
    
    if (rx == ry) {
        // Already in same set, verify no contradiction
        return (dy - dx == d);
    }
    
    // Merge: attach small tree under large tree
    if (sz_arr[rx] < sz_arr[ry]) {
        swap(rx, ry); swap(dx, dy); d = -d;
    }
    pa[ry] = rx;
    dist[ry] = d + dx - dy;
    sz_arr[rx] += sz_arr[ry];
    return true;
}

long long query(int x, int y) {
    find(x); find(y);
    return dist[y] - dist[x];  // height[y] - height[x]
}

int main() {
    int n = 5;
    init(n);
    
    add_info(1, 2, 3);   // height[2] - height[1] = 3 (2 is 3cm taller than 1)
    add_info(2, 3, 5);   // height[3] - height[2] = 5
    
    // Query difference between 1 and 3
    cout << "3 is " << query(1, 3) << " cm taller than 1\n";  // outputs 8
    
    // Add contradictory info
    cout << (add_info(1, 3, 10) ? "Consistent" : "Contradiction") << "\n";  // Contradiction (should be 8)
    cout << (add_info(1, 3, 8)  ? "Consistent" : "Contradiction") << "\n";  // Consistent
    
    return 0;
}

5.6.8 Advanced: Bipartite Union-Find (Species DSU)

Problem Introduction

Classic problem: In an animal kingdom there are three types of animals A, B, C, satisfying: A eats B, B eats C, C eats A.

Input N pieces of information one by one, in the format:

  • 1 X Y: X and Y are the same type
  • 2 X Y: X eats Y

If some information contradicts all previous true information, it is a "lie." Find the total number of lies.

Key challenge: Need to simultaneously track "same type" and "predator-prey" relationships. Regular Union-Find can only handle one equivalence relation.

Bipartite Union-Find: Animal Triangle Relationship

Solution: Split Each Node into Three Parts

Expand each animal x into three virtual nodes:

NodeMeaning
x (original)Set of animals of the same type as x
x + nSet of animals eaten by x (x's prey)
x + 2nSet of animals that eat x (x's predators)

Processing "X and Y are the same type":

x's same-type = y's same-type    β†’ unite(x, y)
x's prey = y's prey              β†’ unite(x+n, y+n)
x's predators = y's predators    β†’ unite(x+2n, y+2n)

Processing "X eats Y" (x's prey is y's same-type):

x's prey = y's same-type         β†’ unite(x+n, y)
x's predators = y's prey         β†’ unite(x+2n, y+n)
x's same-type = y's predators    β†’ unite(x, y+2n)

Detecting contradiction for "X and Y are the same type":

If connected(x, y+n) β†’ contradiction (x and y have predator-prey relationship)
If connected(x, y+2n) β†’ contradiction

Detecting contradiction for "X eats Y":

If connected(x, y) β†’ contradiction (same type cannot have predator-prey relationship)
If connected(x, y+n) β†’ contradiction (y eats x, but claim is x eats y)
πŸ“„ Complete C++ implementation
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 150005;
int pa[MAXN * 3], sz[MAXN * 3];

void init(int n) {
    for (int i = 0; i < 3 * (n + 1); i++) { pa[i] = i; sz[i] = 1; }
}
int find(int x) { return pa[x] == x ? x : pa[x] = find(pa[x]); }
bool same(int x, int y) { return find(x) == find(y); }
void unite(int x, int y) {
    x = find(x); y = find(y);
    if (x == y) return;
    if (sz[x] < sz[y]) swap(x, y);
    pa[y] = x; sz[x] += sz[y];
}

int main() {
    int n, k;
    cin >> n >> k;
    init(n);
    
    int lies = 0;
    while (k--) {
        int t, x, y;
        cin >> t >> x >> y;
        
        // Out-of-range numbers are immediately lies
        if (x < 1 || x > n || y < 1 || y > n) { lies++; continue; }
        
        if (t == 1) {
            // Declare x and y are the same type
            if (same(x, y + n) || same(x, y + 2 * n)) {
                lies++;  // contradiction: x and y have predator-prey relationship
            } else {
                unite(x, y);
                unite(x + n, y + n);
                unite(x + 2 * n, y + 2 * n);
            }
        } else {
            // Declare x eats y
            if (x == y) { lies++; continue; }  // cannot eat itself
            if (same(x, y) || same(x, y + n)) {
                lies++;  // contradiction
            } else {
                unite(x + n, y);
                unite(x + 2 * n, y + n);
                unite(x, y + 2 * n);
            }
        }
    }
    
    cout << lies << "\n";
    return 0;
}

⚠️ Common Mistakes

MistakeCauseFix
Wrong weights after path compression in weighted DSUDidn't accumulate dist during recursionExecute dist[x] += dist[pa[x]] before changing pa[x]
Forgot to update sz during unionOnly changed pa, forgot to maintain sizeAdd sz[rx] += sz[ry]
Array too small for bipartite DSUNeed 3N nodesUse int pa[MAXN * 3]
Check contradiction after uniteAfter merging, info is fused, can't detect contradictionMust check contradiction first, then unite
find recursion stack overflowVery long chains (N > 10^5) exceed recursion depthUse iterative path compression instead

πŸ’ͺ Practice Problems (10 problems, all with complete solutions)

🟒 Basic Practice (1~4)

Problem 1: Count Friend Groups
Given N people (numbered 1~N) and M friendship pairs. People in the same friend group can communicate with each other.
Find the total number of friend groups.

Input: N M, then M lines each with A B meaning A and B are friends.
Output: Number of friend groups.

Example:

Input:  5 3
        1 2
        2 3
        4 5

Output: 2
({1,2,3} and {4,5})
βœ… Complete Solution

Idea: Each time we merge, if the two people were not in the same set (unite returns true), decrement component count by 1. Final dsu.groups is the answer.

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

struct DSU {
    vector<int> pa, sz;
    int groups;
    explicit DSU(int n) : pa(n + 1), sz(n + 1, 1), groups(n) {
        iota(pa.begin(), pa.end(), 0);
    }
    int find(int x) { return pa[x] == x ? x : pa[x] = find(pa[x]); }
    bool unite(int x, int y) {
        x = find(x); y = find(y);
        if (x == y) return false;
        if (sz[x] < sz[y]) swap(x, y);
        pa[y] = x; sz[x] += sz[y]; groups--;
        return true;
    }
};

int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);
    int n, m;
    cin >> n >> m;
    DSU dsu(n);
    while (m--) {
        int a, b; cin >> a >> b;
        dsu.unite(a, b);
    }
    cout << dsu.groups << "\n";
    return 0;
}

Trace (N=5, M=3):

Initial groups = 5
unite(1,2) β†’ different sets, groups = 4, pa[2]=1
unite(2,3) β†’ different sets, groups = 3, pa[3]=1
unite(4,5) β†’ different sets, groups = 2, pa[5]=4
Output: 2 βœ“

Problem 2: Detect Cycle in Graph
Given N nodes and M undirected edges, determine if the graph contains a cycle.

Input: N M, then M lines each with U V for an edge.
Output: YES (has cycle) or NO (no cycle).

βœ… Complete Solution

Idea: When adding edge (u, v), if u and v are already connected (find(u)==find(v)), this edge creates a cycle.

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

struct DSU {
    vector<int> pa, sz;
    explicit DSU(int n) : pa(n + 1), sz(n + 1, 1) {
        iota(pa.begin(), pa.end(), 0);
    }
    int find(int x) { return pa[x] == x ? x : pa[x] = find(pa[x]); }
    bool unite(int x, int y) {
        x = find(x); y = find(y);
        if (x == y) return false;  // already connected = adding edge creates cycle
        if (sz[x] < sz[y]) swap(x, y);
        pa[y] = x; sz[x] += sz[y];
        return true;
    }
};

int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);
    int n, m;
    cin >> n >> m;
    DSU dsu(n);
    bool has_cycle = false;
    while (m--) {
        int u, v; cin >> u >> v;
        if (!dsu.unite(u, v)) has_cycle = true;
    }
    cout << (has_cycle ? "YES" : "NO") << "\n";
    return 0;
}

Key point: unite returning false means both endpoints are already connected β†’ this edge is redundant β†’ cycle exists.


Problem 3: Largest Connected Component
Given N nodes and M edges, output the number of nodes in the largest connected component.

βœ… Complete Solution

Idea: Use Union-Find with sz[]. After all merges, iterate all nodes and find the maximum sz[find(i)].

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

struct DSU {
    vector<int> pa, sz;
    explicit DSU(int n) : pa(n + 1), sz(n + 1, 1) {
        iota(pa.begin(), pa.end(), 0);
    }
    int find(int x) { return pa[x] == x ? x : pa[x] = find(pa[x]); }
    void unite(int x, int y) {
        x = find(x); y = find(y);
        if (x == y) return;
        if (sz[x] < sz[y]) swap(x, y);
        pa[y] = x; sz[x] += sz[y];
    }
    int size(int x) { return sz[find(x)]; }
};

int main() {
    int n, m;
    cin >> n >> m;
    DSU dsu(n);
    while (m--) {
        int u, v; cin >> u >> v;
        dsu.unite(u, v);
    }
    int ans = 0;
    for (int i = 1; i <= n; i++)
        ans = max(ans, dsu.size(i));
    cout << ans << "\n";
    return 0;
}

Problem 4: Kruskal's Minimum Spanning Tree
Given N nodes and M weighted undirected edges, find the total weight of the minimum spanning tree. Output -1 if the graph is disconnected.

βœ… Complete Solution

Idea: Kruskal's algorithm: sort all edges by weight ascending, try to add each one. If both endpoints are in different sets (no cycle), add to MST. If MST has N-1 edges at the end, the graph is connected.

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

struct DSU {
    vector<int> pa, sz;
    explicit DSU(int n) : pa(n + 1), sz(n + 1, 1) {
        iota(pa.begin(), pa.end(), 0);
    }
    int find(int x) { return pa[x] == x ? x : pa[x] = find(pa[x]); }
    bool unite(int x, int y) {
        x = find(x); y = find(y);
        if (x == y) return false;
        if (sz[x] < sz[y]) swap(x, y);
        pa[y] = x; sz[x] += sz[y];
        return true;
    }
};

int main() {
    int n, m;
    cin >> n >> m;
    
    vector<tuple<int,int,int>> edges(m);
    for (auto& [w, u, v] : edges) cin >> u >> v >> w;
    sort(edges.begin(), edges.end());  // sort by weight
    
    DSU dsu(n);
    long long total = 0;
    int cnt = 0;  // edges in MST
    
    for (auto& [w, u, v] : edges) {
        if (dsu.unite(u, v)) {
            total += w;
            cnt++;
            if (cnt == n - 1) break;  // found n-1 edges
        }
    }
    
    cout << (cnt == n - 1 ? total : -1LL) << "\n";
    return 0;
}

Trace example (N=4, edges: 1-2 w=1, 2-3 w=2, 1-3 w=3, 3-4 w=4):

Sorted: (1,1,2), (2,2,3), (3,1,3), (4,3,4)

Add edge(1,2) w=1 β†’ unite succeeds, cnt=1, total=1
Add edge(2,3) w=2 β†’ unite succeeds, cnt=2, total=3
Add edge(1,3) w=3 β†’ find(1)=find(3), already connected, skip
Add edge(3,4) w=4 β†’ unite succeeds, cnt=3=n-1, total=7

Output: 7

🟑 Intermediate Practice (5~8)

Problem 5: Network Connectivity Queries
Given N servers, M operations:

  • connect A B: establish a link between A and B
  • query A B: ask if A and B can communicate
  • block A B: disconnect the direct link between A and B (Note: not disconnecting connectivity!)

Output the result of all query operations.

Hint: Regular Union-Find doesn't support "edge deletion." Solution: offline reverse processing β€” process operations in reverse order, turning "block" into "connect."

βœ… Complete Solution

Core idea:

  1. Record all operations
  2. Process from back to front: block becomes connect; forward connect operations that occur before a block need to be excluded
  3. Use Union-Find + offline reverse processing, output query results in reverse

Here's a simplified version using offline reverse + operation recovery (assuming each pair of servers is disconnected at most once):

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

struct DSU {
    vector<int> pa, sz;
    explicit DSU(int n) : pa(n + 1), sz(n + 1, 1) {
        iota(pa.begin(), pa.end(), 0);
    }
    int find(int x) { return pa[x] == x ? x : pa[x] = find(pa[x]); }
    bool unite(int x, int y) {
        x = find(x); y = find(y);
        if (x == y) return false;
        if (sz[x] < sz[y]) swap(x, y);
        pa[y] = x; sz[x] += sz[y];
        return true;
    }
    bool connected(int x, int y) { return find(x) == find(y); }
};

int main() {
    int n, m;
    cin >> n >> m;
    
    vector<tuple<int,int,int>> ops(m);  // {type, a, b}, type: 0=connect,1=query,2=block
    set<pair<int,int>> blocked;         // blocked edges
    
    for (auto& [t, a, b] : ops) {
        string op; cin >> op >> a >> b;
        if (op == "connect") t = 0;
        else if (op == "query") t = 1;
        else { t = 2; blocked.insert({min(a,b), max(a,b)}); }
    }
    
    // Process in reverse
    DSU dsu(n);
    // First add all edges that exist in the "final state" (connect but not blocked)
    for (auto& [t, a, b] : ops) {
        if (t == 0) {
            auto key = make_pair(min(a,b), max(a,b));
            if (!blocked.count(key)) dsu.unite(a, b);
        }
    }
    
    vector<string> answers;
    for (int i = m - 1; i >= 0; i--) {
        auto [t, a, b] = ops[i];
        if (t == 2) {
            // In reverse, block becomes connect
            dsu.unite(a, b);
        } else if (t == 1) {
            answers.push_back(dsu.connected(a, b) ? "YES" : "NO");
        }
        // connect operations are not processed in reverse (already added during init)
    }
    
    reverse(answers.begin(), answers.end());
    for (auto& s : answers) cout << s << "\n";
    return 0;
}

Problem 6: Height Difference Queries (Weighted Union-Find)
N students, M pieces of information. Each piece is A B D meaning "B is D cm taller than A (D can be negative)."
Then Q queries, each asking "How many cm taller is B than A?" Output unknown if cannot be determined, conflict if there's a known contradiction.

βœ… Complete Solution
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
int pa[MAXN], sz_arr[MAXN];
long long dist[MAXN];

void init(int n) {
    for (int i = 1; i <= n; i++) { pa[i] = i; sz_arr[i] = 1; dist[i] = 0; }
}

int find(int x) {
    if (pa[x] == x) return x;
    int root = find(pa[x]);
    dist[x] += dist[pa[x]];
    pa[x] = root;
    return root;
}

// Declare height[b] - height[a] = d, returns false if contradiction
bool add_info(int a, int b, long long d) {
    int ra = find(a), rb = find(b);
    long long da = dist[a], db = dist[b];
    if (ra == rb) return (db - da == d);
    if (sz_arr[ra] < sz_arr[rb]) { swap(ra, rb); swap(da, db); d = -d; }
    pa[rb] = ra;
    dist[rb] = d + da - db;
    sz_arr[ra] += sz_arr[rb];
    return true;
}

int main() {
    int n, m, q;
    cin >> n >> m;
    init(n);
    
    bool global_conflict = false;
    for (int i = 0; i < m; i++) {
        int a, b; long long d;
        cin >> a >> b >> d;
        if (!add_info(a, b, d)) global_conflict = true;
    }
    
    cin >> q;
    while (q--) {
        int a, b; cin >> a >> b;
        int ra = find(a), rb = find(b);
        if (ra != rb) cout << "unknown\n";
        else if (global_conflict) cout << "conflict\n";
        else cout << dist[b] - dist[a] << "\n";
    }
    return 0;
}

Sample input:

5 3
1 2 3    β†’ height[2] - height[1] = 3
2 3 5    β†’ height[3] - height[2] = 5
1 3 8    β†’ height[3] - height[1] = 8 (consistent with above)

2
1 3      β†’ output 8
1 4      β†’ output unknown

Problem 7: Grid Coloring (Bipartite DSU Variant)
An NΓ—N grid, each cell initially white. M operations, each flipping all cells in a row or column (black↔white).
After all operations, answer Q queries about the color of specific cells (black or white).

Hint: Use "row Union-Find" and "column Union-Find" separately, combined with parity (odd/even flip count) to track colors.

βœ… Complete Solution (simplified: row flips only)

Use weighted Union-Find where dist[x] records parity (0=not flipped, 1=flipped odd number of times):

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

const int MAXN = 200005;
int pa[MAXN];
int flip[MAXN];  // flip[x] = parity of flips of x relative to root

void init(int n) {
    for (int i = 0; i <= n; i++) { pa[i] = i; flip[i] = 0; }
}

int find(int x) {
    if (pa[x] == x) return x;
    int root = find(pa[x]);
    flip[x] ^= flip[pa[x]];  // XOR accumulate parity along path
    pa[x] = root;
    return root;
}

// Declare "x and y are in the same group with flip relation d (0=same color, 1=different color)"
void unite(int x, int y, int d) {
    int rx = find(x), ry = find(y);
    int fx = flip[x], fy = flip[y];
    if (rx == ry) return;
    pa[ry] = rx;
    flip[ry] = d ^ fx ^ fy;
}

// Query color offset of x relative to root
int query(int x) {
    find(x);
    return flip[x];
}

This template applies to all "parity relationship" type bipartite Union-Find problems.


Problem 8: Standard Union-Find Template (Luogu P3367)
M operations: 1 X Y (merge) or 2 X Y (query if connected).

βœ… Complete Solution

This is a standard Union-Find template problem:

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

struct DSU {
    vector<int> pa, sz;
    explicit DSU(int n) : pa(n + 1), sz(n + 1, 1) {
        iota(pa.begin(), pa.end(), 0);
    }
    int find(int x) { return pa[x] == x ? x : pa[x] = find(pa[x]); }
    void unite(int x, int y) {
        x = find(x); y = find(y);
        if (x == y) return;
        if (sz[x] < sz[y]) swap(x, y);
        pa[y] = x; sz[x] += sz[y];
    }
    bool connected(int x, int y) { return find(x) == find(y); }
};

int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);
    int n, m;
    cin >> n >> m;
    DSU dsu(n);
    while (m--) {
        int op, x, y;
        cin >> op >> x >> y;
        if (op == 1) dsu.unite(x, y);
        else cout << (dsu.connected(x, y) ? "Y" : "N") << "\n";
    }
    return 0;
}

πŸ”΄ Challenge Practice (9~10)

Problem 9: Food Chain (NOIP 2001 P2024)
N animals, three-type cyclic relationship (A eats B, B eats C, C eats A). K pieces of information in the same format as Section 5.6.8.
Find the number of lies.

βœ… Complete Solution

Use the "Bipartite Union-Find" code from Section 5.6.8 directly. Key details:

  • Eating itself (x == y and type=2): lie
  • Out-of-range number (x > n or y > n): lie
  • Contradiction check before merge
// Use the code from Section 5.6.8 directly
// Key test cases:
// N=100, K=7
// 1 101 1    β†’ x=101 > N=100, lie, lies=1
// 2 1 2      β†’ declare 1 eats 2, no contradiction, merge
// 2 2 3      β†’ declare 2 eats 3, no contradiction, merge
// 2 3 1      β†’ 1 eats 2 eats 3 eats 1 ← valid cycle
// 1 1 3      β†’ 1 and 3 same type? But 1 eats 3, contradiction, lies=2
// 2 3 3      β†’ x==y, eating itself, lies=3
// 1 1 2      β†’ 1 and 2 same type? But 1 eats 2, contradiction, lies=4
// Output: 4

Complete code is in Section 5.6.8, submit directly.


Problem 10: Persistent Union-Find (Advanced Application)
Given N elements and M operations. Each operation is either a merge or a rollback to a previous version. After each operation, answer queries about the current state.

Hint: Requires Persistent Union-Find β€” use union by rank (no path compression) + segment tree to maintain historical versions of the pa[] array.

βœ… Core Idea (Framework Code)

Why no path compression? Path compression changes the tree structure; after rollback the structure would be corrupted. Use only union by rank (tree height O(log N)), each find takes at most O(log N) steps, which is acceptable.

// Persistent Union-Find framework (using persistent segment tree to maintain pa[])
// Each unite operation only modifies 2 nodes (rx and ry),
// making a point update on the segment tree to generate a new version

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

const int MAXN = 100005;
const int MAXLOG = 20;

struct Node {
    int left, right, val, rnk;  // rnk = rank of tree
} tr[MAXN * MAXLOG * 4];

int root[MAXN], cnt = 0;  // root[i] = root node of version i

// Build initial segment tree (all nodes pa[i] = i)
int build(int l, int r) {
    int node = ++cnt;
    if (l == r) {
        tr[node].val = l;  // pa[l] = l (itself is root)
        tr[node].rnk = 0;
        return node;
    }
    int mid = (l + r) / 2;
    tr[node].left = build(l, mid);
    tr[node].right = build(mid + 1, r);
    return node;
}

// Persistent point update: pa[pos] = new_val
int update(int prev, int l, int r, int pos, int new_val, int new_rnk = -1) {
    int node = ++cnt;
    tr[node] = tr[prev];  // copy previous version
    if (l == r) {
        tr[node].val = new_val;
        if (new_rnk >= 0) tr[node].rnk = new_rnk;
        return node;
    }
    int mid = (l + r) / 2;
    if (pos <= mid)
        tr[node].left = update(tr[prev].left, l, mid, pos, new_val, new_rnk);
    else
        tr[node].right = update(tr[prev].right, mid + 1, r, pos, new_val, new_rnk);
    return node;
}

// Query pa[pos]
int query(int node, int l, int r, int pos) {
    if (l == r) return tr[node].val;
    int mid = (l + r) / 2;
    if (pos <= mid) return query(tr[node].left, l, mid, pos);
    return query(tr[node].right, mid + 1, r, pos);
}

int n;

// Find root of x in version ver (no path compression!)
int find(int ver, int x) {
    int p = query(root[ver], 1, n, x);
    if (p == x) return x;
    return find(ver, p);
}

int main() {
    int m;
    cin >> n >> m;
    root[0] = build(1, n);
    
    for (int i = 1; i <= m; i++) {
        int op; cin >> op;
        if (op == 1) {
            // Merge
            int x, y; cin >> x >> y;
            int rx = find(i - 1, x), ry = find(i - 1, y);
            // ... union by rank, generate root[i]
        } else if (op == 2) {
            // Rollback to version k
            int k; cin >> k;
            root[i] = root[k];
        } else {
            // Query
            int x; cin >> x;
            cout << find(i - 1, x) << "\n";
            root[i] = root[i - 1];
        }
    }
    return 0;
}

Full implementation reference: Luogu P3402 Persistent Union-Find


πŸ’‘ Chapter Connections: Union-Find is one of the most fundamental tools in graph theory β€” checking connectivity (Chapter 5.2), Kruskal's MST (Chapter 8.1) both rely on Union-Find. Weighted Union-Find appears frequently in USACO Gold and competitive programming; it's highly recommended to master it.