目录
[一、扩展域并查集:给元素 "分身",解决多关系难题](#一、扩展域并查集:给元素 “分身”,解决多关系难题)
[1.1 普通并查集的 "痛点":只能处理一种关系](#1.1 普通并查集的 “痛点”:只能处理一种关系)
[1.2 扩展域并查集的核心思想:"一人多域"+"关系映射"](#1.2 扩展域并查集的核心思想:“一人多域”+“关系映射”)
[1.3 扩展域并查集的实现:以 "朋友 - 敌人" 为例](#1.3 扩展域并查集的实现:以 “朋友 - 敌人” 为例)
[1.3.1 初始化:给每个元素的 "分身" 分配初始集合](#1.3.1 初始化:给每个元素的 “分身” 分配初始集合)
[1.3.2 处理关系:按规则合并对应域](#1.3.2 处理关系:按规则合并对应域)
[1.3.3 判断阵营:检查朋友域是否同属一个集合](#1.3.3 判断阵营:检查朋友域是否同属一个集合)
[1.4 扩展域并查集实战 1:洛谷 P1892 团伙(朋友 - 敌人问题)](#1.4 扩展域并查集实战 1:洛谷 P1892 团伙(朋友 - 敌人问题))
[1.5 扩展域并查集实战 2:洛谷 P2024 食物链(三种关系)](#1.5 扩展域并查集实战 2:洛谷 P2024 食物链(三种关系))
[二、带权并查集:给边 "加权",处理量化关系](#二、带权并查集:给边 “加权”,处理量化关系)
[2.1 普通并查集的另一 "痛点":无法表达量化信息](#2.1 普通并查集的另一 “痛点”:无法表达量化信息)
[2.2 带权并查集的核心思想:"权值维护"+"路径压缩时更新权值"](#2.2 带权并查集的核心思想:“权值维护”+“路径压缩时更新权值”)
[2.3 带权并查集的基础实现:以 "距离维护" 为例](#2.3 带权并查集的基础实现:以 “距离维护” 为例)
[2.3.1 数据结构定义](#2.3.1 数据结构定义)
[2.3.2 初始化](#2.3.2 初始化)
[2.3.3 find 操作:路径压缩 + 权值更新](#2.3.3 find 操作:路径压缩 + 权值更新)
[2.3.4 union 操作:合并两个集合 + 维护权值](#2.3.4 union 操作:合并两个集合 + 维护权值)
[2.3.5 query 操作:查询两个节点的距离](#2.3.5 query 操作:查询两个节点的距离)
[2.4 带权并查集实战 1:洛谷 P2024 食物链(用权值表示种类关系)](#2.4 带权并查集实战 1:洛谷 P2024 食物链(用权值表示种类关系))
[2.5 带权并查集实战 2:洛谷 P1196 银河英雄传说(维护距离和队列大小)](#2.5 带权并查集实战 2:洛谷 P1196 银河英雄传说(维护距离和队列大小))
[三、扩展域 VS 带权并查集:如何选择?](#三、扩展域 VS 带权并查集:如何选择?)
前言
在掌握了普通并查集后,你可能会发现:面对 "朋友的敌人是敌人""A 吃 B、B 吃 C、C 吃 A" 这类包含多种关系 或量化约束的问题时,普通并查集就像 "巧妇难为无米之炊"------ 它只能维护 "是否同属一个集合",却无法表达元素间更复杂的关联。
别担心!今天这篇文章,我们就来解锁并查集的两大进阶形态:扩展域并查集 和带权并查集。前者能搞定 "多关系共存" 的场景,后者可处理 "节点间有量化信息" 的问题。从原理到实战,从代码到思路,保证让你学完就能上手解决复杂问题!下面就让我们正式开始吧!
一、扩展域并查集:给元素 "分身",解决多关系难题
1.1 普通并查集的 "痛点":只能处理一种关系
先看一个经典场景:有 n 个人,他们之间只有 "朋友" 或 "敌人" 两种关系,且满足 "朋友的朋友是朋友,敌人的敌人是朋友"。现在要判断两个人是否能归为同一阵营(朋友)。
如果用普通并查集:
- 朋友关系可以合并,但敌人关系怎么处理?总不能把敌人也合并吧?
- 比如 A 和 B 是敌人,B 和 C 是敌人,按规则 A 和 C 是朋友,但普通并查集无法传递这种 "敌人的敌人" 的关系。
这时候,扩展域并查集 就派上用场了 ------ 它的核心思路是:给每个元素 "拆分" 成多个 "域"(比如朋友域、敌人域),每个域代表一种状态,通过维护不同域之间的关系,间接表达元素间的复杂关联。
1.2 扩展域并查集的核心思想:"一人多域"+"关系映射"
我们以 "朋友 - 敌人" 问题为例,具体解释扩展域的设计:
- 给每个元素 x 拆分成两个域:
- 朋友域(x):代表与 x 是朋友的集合;
- 敌人域(x+n):代表与 x 是敌人的集合(n 是总人数,用 x+n 避免与朋友域下标冲突)。
- 然后通过以下规则合并域,间接表达关系:
- 如果 A 和 B 是朋友:A 的朋友域与 B 的朋友域合并(朋友的朋友是朋友),A 的敌人域与 B 的敌人域合并(朋友的敌人是共同敌人);
- 如果 A 和 B 是敌人:A 的朋友域与 B 的敌人域合并(A 的朋友是 B 的敌人),A 的敌人域与 B 的朋友域合并(A 的敌人是 B 的朋友)。
通过这种 "域的合并",我们就能用并查集的方式,间接维护 "朋友" 和 "敌人" 两种关系了。
1.3 扩展域并查集的实现:以 "朋友 - 敌人" 为例
1.3.1 初始化:给每个元素的 "分身" 分配初始集合
每个元素 x 的朋友域 x 和敌人域 x+n,初始时都是独立集合(父节点指向自己):
cpp
#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的根节点(方向不影响,统一即可)
}
}
1.3.2 处理关系:按规则合并对应域
- 朋友关系(A 和 B 是朋友):合并 A 的朋友域(A)和 B 的朋友域(B),合并 A 的敌人域(A+n)和 B 的敌人域(B+n);
- 敌人关系(A 和 B 是敌人):合并 A 的朋友域(A)和 B 的敌人域(B+n),合并 A 的敌人域(A+n)和 B 的朋友域(B)。
cpp
// 处理关系: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);
}
}
1.3.3 判断阵营:检查朋友域是否同属一个集合
两个人是否能归为同一阵营(朋友),只需判断他们的朋友域是否在同一个集合:
cpp
bool is_friend(int x, int y) {
return find(x) == find(y);
}
1.4 扩展域并查集实战 1:洛谷 P1892 团伙(朋友 - 敌人问题)
题目链接:https://www.luogu.com.cn/problem/P1892

题目描述
有 n 个人,关系分为 "朋友(F)" 和 "敌人(E)",规则是 "朋友的朋友是朋友,敌人的敌人是朋友"。两个人在同一团伙当且仅当是朋友,求最多有多少个团伙。
输入示例
6
4
E 1 4
F 3 5
F 4 6
E 1 2
输出示例
3
解题思路
- 用扩展域并查集维护朋友和敌人关系;
- 最终统计有多少个独立的 "朋友域"(根节点是自己的朋友域),每个独立的朋友域对应一个团伙。
完整代码
cpp
#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;
}
代码解释
- 初始化时,每个朋友域(1~6)和敌人域(7~12)都是独立集合;
- 处理关系:
- E 1 4:合并 1 和 4+6=10,合并 1+6=7 和 4;
- F 3 5:合并 3 和 5,合并 3+6=9 和 5+6=11;
- F 4 6:合并 4 和 6,合并 4+6=10 和 6+6=12;
- E 1 2:合并 1 和 2+6=8,合并 1+6=7 和 2;
- 最终朋友域的根节点:1(1、2、4、6 的根)、3(3、5 的根)、7(7 的根?不,7 是 1 的敌人域,朋友域只看 1~6)------ 实际统计得 3 个独立团伙,与输出一致。
1.5 扩展域并查集实战 2:洛谷 P2024 食物链(三种关系)
题目链接: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 个域:
- 同类域(x):代表与 x 是同类的集合;
- 捕食域(x+n):代表 x 捕食的动物集合;
- 被捕食域(x+2n):代表捕食 x 的动物集合。
合并规则:
- 如果 X 和 Y 是同类 (1 X Y):
- X 的同类域 ↔ Y 的同类域;
- X 的捕食域 ↔ Y 的捕食域;
- X 的被捕食域 ↔ Y 的被捕食域;
- 如果 X 吃 Y (2 X Y):
- X 的捕食域 ↔ Y 的同类域(X 捕食的就是 Y 这类);
- X 的同类域 ↔ Y 的被捕食域(Y 被捕食的就是 X 这类);
- X 的被捕食域 ↔ Y 的捕食域(捕食 X 的,是 Y 捕食的对象,符合 A 吃 B、B 吃 C、C 吃 A)。
假话判断:
- 先检查 X/Y 是否超范围或 X=Y(X 吃 X),是则为假话;
- 再检查当前关系是否与已有关系冲突(比如 X 和 Y 是同类,但 X 的捕食域已包含 Y),是则为假话。
完整代码
cpp
#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;
}
代码解释
- 初始化时,每个动物的 3 个域都是独立集合;
- 处理每句话时,先检查明显的假话条件(超范围、X 吃 X);
- 再通过扩展域的合并关系,判断是否与已有关系冲突;
- 最终统计假话数量,与示例输出一致。
二、带权并查集:给边 "加权",处理量化关系
2.1 普通并查集的另一 "痛点":无法表达量化信息
再看一个场景:有 n 个节点,每个节点之间有 "距离"(比如 A 到 B 的距离是 2,B 到 C 的距离是 3,那么 A 到 C 的距离是 5)。现在要查询两个节点之间的距离,或合并两个有距离约束的节点集合。
普通并查集只能判断节点是否同属一个集合,却无法维护 "距离" 这种量化关系 。这时候,带权并查集 就登场了 ------ 它的核心思路是:在普通并查集的基础上,给每个节点增加一个 "权值"(比如到父节点的距离),通过维护这个权值,间接计算节点间的量化关系。
2.2 带权并查集的核心思想:"权值维护"+"路径压缩时更新权值"
带权并查集的关键在于两个点:
- 权值的定义 :通常用**d[x]**表示节点 x 到其父节点 fa [x] 的 "关系值"(比如距离、差值、种类差等);
- 路径压缩时的权值更新 :在执行find (x) 时,不仅要把 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])。
2.3 带权并查集的基础实现:以 "距离维护" 为例
2.3.1 数据结构定义
cpp
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int fa[N]; // 父节点数组
int d[N]; // 权值数组:d[x]表示x到fa[x]的距离
int n; // 节点总数
2.3.2 初始化
每个节点的父节点是自己,到自己的距离是 0:
cpp
void init() {
for (int i = 1; i <= n; i++) {
fa[i] = i;
d[i] = 0; // 自己到自己的距离为0
}
}
2.3.3 find 操作:路径压缩 + 权值更新
这是带权并查集的核心!在递归找到根节点后,要同步更新当前节点的权值:
cpp
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;
}
2.3.4 union 操作:合并两个集合 + 维护权值
假设要合并 x 和 y,且已知 x 到 y 的距离是 w(即 x → y 的距离为 w)。合并步骤:
- 找到 x 和 y 的根节点 fx 和 fy;
- 如果 fx != fy,将 fx 的父节点指向 fy,并更新 fx 的权值 d [fx],让 x 到 y 的距离满足 w = d [x] + d [fx] - d [y](推导见下文)。
cpp
// 合并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];
}
}
2.3.5 query 操作:查询两个节点的距离
如果 x 和 y 在同一个集合,x 到 y 的距离 = d [x] - d [y](因为 d [x] 是 x 到根的距离,d [y] 是 y 到根的距离,两者差值就是 x 到 y 的距离);如果不在同一个集合,距离未知。
cpp
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];
}
2.4 带权并查集实战 1:洛谷 P2024 食物链(用权值表示种类关系)
题目回顾
还是 "食物链" 问题,但这次我们用带权并查集解决。核心思路是:用权值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(与自己同类);
- 处理关系:
- 如果是同类(1 X Y):检查**(d[Y] - d[X]) % 3**是否为 0,是则合法,否则为假话;若未合并,执行合并(权值 w=0,因为 X 到 Y 的距离为 0,即同类);
- 如果是 X 吃 Y(2 X Y):检查**(d[Y] - d[X]) % 3**是否为 1(X 捕食 Y,即 Y 的种类差比 X 大 1),是则合法,否则为假话;若未合并,执行合并(权值 w=1,因为 X 到 Y 的距离为 1,即 X 吃 Y);
- 统计假话数量。
完整代码
cpp
#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 的关系,符合则合法,否则为假话;
- 合并时,根据关系设置权值 w(同类 w=0,捕食 w=1),确保合并后权值关系正确。
2.5 带权并查集实战 2:洛谷 P1196 银河英雄传说(维护距离和队列大小)
题目链接:https://www.luogu.com.cn/problem/P1196

题目描述
有 30000 列战舰,初始时第 i 号战舰在第 i 列。有两种指令:
- M i j:将 i 号战舰所在的队列,作为整体接在 j 号战舰所在队列的尾部;
- C i j:查询 i 和 j 是否在同一列,若是则输出它们之间的战舰数(即 | 距离 | - 1),否则输出 - 1。
输入示例
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):
- 找到 i 的根 fi 和 j 的根 fj;
- 将 fi 的父节点指向 fj;
- 更新d[fi] = cnt[fj](fi 队列的头部到 fj 队列的尾部的距离,就是 fj 队列的大小);
- 更新cnt[fj] += cnt[fi](fj 队列的大小增加 fi 队列的大小)。
查询规则(C i j):
- 若 i 和 j 不在同一集合,输出 - 1;
- 若在同一集合,输出abs(d[i] - d[j]) - 1(距离差减 1 就是中间的战舰数)。
完整代码
cpp
#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;
}
代码解释
- 初始化时,每个战舰的队列大小为 1,距离为 0;
- 合并 M 2 3:
- find(2)=2,find(3)=3;
- fa [2] = 3,d [2] = cnt [3] = 1(2 到 3 的距离是 1,即 2 在 3 后面);
- cnt [3] = 1 + 1 = 2(队列 3→2 的大小为 2);
- 合并 M 2 4:
- find (2)=3(因为 2 的父节点是 3),find (4)=4;
- fa [3] = 4,d [3] = cnt [4] = 1(3 到 4 的距离是 1,即 3 在 4 后面);
- cnt [4] = 1 + 2 = 3(队列 4→3→2 的大小为 3);
- 查询 C 4 2:
- find (4)=4,find (2)=4(同一集合);
- d [4] = 0,d [2] = d [2](1) + d [3](1) = 2(路径压缩后);
- 距离差是 2-0=2,中间战舰数是 2-1=1,与输出一致。
三、扩展域 VS 带权并查集:如何选择?
很多人会问:扩展域和带权并查集都能处理复杂关系,该怎么选?其实两者各有侧重,核心区别在于关系的类型:
| 特性 | 扩展域并查集 | 带权并查集 |
|---|---|---|
| 核心思路 | 给元素 "分身",用多个域表达不同状态 | 给边 "加权",用权值表达量化关系 |
| 适用场景 | 关系是 "离散的多状态"(如朋友 / 敌人、同类 / 捕食 / 被捕食) | 关系是 "连续的量化值"(如距离、差值、种类差) |
| 实现复杂度 | 需设计多个域的映射规则,代码量稍大 | 需维护权值更新,数学推导稍多 |
| 典型问题 | 团伙、食物链(多状态) | 银河英雄传说(距离)、食物链(种类差) |
简单来说:
- 如果问题中的关系是 "非此即彼" 的离散状态 (比如朋友 / 敌人、喜欢 / 讨厌),优先用扩展域并查集;
- 如果问题中的关系需要**"量化"** (比如距离、差值、比例),优先用带权并查集。
总结
并查集的拓展是数据结构中的 "进阶技巧",但只要掌握了 "域的设计" 和 "权值的维护" 这两个核心,再复杂的问题也能迎刃而解。希望这篇文章能帮你打开并查集的 "进阶大门",下次遇到复杂关系问题,能自信地说:"用扩展域 / 带权并查集,搞定!"