π 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:
- Implement Union-Find with path compression + union by size in O(Ξ±(n))
- Use Union-Find to check graph connectivity and detect cycles
- Implement weighted Union-Find to solve difference/relation problems
- Use bipartite Union-Find to solve multi-relation grouping problems
- 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:
| Approach | Query Time | Merge Time |
|---|---|---|
| Brute-force BFS/DFS | O(N+M) | O(1) |
Maintain group[] array | O(1) | O(N) (need to update all) |
| Union-Find | O(Ξ±(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:
Key observations:
- Each
uniteoperation connects one tree's root to another tree's root, not two arbitrary nodes directly unite(2, 3)actually executespa[find(3)] β find(2), i.e.,pa[3] β 1. So after the merge,4still hangs under3(not directly under1)- To make
4connect directly to root1, 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.
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).
π 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:
| Optimization | Per Operation |
|---|---|
| None | O(N) |
| Path compression only | Amortized O(Ξ±(N)) |
| Union by size only | O(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:
- How many cm taller is B than A?
- 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.
Core Design
dist[x]= height[x] - height[find(x)] (x's height minus root's height)- During path compression, accumulate
distalong 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 type2 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.
Solution: Split Each Node into Three Parts
Expand each animal x into three virtual nodes:
| Node | Meaning |
|---|---|
x (original) | Set of animals of the same type as x |
x + n | Set of animals eaten by x (x's prey) |
x + 2n | Set 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
| Mistake | Cause | Fix |
|---|---|---|
| Wrong weights after path compression in weighted DSU | Didn't accumulate dist during recursion | Execute dist[x] += dist[pa[x]] before changing pa[x] |
Forgot to update sz during union | Only changed pa, forgot to maintain size | Add sz[rx] += sz[ry] |
| Array too small for bipartite DSU | Need 3N nodes | Use int pa[MAXN * 3] |
| Check contradiction after unite | After merging, info is fused, can't detect contradiction | Must check contradiction first, then unite |
find recursion stack overflow | Very long chains (N > 10^5) exceed recursion depth | Use 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 Bquery A B: ask if A and B can communicateblock 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:
- Record all operations
- Process from back to front:
blockbecomesconnect; forwardconnectoperations that occur before ablockneed to be excluded - 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.