在掌握了普通并查集后,你可能会发现:面对 “朋友的敌人是敌人”“A 吃 B、B 吃 C、C 吃 A” 这类包含多种关系或量化约束的问题时,普通并查集就像 “巧妇难为无米之炊”—— 它只能维护 “是否同属一个集合”,却无法表达元素间更复杂的关联。 别担心!今天这篇文章,我们就来解锁并查集的两大进阶形态:扩展域并查集和带权并查集。前者能搞定 “多关系共存” 的场景,后者可处理 “节点间有量化信息” 的问题。从原理到实战,从代码到思路,保证让你学完就能上手解决复杂问题!下面就让我们正式开始吧!
先看一个经典场景:有 n 个人,他们之间只有 “朋友” 或 “敌人” 两种关系,且满足 “朋友的朋友是朋友,敌人的敌人是朋友”。现在要判断两个人是否能归为同一阵营(朋友)。
如果用普通并查集:
这时候,扩展域并查集就派上用场了 —— 它的核心思路是:给每个元素 “拆分” 成多个 “域”(比如朋友域、敌人域),每个域代表一种状态,通过维护不同域之间的关系,间接表达元素间的复杂关联。
我们以 “朋友 - 敌人” 问题为例,具体解释扩展域的设计:
通过这种 “域的合并”,我们就能用并查集的方式,间接维护 “朋友” 和 “敌人” 两种关系了。
每个元素 x 的朋友域 x 和敌人域 x+n,初始时都是独立集合(父节点指向自己):
#include <iostream>
using namespace std;
const int N = 1010; // 假设最多1000人
int fa[N * 2]; // 扩展域:x是朋友域,x+N是敌人域
int n, m; // n是人数,m是关系数
// 初始化并查集(带路径压缩)
void init() {
for (int i = 1; i <= 2 * n; i++) { // 注意:域的范围是1~2n
fa[i] = i;
}
}
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void un(int x, int y) { // 合并x和y所在的集合
int fx = find(x);
int fy = find(y);
if (fx != fy) {
fa[fy] = fx; // 让y的根节点指向x的根节点(方向不影响,统一即可)
}
}// 处理关系:op是'F'(朋友)或'E'(敌人),x和y是两个人
void handle_relation(char op, int x, int y) {
if (op == 'F') {
// 朋友:x的朋友域 ↔ y的朋友域,x的敌人域 ↔ y的敌人域
un(x, y);
un(x + n, y + n);
} else { // op == 'E'
// 敌人:x的朋友域 ↔ y的敌人域,x的敌人域 ↔ y的朋友域
un(x, y + n);
un(x + n, y);
}
}两个人是否能归为同一阵营(朋友),只需判断他们的朋友域是否在同一个集合:
bool is_friend(int x, int y) {
return find(x) == find(y);
}题目链接:https://www.luogu.com.cn/problem/P1892

有 n 个人,关系分为 “朋友(F)” 和 “敌人(E)”,规则是 “朋友的朋友是朋友,敌人的敌人是朋友”。两个人在同一团伙当且仅当是朋友,求最多有多少个团伙。
6
4
E 1 4
F 3 5
F 4 6
E 1 23#include <iostream>
using namespace std;
const int N = 1010;
int fa[N * 2];
int n, m;
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void un(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
fa[fy] = fx;
}
}
int main() {
cin >> n >> m;
init(); // 初始化扩展域
while (m--) {
char op;
int x, y;
cin >> op >> x >> y;
if (op == 'F') {
un(x, y);
un(x + n, y + n);
} else {
un(x, y + n);
un(x + n, y);
}
}
// 统计独立的朋友域(根节点是自己的朋友域)
int cnt = 0;
for (int i = 1; i <= n; i++) {
if (find(i) == i) { // 朋友域i的根是自己,说明是一个独立团伙
cnt++;
}
}
cout << cnt << endl;
return 0;
}题目链接:https://www.luogu.com.cn/problem/P2024

动物王国有 A、B、C 三类动物,食物链是 A 吃 B、B 吃 C、C 吃 A。现在有 n 个动物,k 句话描述关系(1 X Y 表示 X 和 Y 是同类,2 X Y 表示 X 吃 Y),判断有多少句假话(假话条件:与之前冲突、X/Y 超范围、X 吃 X)。
100 7
1 101 1 // X超范围,假话
2 1 2 // 真话
2 2 3 // 真话
2 3 3 // X吃X,假话
1 1 3 // 与1吃2、2吃3冲突,假话
2 3 1 // 真话(3是C,1是A,C吃A)
1 5 5 // 真话3这道题有三种关系(同类、捕食、被捕食),需要扩展 3 个域:
合并规则:
假话判断:
#include <iostream>
using namespace std;
const int N = 5e4 + 10; // 最多5e4个动物
int fa[N * 3]; // 3个域:x(同类)、x+N(捕食)、x+2N(被捕食)
int n, k;
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void un(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
fa[fy] = fx;
}
}
int main() {
cin >> n >> k;
// 初始化3个域
for (int i = 1; i <= 3 * n; i++) {
fa[i] = i;
}
int lie = 0; // 假话数量
while (k--) {
int op, x, y;
cin >> op >> x >> y;
// 假话条件1:X或Y超范围
if (x > n || y > n) {
lie++;
continue;
}
// 假话条件2:X吃X(op=2且x=y)
if (op == 2 && x == y) {
lie++;
continue;
}
if (op == 1) { // X和Y是同类
// 冲突判断:X的捕食域包含Y,或X的被捕食域包含Y
if (find(x) == find(y + n) || find(x) == find(y + 2 * n)) {
lie++;
} else {
// 合并同类、捕食、被捕食域
un(x, y);
un(x + n, y + n);
un(x + 2 * n, y + 2 * n);
}
} else { // op=2,X吃Y
// 冲突判断:X和Y是同类,或X的被捕食域包含Y(Y吃X)
if (find(x) == find(y) || find(x) == find(y + n)) {
lie++;
} else {
// 合并:X捕食Y同类,X同类是Y被捕食,X被捕食是Y捕食
un(x + n, y);
un(x, y + 2 * n);
un(x + 2 * n, y + n);
}
}
}
cout << lie << endl;
return 0;
}再看一个场景:有 n 个节点,每个节点之间有 “距离”(比如 A 到 B 的距离是 2,B 到 C 的距离是 3,那么 A 到 C 的距离是 5)。现在要查询两个节点之间的距离,或合并两个有距离约束的节点集合。
普通并查集只能判断节点是否同属一个集合,却无法维护 “距离” 这种量化关系。这时候,带权并查集就登场了 —— 它的核心思路是:在普通并查集的基础上,给每个节点增加一个 “权值”(比如到父节点的距离),通过维护这个权值,间接计算节点间的量化关系。
带权并查集的关键在于两个点:
d[x]表示节点 x 到其父节点 fa [x] 的 “关系值”(比如距离、差值、种类差等);d[x],让d[x]最终表示 x 到根节点的关系值(而不是到原父节点的关系值)。 以 “距离” 为例,假设原结构是x → y → root,d[x]是 x 到 y 的距离,d[y]是 y 到 root 的距离。路径压缩后,x 直接指向 root,此时d[x]需要更新为 x 到 root 的总距离(d [x] + d [y])。
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int fa[N]; // 父节点数组
int d[N]; // 权值数组:d[x]表示x到fa[x]的距离
int n; // 节点总数每个节点的父节点是自己,到自己的距离是 0:
void init() {
for (int i = 1; i <= n; i++) {
fa[i] = i;
d[i] = 0; // 自己到自己的距离为0
}
}这是带权并查集的核心!在递归找到根节点后,要同步更新当前节点的权值:
int find(int x) {
if (fa[x] == x) { // 找到根节点,直接返回
return x;
}
// 先递归找到根节点(必须先找根,再更新父节点和权值)
int root = find(fa[x]);
// 更新权值:x到根的距离 = x到原父节点的距离 + 原父节点到根的距离
d[x] += d[fa[x]];
// 路径压缩:x的父节点直接指向根
fa[x] = root;
return root;
}假设要合并 x 和 y,且已知 x 到 y 的距离是 w(即 x → y 的距离为 w)。合并步骤:
// 合并x和y所在的集合,且x到y的距离为w
void un(int x, int y, int w) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
fa[fx] = fy; // 让fx的父节点指向fy
// 推导:x到y的距离 = x到fx的距离 + fx到fy的距离 - y到fy的距离
// 即 w = d[x] + d[fx] - d[y] → d[fx] = d[y] + w - d[x]
d[fx] = d[y] + w - d[x];
}
}如果 x 和 y 在同一个集合,x 到 y 的距离 = d [x] - d [y](因为 d [x] 是 x 到根的距离,d [y] 是 y 到根的距离,两者差值就是 x 到 y 的距离);如果不在同一个集合,距离未知。
const int INF = 0x3f3f3f3f;
// 查询x到y的距离,不在同一集合返回INF
int query(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
return INF;
}
return d[x] - d[y];
} 还是 “食物链” 问题,但这次我们用带权并查集解决。核心思路是:用权值d[x]表示 x 到根节点的 “种类差”,通过d[x] % 3的值判断 x 与根节点的关系:
d[x] % 3 == 0:x 与根节点是同类;d[x] % 3 == 1:x 捕食根节点;d[x] % 3 == 2:根节点捕食 x(即 x 被捕食)。d[x] = 0(与自己同类);(d[Y] - d[X]) % 3是否为 0,是则合法,否则为假话;若未合并,执行合并(权值 w=0,因为 X 到 Y 的距离为 0,即同类);(d[Y] - d[X]) % 3是否为 1(X 捕食 Y,即 Y 的种类差比 X 大 1),是则合法,否则为假话;若未合并,执行合并(权值 w=1,因为 X 到 Y 的距离为 1,即 X 吃 Y);#include <iostream>
using namespace std;
const int N = 5e4 + 10;
int fa[N];
int d[N]; // d[x]表示x到根节点的种类差,d[x]%3表示关系
int n, k;
int find(int x) {
if (fa[x] == x) {
return x;
}
int root = find(fa[x]);
d[x] += d[fa[x]]; // 更新x到根的种类差
fa[x] = root;
return root;
}
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++) {
fa[i] = i;
d[i] = 0;
}
int lie = 0;
while (k--) {
int op, x, y;
cin >> op >> x >> y;
if (x > n || y > n) { // 超范围
lie++;
continue;
}
if (op == 2 && x == y) { // X吃X
lie++;
continue;
}
int fx = find(x);
int fy = find(y);
if (fx == fy) { // 已在同一集合,判断是否冲突
if (op == 1) {
// 同类:(d[y]-d[x])%3应等于0
if ((d[y] - d[x]) % 3 != 0) {
lie++;
}
} else { // op==2,X吃Y:(d[y]-d[x])%3应等于1
if ((d[y] - d[x]) % 3 != 1) {
lie++;
}
}
} else { // 未合并,执行合并
if (op == 1) {
// X和Y是同类,x到y的距离为0 → w=0
un(x, y, 0);
} else {
// X吃Y,x到y的距离为1 → w=1
un(x, y, 1);
}
}
}
cout << lie << endl;
return 0;
}d[x]的更新:路径压缩时,d[x]累加父节点到根的权值,最终表示 x 到根的种类差;(d[y]-d[x])%3的值判断 X 和 Y 的关系,符合则合法,否则为假话;题目链接:https://www.luogu.com.cn/problem/P1196

有 30000 列战舰,初始时第 i 号战舰在第 i 列。有两种指令:
4
M 2 3 // 2的队列接在3后面:3 → 2
C 1 2 // 1和2不在同一列,输出-1
M 2 4 // 2的队列(3→2)接在4后面:4 → 3 → 2
C 4 2 // 4和2之间有1艘战舰(3),输出1-1
1这道题需要维护两个量化信息:
因此,我们需要在带权并查集中增加一个cnt[]数组,记录每个根节点对应的队列大小:
d[x]:x 到根节点的距离(即 x 前面有多少艘战舰);cnt[x]:根节点 x 对应的队列大小(非根节点的 cnt 值无意义)。合并规则(M i j):
d[fi] = cnt[fj](fi 队列的头部到 fj 队列的尾部的距离,就是 fj 队列的大小);cnt[fj] += cnt[fi](fj 队列的大小增加 fi 队列的大小)。查询规则(C i j):
abs(d[i] - d[j]) - 1(距离差减 1 就是中间的战舰数)。#include <iostream>
#include <cmath>
using namespace std;
const int N = 3e4 + 10;
int fa[N]; // 父节点
int d[N]; // d[x]:x到根节点的距离(前面有多少艘战舰)
int cnt[N]; // cnt[x]:根节点x的队列大小
int n = 3e4; // 固定30000列
int find(int x) {
if (fa[x] == x) {
return x;
}
int root = find(fa[x]);
d[x] += d[fa[x]]; // 更新x到根的距离
fa[x] = root;
return root;
}
// 合并i和j的队列:i的队列接在j的队列后面
void un(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
fa[fx] = fy;
d[fx] = cnt[fy]; // fx队列头部到fy队列尾部的距离是fy的队列大小
cnt[fy] += cnt[fx]; // 更新fy的队列大小
}
}
// 查询x和y之间的战舰数
int query(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
return -1;
}
return abs(d[x] - d[y]) - 1; // 距离差减1是中间的战舰数
}
int main() {
// 初始化:每个战舰自己是一个队列
for (int i = 1; i <= n; i++) {
fa[i] = i;
d[i] = 0;
cnt[i] = 1; // 初始队列大小为1
}
int T;
cin >> T;
while (T--) {
char op;
int x, y;
cin >> op >> x >> y;
if (op == 'M') {
un(x, y);
} else {
cout << query(x, y) << endl;
}
}
return 0;
}很多人会问:扩展域和带权并查集都能处理复杂关系,该怎么选?其实两者各有侧重,核心区别在于关系的类型:
特性 | 扩展域并查集 | 带权并查集 |
|---|---|---|
核心思路 | 给元素 “分身”,用多个域表达不同状态 | 给边 “加权”,用权值表达量化关系 |
适用场景 | 关系是 “离散的多状态”(如朋友 / 敌人、同类 / 捕食 / 被捕食) | 关系是 “连续的量化值”(如距离、差值、种类差) |
实现复杂度 | 需设计多个域的映射规则,代码量稍大 | 需维护权值更新,数学推导稍多 |
典型问题 | 团伙、食物链(多状态) | 银河英雄传说(距离)、食物链(种类差) |
简单来说:
并查集的拓展是数据结构中的 “进阶技巧”,但只要掌握了 “域的设计” 和 “权值的维护” 这两个核心,再复杂的问题也能迎刃而解。希望这篇文章能帮你打开并查集的 “进阶大门”,下次遇到复杂关系问题,能自信地说:“用扩展域 / 带权并查集,搞定!”