一、 普通并查集
1.1 数据结构与基础概念
- 概念解释 :
并查集 (Union-Find Set):一种用于管理元素所属集合的数据结构,能够高效地处理"合并两个集合"以及"查询两个元素是否同属一个集合"的操作。 - 笔记:
- 标签:预处理、基础算法。
- 维护"集合"的三件套:查询、合并、判同。
- 数据结构 :使用数组
fa[i]作为结点的父指针,根结点指向自己。
1.2 核心操作实现
- 初始化:
cpp
void init(int n) { for(int i=1;i<=n;i++) fa[i]=i; }
功能与参数说明:初始化阶段,让每个元素自成一个独立的集合。参数 n 为元素总数,遍历使得 fa[i] = i。
- 查找:
cpp
int find(int x) { // 路径压缩
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
功能与参数说明:查找元素 x 所在集合的根节点。
- 概念解释(路径压缩) :在递归查找根节点的回溯过程中,将查找路径上的所有节点直接连接到根节点上。这能使后续查询的时间复杂度逼近 O(1)O(1)O(1)。
- 合并:
cpp
void un(int x,int y) { // 按秩/大小合并可写一行
int fx=find(x), fy=find(y);
if(fx!=fy) fa[fx]=fy; // 把 fx 挂到 fy 下
}
功能与参数说明:将 x 和 y 所在的集合合并。分别找到两者根节点,若不同,则将其中一棵树的根挂载到另一棵树的根下。
- 概念解释(按秩合并):一种优化策略,把较矮/较小的树挂到较高/较大的树下,防止树退化成链表。结合路径压缩通常可简化实现。
- 判同:
cpp
bool same(int x,int y) { return find(x)==find(y); }
功能与参数说明:判断 x 和 y 是否属于同一个集合。通过比对两者的根节点是否一致来返回布尔值。
1.3 统计集合个数
- 笔记:通过判定有多少个元素的父节点依然指向自己(即有多少个根节点),来统计连通块/集合的总数。
cpp
int cnt=0;
for(int i=1;i<=n;i++) if(fa[i]==i) cnt++;
二、 扩展域查并集(关系 > 2种)
2.1 多关系处理机制
- 概念解释 :
扩展域并查集:当元素间的关系不仅限于"同属一集"(如存在"敌对"关系)时,通过将原数组的容量扩大数倍,让每个元素的多个"域"来代表其不同的状态或身份。 - 笔记:
- 核心思想:把"每个元素"拆成多个域,每个域代表一种状态/关系,用偏移量存储。
- 经典场景:朋友 & 敌人(2倍域)
- 域 1∼n1 \sim n1∼n (或 0∼n−10 \sim n-10∼n−1):朋友身份。
- 域 n+1∼2nn+1 \sim 2nn+1∼2n (或 n∼2n−1n \sim 2n-1n∼2n−1):敌人身份。
2.2 核心操作实现
- 初始化:
cpp
const int N = 1e6 + 10;
int fa[N * 2]; // 0~n-1 为原域,n~2n-1 为"敌人域"
void init(int n) {
for (int i = 1; i <= 2 * n; i++) fa[i] = i;
}
功能与参数说明:为 2n2n2n 个元素(包含本体域和敌人域)进行独立初始化,自成一派。
- 查找:
cpp
int find(int x) { // 与普通版完全相同
if (fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
功能与参数说明:扩展域中的查找逻辑与普通并查集完全一致,仍使用路径压缩。
- 合并:
cpp
void un(int x, int y) { // 合并根,普通写法
int fx = find(x), fy = find(y);
if (fx != fy) fa[fx] = fy;
}
void mergeFriend(int x, int y, int n) { // 朋友:双向绑定
un(x, y);
un(x + n, y + n);
}
void mergeEnemy(int x, int y, int n) { // 敌人:交叉绑定
un(x, y + n);
un(y, x + n);
}
功能与参数说明:
- 朋友关系 :合并 xxx 与 yyy 的原域,同时合并 xxx 与 yyy 的敌人域。
- 敌人关系 :说"xxx 和 y+ny+ny+n 合并"意思是:xxx 和 yyy 的敌人是朋友。执行交叉绑定。
- 判同 (状态查询):
cpp
bool isFriend(int x, int y) { return find(x) == find(y); }
bool isEnemy (int x, int y) { return find(x) == find(y + n); }
bool conflict(int x, int y, int n) { // 判断是否"既友又敌"
return isFriend(x, y) && isEnemy(x, y, n);
}
功能与参数说明:分别检测本体域是否连通、交叉域是否连通,以及是否同时触发导致逻辑冲突。
三、 带权并查集
3.1 核心思想与数据结构
- 概念解释 :
带权并查集:在边上记录权值的并查集,通常用来维护节点间的一维相对关系(如相对距离、高度差、分数差)。 - 笔记:
- 核心思想 :给每条父边记录"权值
d[i]",表示 iii 到父节点的某种累积量。路径压缩后d[i]直接是 i→根i \rightarrow \text{根}i→根 的累积量。 - 数据结构:引入额外数组维护权值。
cpp
int fa[N], d[N]; // d[i]:i 相对于父节点的权值
3.2 核心操作实现
- 初始化:
cpp
void init(int n) {
for(int i=1;i<=n;i++) { fa[i]=i; d[i]=0; }
}
功能与参数说明:初始状态下,父节点是自己,距离自身权值偏差为 000。
- 查找:
cpp
int find(int x) {
if(fa[x]==x) return x;
int root=find(fa[x]); // 先让祖先挂到根
d[x] += d[fa[x]]; // 累加"祖父→根"部分
return fa[x]=root; // 挂根,返回根
}
功能与参数说明:路径压缩并累计权值。在递归回溯阶段,将当前节点原本到父节点的权值,加上父节点到根节点的权值,完成权值的路径压缩。
- 合并:
cpp
void un(int x,int y,int w) { // w: x→y 的给定边权
int fx=find(x), fy=find(y);
if(fx==fy) return; // 已同集合,视情况判断矛盾
fa[fx]=fy;
d[fx] = d[y] - d[x] - w; // 保证公式成立
}
功能与参数说明:以 "xxx 到 yyy 的边权为 www" 为例。目标让 fxfxfx 挂到 fyfyfy,必须满足向量等式 dx+dfx+w=dydx + dfx + w = dydx+dfx+w=dy,故反解出 dfxdfxdfx 的赋值公式。
- 判同 (查询两点间量值):
cpp
int query(int x,int y) {
int fx=find(x), fy=find(y);
if(fx!=fy) return INF; // 不同集合,量值未知
return d[y] - d[x]; // 根据合并公式推导
}
功能与参数说明:判定两点是否同源,若同源,通过两者的绝对偏移量相减得出相对量值。
四、 选用策略
| 场景 | 用法 |
|---|---|
| 纯连通、集合计数 | 普通 UFS + 路径压缩 + 按秩合并 |
| 朋友-敌人 / 食物链 | 扩展域(2 倍或 3 倍) |
| 差分、距离、相对高度 | 带权 UFS(维护 d[]) |
五、 实战:P1525 关押罪犯
5.1 问题建模与策略
- 概念解释 :
贪心算法 (Greedy Algorithm):在对问题求解时,总是做出在当前状态下最优的选择。 - 笔记:
- 题目核心 :NNN 名罪犯分 222 座监狱,同监狱有仇则发生摩擦产生"怨气值 ccc"。求最大冲突事件的怨气值最小化。
- 数据规模 :N≤20000,M≤100000N \le 20000, M \le 100000N≤20000,M≤100000 →\rightarrow→ 时间复杂度需控制在 O(MlogM)O(M \log M)O(MlogM)。
- 问题建模 :将罪犯分成两个监狱,使同一监狱内的最大仇恨值最小 ⇔\Leftrightarrow⇔ 优先把仇恨值大的罪犯分到不同监狱。
- 贪心策略:
- 从仇恨值最大的矛盾开始处理。
- 尽量把仇恨值大的罪犯分到不同监狱。
- 当发现两个罪犯无法分到不同监狱时,当前的仇恨值就是答案。
-
逻辑梳理 (扩展域并查集):
-
1∼n1 \sim n1∼n 域:罪犯本身集合;n+1∼2nn+1 \sim 2nn+1∼2n 域:罪犯的敌人集合。
-
un(a, b + n):aaa 与 bbb 的敌人是朋友。 -
un(b, a + n):bbb 与 aaa 的敌人是朋友。 -
冲突检测 :
if(find(a) == find(b)),说明之前的强力分配已经迫使他们在同一个监狱(已经是朋友),矛盾爆发。
5.2 代码实现
- 笔记:结构体、排序支持与完整主逻辑。
cpp
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 4e4 + 10, M = 1e5 + 10;
int n, m;
struct node
{
int a, b, c; // a,b:两个罪犯编号, c:仇恨值
}e[M];
int fa[N]; // 并查集数组
// 查找
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
// 合并
void un(int x, int y)
{
fa[find(x)] = find(y);
}
// 贪心排序支持
bool cmp(node& x, node& y)
{
return x.c > y.c;
}
int main()
{
cin >> n >> m; // n:罪犯数量, m:矛盾数量
for(int i = 1; i <= m; i++)
cin >> e[i].a >> e[i].b >> e[i].c;
// 初始化:扩展为2倍
for(int i = 1; i <= n + n; i++)
fa[i] = i;
// 贪心:按仇恨值从大到小排序
sort(e + 1, e + 1 + m, cmp);
for(int i = 1; i <= m; i++)
{
int a = e[i].a, b = e[i].b, c = e[i].c;
// 建立敌人关系:交叉绑定
un(a, b + n);
un(b, a + n);
// 判同检测:如果a和b已经是朋友,说明无法分到不同监狱
if(find(a) == find(b))
{
cout << c << endl; // 输出当前最大的不可避免的冲突
return 0;
}
}
cout << 0 << endl;
return 0;
}