并查集
并查集是一种动态维护集合关系的集合,一般包括查询集合和合并集合两个操作。
是一种维护集合关系的数据结构。在适当优化下可以达到常数级别的时间复杂度。
中心思想
中心思想为:换老大(即祖宗、父亲)。
我们设第 \(i\) 个集合的老大为 \(p_i\) 。
在开始时,每个集合的老大就是它本身,而合并集合就是把一个集合的老大换成另一个集合的老大,即 \(p_i = p_j\),而判断两个集合是否联通,就是判断他们的老大是不是一个。而整个联通块老大的老大就是它本身。这就是并查集的中心思想。
可写出查询代码
int find(int x)
{
if (x != p[x]) return find(p[x]); // 如果x集合不是老大,则继续查找
return x; // 如果 x 是老大,则直接输出老大
}
这是基本的代码,但并不常用,没有优化的并查集是 \(\operatorname O(n)\) 的时间复杂度。
具体如下图
![Pasted image 20250730193024.png]]
对于合并集合,设两个集合 \(a,b\) 的老大(树根)为 \(pa,pb\),如果 \(pa = pb\) 说明两个集合互相联通,不需要合并;如果 \(pa \not = pb\),则说明两个集合不联通,可以进行合并,合并两个集合就是使 \(p_{pb} = pa\)。
代码为:
int pa = find(a), pb = find(b);
if (pa != pb)
{
// if (sizes[pa] < sizes[pb]) swap(pb, pa);
p[pb] = pa;
// sizes[pa] += sizes[pb];
}
需要注意一下,我们一般不会每次使用 \(pa/pb\) 时都调用 \(find()\),因为在维护值时,可能会错误。为了保证正确,一般存下 \(pa,pb\)
优化
分为两种优化:路径压缩和按秩合并
路径压缩
路径压缩是最常用的,也是优化最明显的方法。原代码 \(\operatorname O(n)\) 时间复杂度的原因在于每次查询都必须一级一级地向上查询。如果每一级都知道自己的最高级,那么就可以优化查询次数。
而路径压缩可以在查询结束后(即找到老大后),把原本的小弟的上级老大全部换为最大的老大。从而降低之后向上查询的次数。优化后均摊时间复杂度为 \(\operatorname O(\log n)\)(实际上远小于此)
最常见的代码为:
int find(int x)
{
if (x != p[x]) p[x] = find(p[x]); // 在向上查询之后更改老大
return p[x]; // 因为上面已经更新了(如果没更新,那么这就是最大的老大)老大为
// 最大的老大,所以返回值即为最大的老大
}
干净一点
int find(int x)
{
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
这是最常用的代码,也是最常用的优化方法。
值得一提的是,这种优化可以被人为卡成 \(\operatorname O(n)\)。
如何卡掉路径压缩
按秩合并
即字面意思,按照秩序合并,这个秩序可以说集合的最大深度,也可以是集合点的数量。一般使用集合数量(较为方便统计)作为秩。合并方法为将秩小的连接在秩大的后面。
简单说就是把点数少的集合连接在点数多的集合后面。
按照思路写出算法即可:
int sizes[N];
int p[N];
int pa = find(a), pb = find(b);
if (pa != pb)
{
if (sizes[pa] < sizes[pb]) swap(pa, pb); // 秩序
p[pb] = p[pa];
sizes[pa] += sizes[pb];
sizes[pb] = 0; // 通常需要清零
}
这种方法优化较少,但与路径压缩相结合可以达到常数级别的效率。
代码
常用代码为
//
int p[x];
int find(int x)
{
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
int main()
{
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 使用find查询
}
扩展
-
常用语维护联通块关系等,在特殊情况下可以维护数值(带权并查集),和矛盾关系(扩展域并查集)。
-
敌人的敌人就是朋友,很多情况可以按照这个模式来合并集合。
进阶应用
带权并查集(边带权并查集)
带权一般是带一个东西,如每个点到根节点的距离之类的。一般代码为:
int d[N]; // 到根节点的距离,可以存别的
int find(int x) // 返回根节点
{
if (p[x] != x)
{
int root = find(p[x]);
d[x] += d[p[x]]; // 因为路径压缩操作完后,p[x]指向root根节点,根据递归
p[x] = root; //find(p[x])结束后d[p[x]]含的就是p[x]到根的距离,p[x]指向
} //root,d[x] 存的应该是x到root的距离,即d[x]+d[p[x]]. x -> p[x] -> root
return p[x];
}
可能要维护并查集内的点(存下来),这时候把少点连到多点上。可以保证此类操作总和最多 \(\operatorname O(n\log n)\)。可以防卡。如果不加,可能会卡成 \(\operatorname O(n^2)\)。可以叫一个 Trick。(按秩合并)
可证,最坏情况为每次都是 \(pa_{size} = pb_{size}\), 此时可以把序列分为两两一组,合并,再两两一组,合并,重复直到全部合并。从上到下每一层最多加 \(n\) 次,每层连通块数量减半,所以一共 \(\log n\) 层。时间复杂度为 \(\operatorname O(n\log n)\)。
扩展域并查集
可以保存关系,用好了或许能用于DP。
详细可以见 [[#扩展域并查集ha|后面]]
为了保证覆盖全面,关系必须紧凑,不能少不能多,即使看起来无用,但实际上他会影响传递。而关于判断,不用枚举各种情况,只需要表达出意思即可。详细可见 食物链
后面
连续^有可能传递大数
比如这个
#include <iostream>
using namespace std;
int main()
{
int a = 1, b = 1;
while (a > 0)
{
a *= 2;
a ^= 1;
cout << a << endl;
}
return 0;
}
int溢出奇偶性不变
带权边并查集写法
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map>
using namespace std;
const int N = 10010;
int p[N], d[N];
int n, m, cnt;
unordered_map<int, int> S;
int get(int x)
{
if (!S.count(x)) S[x] = ++ cnt;
return S[x];
}
int find(int x)
{
if (p[x] != x)
{
int root = find(p[x]);
d[x] ^= d[p[x]]; // 换成加法可能会溢出int变成负数(溢出后奇偶性不变)
p[x] = root;
}
return p[x];
}
int main()
{
cin >> n >> m;
for (int i = 1; i < N; i ++ ) p[i] = i;
int res = m;
for (int i = 1; i <= m; i ++ )
{
int a, b;
string op;
cin >> a >> b >> op;
a = get(a - 1), b = get(b);
int t = 0;
if (op == "odd") t = 1;
int pa = find(a), pb = find(b);
if (pa == pb)
{
if (d[a] ^ d[b] != t) // 使用加法需要把它变成正值即 ((d[a] + d[b]) % 2 + 2) % 2 != t ,这是为了防止int溢出成负数
{
res = i - 1;
break;
}
}
else
{
p[pa] = pb;
d[pa] = d[a] ^ d[b] ^ t; // 这里d[]的值全部由t提供(d[]初值为0), t最大是1,
} // 所以在异或下最大是1,所以偶数全是0, 奇数全是1,没有溢出空间的风险
}
cout << res << endl;
return 0;
}
扩展域并查集ha
总体思想用每个变量作为_条件_, 如a 为 a 是奇数, a + N 为 a 是偶数
或者 b 为 b在 A类, b + N 为 b在 B类 ,b + 2N 为 b在 C类;
然后把情况放入相应的集合, 如果不同集合冲突(相等)则说明是谎话(有矛盾)
同一个集合的含义:如果集合内一个条件成立,则整个集合都成立(即可以互相推出)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <unordered_map>
using namespace std;
const int N = 20010, M = N / 2;
int p[N];
int n, m, cnt, res;
unordered_map<int, int> S;
int get(int x)
{
if (S.count(x) == 0) S[x] = ++ cnt;
return S[x];
}
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
cin >> n >> m;
for (int i = 1; i < N; i ++ ) p[i] = i;
res = m;
for (int i = 1; i <= m; i ++ )
{
int a, b;
string op;
cin >> a >> b >> op;
a = get(a - 1), b = get(b);
if (op == "even") // 如果偶数
{
if (find(a + M) == find(b)) // 设a + M 为 a 是奇数, a是a 是偶数, b 同理设,则 a + M 一定和b + M
{ // 在一个集合 a 与 b 一定在一个集合 相等则有谎言
res = i - 1;
break;
}
else
{
p[find(a + M)] = find(b + M);
p[find(a)] = find(b);
}
}
else
{
if (find(a) == find(b)) // 与上面同理
{
res = i - 1;
break;
}
else
{
p[find(a)] = find(b + M);
p[find(a + M)] = find(b);
}
}
}
cout << res ;
return 0;
}


浙公网安备 33010602011771号