并查集及带权与扩展域并查集:原理与应用
并查集的基本概念
并查集(Disjoint Set Union,DSU)是一种用于管理元素分组的数据结构,支持两种核心操作:
- 查找(Find):确定元素所属的集合(通常通过根节点表示)。
- 合并(Union):将两个集合合并为一个。
其底层逻辑基于树结构,通过路径压缩和按秩合并优化效率,使得操作接近常数时间复杂度。
底层实现逻辑
数据结构
使用数组parent[]表示每个节点的父节点,初始化时每个节点自成一集合(parent[i] = i)。
int parent[MAX_SIZE]; // 父节点数组
int rank[MAX_SIZE]; // 秩(树高度)
void init(int n) {
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 0;
}
}
查找(Find)
递归或迭代找到根节点,同时应用路径压缩优化(直接指向根节点,降低树高度)。
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
合并(Union)
按秩合并:将秩较小的树合并到秩较大的树下,避免树过高。
void unionSets(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
应用场景
1. 连通性问题
- 动态连通性:判断图中两个节点是否连通(如Kruskal算法中的边处理)。
- 社交网络:快速合并好友关系分组。
2. 最小生成树(Kruskal算法)
通过并查集高效判断边的两个顶点是否属于同一集合,避免环的形成。
3. 图像处理
像素区域合并,标记连通区域。
4. 游戏开发
网格地图中动态连接的区块管理(如迷宫生成)。
完整代码示例
#include <stdio.h>
#define MAX_SIZE 100
int parent[MAX_SIZE];
int rank[MAX_SIZE];
void init(int n) {
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 0;
}
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
void unionSets(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
if (rank[rootX] == rank[rootY]) {
rank[rootX]++;
}
}
}
}
int main() {
int n = 10;
init(n);
unionSets(1, 2);
unionSets(2, 3);
printf("Find(3): %d\n", find(3)); // 输出应为1的根
return 0;
}
通过路径压缩和按秩合并,并查集的均摊时间复杂度接近 o(1)。
扩展域并查集原理
扩展域并查集通过将元素拆分为多个逻辑部分(域)来处理复杂关系。常见场景是处理具有对立或依赖关系的元素,例如朋友与敌人、染色问题等。每个元素被拆分为多个虚拟节点,通常为原始元素的倍数关系。
逻辑核心在于通过合并不同域表达元素间的关系。例如处理敌对关系时,若元素A与B敌对,则将A的朋友域与B的敌人域合并,A的敌人域与B的朋友域合并。这种拆分使得关系传递性得以维护。
带权并查集原理
带权并查集在普通并查集基础上增加权值数组,记录节点与父节点之间的关系。权值通常表示相对关系,如距离、差值或模数。路径压缩和合并操作需同步维护权值以保证关系正确性。
核心操作包含权值的传递与合并。路径压缩时,权值需按路径叠加更新;合并集合时,需根据关系计算新权值。例如处理模运算关系时,权值表示与父节点的差值模某个数。
扩展域并查集C代码
#include <stdio.h>
#define MAXN 10000
int parent[MAXN * 2]; // 每个元素拆分为2个域
void init(int n) {
for (int i = 0; i < 2 * n; i++) {
parent[i] = i;
}
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
void merge(int x, int y) {
parent[find(x)] = find(y);
}
// 示例:处理敌对关系
void add_enemy(int a, int b, int n) {
merge(a, b + n); // a的朋友与b的敌人合并
merge(a + n, b); // a的敌人与b的朋友合并
}
int is_friend(int a, int b) {
return find(a) == find(b);
}
int is_enemy(int a, int b, int n) {
return find(a) == find(b + n);
}
带权并查集C代码
#include <stdio.h>
#define MAXN 10000
int parent[MAXN];
int weight[MAXN]; // 权值数组
void init(int n) {
for (int i = 0; i < n; i++) {
parent[i] = i;
weight[i] = 0;
}
}
int find(int x) {
if (parent[x] != x) {
int old_parent = parent[x];
parent[x] = find(parent[x]);
weight[x] += weight[old_parent]; // 路径压缩时更新权值
}
return parent[x];
}
void merge(int x, int y, int w) {
int root_x = find(x);
int root_y = find(y);
if (root_x != root_y) {
parent[root_x] = root_y;
weight[root_x] = weight[y] - weight[x] + w; // 根据关系计算新权值
}
}
// 示例:查询x与y的关系
int query(int x, int y) {
if (find(x) != find(y)) {
return -1; // 无关系
}
return weight[x] - weight[y];
}
关键逻辑对比
扩展域通过拆分节点表达多元关系,适合离散型关系处理。带权并查集通过数值维护连续关系,适合计算型问题。两者均需在合并与查询时维护关系正确性,扩展域的空间复杂度更高,带权并查集的时间复杂度更优。
实际应用中,扩展域适合明确对立关系场景(如二分图检测),带权并查集适合数值传递场景(如差分约束)。代码实现时需注意初始化范围和权值更新方向。
例题:洛谷P1525 [NOIP 2010 提高组] 关押罪犯
扩展域并查集解决关押罪犯问题
问题描述 NOIP关押罪犯问题描述为:将N个罪犯分配到两个监狱,使得同一监狱内1的罪犯之间的冲突值尽可能小。需要找到一种分配方案,使得最大冲突值最小。
扩展域并查集思路 扩展域并查集通过将每个元素拆分为多个域(通常为2个)来表示不同的关系状态。在本题中,每个罪犯可以拆分为“在A监狱”和“在B监狱”两个域。
实现步骤
定义并查集数组fa,大小为2*N,其中1..N表示在A监狱,N+1..2*N表示在B监狱。
初始化并查集,每个元素的父节点为自己:
for(int i=1; i<=2*N; i++) fa[i] = i;
处理冲突关系时,对于每对罪犯(x,y)和冲突值c:
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y) {
fa[find(x)] = find(y);
}
将冲突关系按冲突值从大到小排序,依次处理:
sort(edges.begin(), edges.end(), [](auto &a, auto &b){
return a.c > b.c;
});
for(auto &e : edges) {
int x = e.x, y = e.y, c = e.c;
if(find(x) == find(y)) {
// 找到无法避免的冲突
cout << c << endl;
return;
}
// 将x和y分配到不同监狱
merge(x, y+N);
merge(x+N, y);
}
完整代码示例
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN = 2e4+5;
int fa[MAXN*2];
struct Edge {
int x, y, c;
};
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y) {
fa[find(x)] = find(y);
}
int main() {
int N, M;
cin >> N >> M;
vector<Edge> edges(M);
for(int i=1; i<=2*N; i++) fa[i] = i;
for(int i=0; i<M; i++) {
cin >> edges[i].x >> edges[i].y >> edges[i].c;
}
sort(edges.begin(), edges.end(), [](auto &a, auto &b){
return a.c > b.c;
});
for(auto &e : edges) {
int x = e.x, y = e.y, c = e.c;
if(find(x) == find(y)) {
cout << c << endl;
return 0;
}
merge(x, y+N);
merge(x+N, y);
}
cout << 0 << endl;
return 0;
}
复杂度分析
- 时间复杂度:O(Mα(N)),其中α为反阿克曼函数
- 空间复杂度:O(N)
注意事项
- 扩展域的大小需要足够(通常是原问题的2倍)
- 冲突关系需要按冲突值从大到小处理
- 当发现两个罪犯必须在同一监狱时,当前冲突值即为答案
更多推荐


所有评论(0)