before 正片
讲之前说一下,并查集可有用了!什么题都能用并查集乱搞!
(最基础的)正片
并查集是啥?
- 定义:并查集(disjoint-set data structure)分为3个部分,并,查,集。然后我们讲的顺序是集,查,并。
2.珂以干嘛:
2.1 初级:珂以维护在一块的关系。打个比方,我们珂以快速求两个人是不是在同一个班里面。同时,我们也珂以记录集合里面的信息(比如有多少元素,最大最小的元素,等)。
2.2 升级:珂以维护种类的关系(后面会讲)。举个栗子,就是敌人和朋友的关系,并且敌人的敌人是朋友。
那说完这些,就要讲实现了。
首先是“集”的实现。通常,我们把它用一个森林(也就是很多个树结构)来表示。但是,我们维护的很简单,只要知道每个节点的父亲是谁就珂以了。如果一个节点的父亲是自己,说明它是祖先。初始状态每个节点都是祖先(这个不加就炸了)。
然后是这一步的代码实现(我喜欢面向对象):
class DSU
//DSU是并查集的英文名简写
{
public:
#define N 1001000
int Father[N];
void Init()
{
for(int i=0;i<N;i++)
{
Father[i]=i;
}
}
};
然后并查集就学了13\frac{1}{3}31了。
接下来是如何“查”。
这个很显然,写一个递归就珂以了。不断的找,如果是祖先,就返回自己,否则就去问自己的爸爸祖先是谁,然后爸爸再问爸爸的爸爸,爸爸的爸爸再问爸爸的爸爸的爸爸的爸爸…直到没有爸爸(自己是祖先)。
代码:
//写在类里面
int Find(int x)
{
if (x==Father[x])
{
return x;
}
else
{
return Find(Father[x]);
}
}
然后并查集就学了23\frac{2}{3}32了。
接下来是“并”。这也同样很简单,如果我们要并xxx和yyy,只要把xxx的祖先的爸爸设置成yyy的祖先,xxx就并到yyy上了。代码:
//这个也写在类里面
void Merge(int x,int y)
{
int ax=Find(x),ay=Find(y);
Father[ax]=ay;
}
这样并查集就学了33\frac{3}{3}33了。
是不是很水?但这还不够,后面还有优化(依然很水)。
正片的优化
优化1. 路径压缩
我们会发现,在维护并查集的过程中,其实中间经过哪些父亲,并不是很重要,只要知道谁是祖先就珂以了,所以在找的过程中,直接把爸爸的爸爸设置成祖先即可(这样快了好多,理论上只要有了这个优化,不加优化2很多题都珂以过了。)
代码:
int Find(int x)
{
if (x==Father[x])
{
return x;
}
else
{
Father[x]=Find(Father[x]);
//直接接到祖先上
return Father[x];
}
}
优化2. 按秩合并
【注释】
秩:集合大小
也就是说我们把集合从小的接到大的。这样就避免了结构十分不平衡。(如果出题人故意卡你的话,结构不平衡就算路径压缩也没有用)。这就涉及到记录集合大小的问题。容易想到,只要记录一个CntCntCnt数组,记录以iii为根的子树的集合大小。而iii所在的集合大小,就是Cnt[Find(i)]Cnt[Find(i)]Cnt[Find(i)]。
显然,初始的时候,由于每个点都单独在一个只有自己的集合,所以Cnt[i]=1Cnt[i]=1Cnt[i]=1。
代码(由于改动较大,直接贴完整代码):
class DSU
{
public:
#define N 1001000
int Father[N];
int Cnt[N];
void Init()
{
for(int i=0;i<N;i++)
{
Father[i]=i;
Cnt[i]=1;//注意初始化!
}
}
int Find(int x)
{
if (x==Father[x])
{
return x;
}
else
{
Father[x]=Find(Father[x]);
return Father[x];
}
}
void Merge(int x,int y)
{
int ax=Find(x),ay=Find(y);
if (ax==ay) return; //2020.02.11 update: 这句不加出大问题!
if (Cnt[ax]<Cnt[ay])//小的接到大的上
{
Cnt[ay]+=Cnt[ax];
Father[ax]=ay;
}
else
{
Cnt[ax]+=Cnt[ay];
Father[ay]=ax;
}
}
};
种类并查集
种类并查集珂以记录“分组”的关系。闲话少说,来道例题。
例题:洛谷2024 [NOI2001]食物链
(一道经典题)有nnn个生物,每个生物可能是A,B,CA,B,CA,B,C种类中的一种。其中,AAA吃BBB,BBB吃CCC,CCC吃AAA。
给你kkk句话,格式为:
- x yx\ yx y 表示x,yx,yx,y同类。
- x yx\ yx y 表示xxx吃yyy。
请你求出有多少句话是假的。假如两句话矛盾了,那靠前的那句话是真的。假的话有几种可能:
- xxx或y>ny>ny>n (越界了,自然是假的)
- 吃自己的情况显然是假的
- 和前面的某句话矛盾了,则前面的是真的,这句是假的
解法
普通的并查集只能维护“同类”的关系。我们要维护三个种类,怎么办呢?
拆点。我们把每个点复制三遍,分别表示这个点属于AAA的情况,属于BBB的情况,属于CCC的情况。我们记点uuu属于种类XXX的情况为X(u)X(u)X(u)。像这样:

对于1 x y1\ x\ y1 x y的操作,我们合并A(x)↔A(y)A(x)\leftrightarrow A(y)A(x)↔A(y),B(x)↔B(y)B(x)\leftrightarrow B(y)B(x)↔B(y),C(x)↔C(y)C(x)\leftrightarrow C(y)C(x)↔C(y)。解释的具体点,就是:
xxx属于AAA时,则yyy属于AAA;
xxx属于BBB时,则yyy属于BBB;
xxx属于CCC时,则yyy属于CCC;
比如说我们令111和222为同类,就这样连边:

对于2 x y2\ x\ y2 x y的操作,我们合并A(x)↔B(y)A(x)\leftrightarrow B(y)A(x)↔B(y),B(x)↔C(y)B(x)\leftrightarrow C(y)B(x)↔C(y),C(x)↔A(y)C(x)\leftrightarrow A(y)C(x)↔A(y)。同理,它相当于:
xxx属于AAA时,则yyy属于BBB;
…(不赘述了)
比如我们又令333吃444,画出来图就是这样的:

那么,什么情况是矛盾的情况呢?
我们要在一遍处理的时候一遍统计。
首先,“越界”的情况很好判断,统计答案然后continuecontinuecontinue掉即珂;
关于“自己吃自己”的情况,在操作2 x y2\ x\ y2 x y的时候,如果x,yx,yx,y在同一个集合,就是不合法的。
还有其他的矛盾的情况
- 操作2 x y2\ x\ y2 x y的时候,如果这个时候yyy吃xxx,那就是矛盾的
- 操作1 x y1\ x\ y1 x y的时候,如果这个时候xxx吃yyy或者yyy吃xxx,也是矛盾的。
就这些了。注意代码实现。
例题代码
#include<bits/stdc++.h>
#define N 1001000 //其实15000+1就够了
using namespace std;
class DSU
{
private:
int Father[N]; //懒得写按秩合并了(千万不要学我)
public:
void Init()
{
for(int i=0;i<N;i++)
{
Father[i]=i;
}
}
int Find(int x)
{
return x==Father[x]?x:Father[x]=Find(Father[x]);
}
void Merge(int x,int y)
{
Father[Find(x)]=Find(y);
}
}D;
int n,m;
#define A(x) x
#define B(x) x+n
#define C(x) x+2*n
//开三倍空间
//1~n表示A种类
//n+1~2n表示B种类
//2n+1~3n表示C种类
void Eat(int x,int y) //x吃y的关系
{
D.Merge(A(x),B(y));
D.Merge(B(x),C(y));
D.Merge(C(x),A(y));
}
void Same(int x,int y) //x和y同类
{
D.Merge(A(x),A(y));
D.Merge(B(x),B(y));
D.Merge(C(x),C(y));
}
bool Query(int x,int y)
{
return D.Find(x)==D.Find(y);
}
int cnt=0;
void Input()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int o,u,v;
cin>>o>>u>>v;
if (u>n or v>n)
{
cnt++;
continue;
}
if (o==1) //同类的情况
{
if (Query(u+n,v) or Query(u,v+n)) //u,v有吃的关系
{
cnt++;
continue;
}
else
{
Same(u,v);
}
}
else //u吃v
{
if (Query(u,v) or Query(u+n,v)) //u,v同类,或者v吃u
{
cnt++;
continue;
}
else
{
Eat(u,v);
}
}
}
printf("%d\n",cnt);
}
main()
{
D.Init();
Input();
return 0;
}
本文详细介绍了并查集的概念、基本操作以及两种常见的优化方法——路径压缩和按秩合并。通过实例展示了如何使用并查集解决实际问题,如洛谷2024 [NOI2001]食物链问题,解释了如何处理不同种类的关系,以及在处理过程中如何判断和处理矛盾情况。
1175

被折叠的 条评论
为什么被折叠?



