普通并查集
以洛谷 P3367 【模板】并查集 为例
题目描述
如题,现在有一个并查集,你需要完成合并和查询操作。
我们可以想到这样,对于每一次的合并操作,我们得到的结果可以用一棵树来表示,那么整个并查集的过程实际上是对森林进行操作,而我们判断两个元素是否在一个集合中,只需要判断其对应的森林的根节点是否相等就可以了
于是我们有了最初的想法:
fa[i]=i;
for_each_merge : fa[x] = y;
for_each_question : check(root_x,root_y)
但是,这样的时间复杂度肯定是不够优秀的,那么我们考虑一下几点:
对于每一个点 x 来说:
- 确定 x 所在的集合,只需确定 x 所在的森林的根
- x 所在的森林的根与 x 的父亲节点无关
那么知道了以上的一点性质,我们就可以很方便的去优化我们的代码
比如对于下面的一棵树(森林的一部分)
其实我们要仅仅需要当前节点对应的根即可
于是,我们可以将我们本来用来记录父亲节点的数组直接用来记录根节点,于是上面的图树退化为这样
在明确我们思路之后,就可以来构建代码了
在代码中,包含两个函数,一个是 find() 函数,起的作用是去寻找我们当前节点所在的树的根节点,并且在查找过程中更新路径上的节点的根节点,使其父亲节点全部都指向根
另一个函数是 merge() ,起到合并两个集合(森林)的作用
int Find(int x)
{
if(fa[x]==x) //当前点的父亲节点就是自己,那么该点就是当前树的根节点
return fa[x]; //返回跟节点,以便更新下方节点所指向的根
return fa[x]=Find(fa[x]); //否则就继续向上找根,并且更新当前点的父亲节点
}
void Merge(int x, int y)
{
f[Find(x)] = Find(y);
//这里只需要将x所在树的根的根修改为y所在的树的根,那么就完成了对x,y两棵树的合并
}
全部代码:
#include <bits/stdc++.h>
using namespace std;
int f[100010];
int n, m;
int Find(int x)
{
return ((f[x] == x) ? (x) : (f[x] = Find(f[x])));
}
void Merge(int x, int y)
{
f[Find(x)] = Find(y);
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
f[i] = i; //在未构建的时候,每个点单独成树,根即自身
for (int i = 1, flag, x, y; i <= m; i++)
{
scanf("%d%d%d", &flag, &x, &y);
if (flag == 1)
{
Merge(x, y);
}
else
{
if (Find(x) == Find(y))
printf("Y\n");
else
printf("N\n");
}
}
return 0;
}
权值并查集1(Find them, Catch them为例)
题目大意,有 n 个罪犯,2个犯罪团伙,每一个罪犯只属于其中某一个,并给出了 m 次操作
操作A是询问这两个罪犯是否是一个团伙
操作D是给出这两个罪犯不在一个团伙
同样我们分析,这与上面的并查集来说,又多了一层条件:是否属于同一种类别。对于这种并查集,我们称为权值并查集或种类并查集,此时我们不但要记录当前点所在树的根节点,同时,我们应该来记录当前点与其根节点的对应关系,并且我们能从这种对应关系中来推导出它们原来的关系
那么我们就要在这个程序中引入一个新的数组 en (enemy)
我们考虑如下关系,我们用0来代表是同一个帮派,1代表不同帮派,那么对于任意两个点它们分别和根的关系:
同一帮派和同一帮派:这两个点为同一帮派
同一帮派和不同帮派:这两个点为不同帮派
不同帮派和不同帮派:这两个点为同一帮派
也就是说:朋友的朋友是朋友,朋友的敌人是敌人,敌人的敌人是朋友
那么我们可以很方便的知道,对于帮派的更新,我们只需要对其值异或或求和模 2 即可
而对一条链上的各个点,我们不妨来画一个图辅助分析
[相同的颜色表示相同的帮派,边上的值代表当前点和其父亲节点的关系]
那么我们更新过程如下
也就是说,我们需要递归寻找当前点的根节点,并且在回溯的过程中去更新当前点和根节点的关系
然后就可以写出这样的一个Find函数
int Find(int x)
{
if (fa[x] != x)
{
fa[x] = Find(fa[x]);
en[x] = (en[x] + en[fa[x]]) % 2;
}
return fa[x];
}
然而不要高兴的太早,这必然是一个错误的代码,原因很简单,我们这里的更新方式是先对当前节点的父亲节点更新了,然后再对于当前节点的权值更新
但是仔细看上图,你会发现,我们对于每一次的关系更新,都是基于以前的父亲节点而不是经过我们Find函数处理过后的根节点
我们以上图为例,对于6号节点,我们应该通过5号节点去更新权值,但是在执行完Find操作后,我们6号节点的父亲已经变成1号节点了,自然我们再去更新就会出问题
所以以上的代码应该做出如下修改
int Find(int x)
{
if (fa[x] != x)
{
int t = fa[x]; //在这里记录一下之前的父亲节点
fa[x] = Find(fa[x]);
en[x] = (en[x] + en[t]) % 2;
}
return fa[x];
}
而对于查找函数,我们就要用到上面朋友的朋友是朋友,朋友的敌人是敌人,敌人的敌人是朋友的思想了
如下图:
对于上面的一棵树,我们要查找任意两点的关系,其实就是找这两点所在链上的权值异或和
而对于任意一点,我们执行完一次Find操作后,其父亲一定直接指向根,有且仅有0和1两种状态
比如节点3和6
我们对3和6分别做Find操作,那么3与根的关系是“敌人”,6与根的关系是“敌人”,所以3和6的关系是朋友
再比如6和7
一样做Find操作,我们可以知道,6与根的关系是“敌人”,7与根的关系是“朋友”,所以6和7的关系是敌人
Find操作到此结束
接下来就是Merge操作
虽然题目给的是x和y的关系,但是实际上我们可以将其转化为 Root_x 和 Root_y 的关系
有应为题目给出的是x和y为敌对关系,所以 Root_x 和 Root_y 通过(Root_x,x)与(Root_y,y)来更新时,还需要异或 1
证明如下:
我们定义有序点对 [A,B] = 0 or 1
分别表示A指向B时,其关系状态
那么我们可以得到如下性质
性质1:
:对于任意 [A,B] ≡ [B,A]
性质2:
对于 [A,B] = a [B,C] = b
恒有 [A,C] ≡ [A,B] XOR [B,C]
由性质2,我们可以得到:
所以对于[Root_x,Root_y] ≡ [Root_x,x] XOR [x,y] XOR [y,Root_y]
再由性质1,我们可以得到:
[Root_x,Root_y] ≡ [x,Root_x] XOR [x,y] XOR [y,Root_y]
于是我们的Merge函数就可以愉快的写了
void Merge(int x, int y)
{
int fx = Find(x);
int fy = Find(y);
if (fx != fy)
{
fa[fy] = fx;
en[fy] = (en[x] + en[y] + 1) % 2;
}
}
最后附上全部代码
#include <cstdio>
using namespace std;
int fa[100010]; //自己所属的祖先
int en[100010]; //自己和祖先的关系,1表示不同,0表示相同
int n, m;
int Find(int x)
{
if (fa[x] != x)
{
int t = fa[x];
fa[x] = Find(fa[x]);
en[x] = (en[x] + en[t]) % 2;
}
return fa[x];
}
void Merge(int x, int y)
{
int fx = Find(x);
int fy = Find(y);
if (fx != fy)
{
fa[fy] = fx;
en[fy] = (en[x] + en[y] + 1) % 2;
}
}
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
scanf("%d%d", &n, &m);
char s[5];
for (int i = 1; i <= n; i++)
fa[i] = i, en[i] = 0;
for (int i = 1, x, y; i <= m; i++)
{
scanf("%s%d%d", s, &x, &y);
if (s[0] == 'A')
{
if (Find(x) == Find(y))
{
if (en[x] == en[y])
puts("In the same gang.");
else
puts("In different gangs.");
}
else
puts("Not sure yet.");
}
else
Merge(x, y);
}
}
return 0;
}
权值并查集2(食物链)
这是一道相对具有技术含量的题,但是基本思路和上面差不多,都是多记录一下当前节点和根节点的关系
因为我们这里有吃与被吃两种关系,所以我们不妨假设用0,1,2来储存这种关系状态,x能吃x-1,且能被x+1吃(当然要对3取模)
那么我们对于上面的Find和Merge函数稍加改造就能得到我们这一题可用的函数
int find(int x)
{
if (fa[x] != x)
{
int f = fa[x];
//因为在下方的更新后,当前的fa已经改变,而对dist的更新在fa后,所以要先行记录
fa[x] = find(fa[x]);
dist[x] = (dist[x] + dist[f]) % 3; //更新关系
}
return fa[x];
}
Merge:
//合并同类
if (find(x) != find(y))
{
//改变x的根节点的指向
dist[fa[x]] = (dist[y] - dist[x] + 3) % 3;
fa[fa[x]] = fa[y];
}
//合并不同类(y被x吃)
if (find(x) != find(y))
{
dist[fa[x]] = (dist[y] - dist[x] + 4) % 3;
fa[fa[x]] = fa[y];
}
我们应当特别注意这一句:
dist[fa[x]] = (dist[y] - dist[x] + 4) % 3;
这里实际可以被拆分为这样:
dist[fa[x]] = ((dist[y] - dist[x] + 3) + 1) % 3;
这里的 +1 实际就反映了 x 和 y 的关系,而Root_x和Root_y同样可以由Dist_x , Dist_y , xy 来表示
至此,改题目的讲解已经完成,下面放上代码:
#include <bits/stdc++.h>
using namespace std;
int fa[100010], dist[100010];
int ans, n, k;
int find(int x)
{
if (fa[x] != x)
{
int f = fa[x];
//因为在下方的更新后,当前的fa已经改变,而对dist的更新在fa后,所以要先行记录
fa[x] = find(fa[x]);
dist[x] = (dist[x] + dist[f]) % 3; //更新关系
}
return fa[x];
}
void setup()
{
for (int i = 1; i <= n; i++)
fa[i] = i, dist[i] = 0;
}
int main()
{
int flag, x, y;
scanf("%d%d", &n, &k);
setup();
while (k--)
{
scanf("%d%d%d", &flag, &x, &y);
if (x > n || y > n)
{
ans++;
continue;
}
if (flag == 1)
{
if (find(x) != find(y))
{
//改变x的根节点的指向
dist[fa[x]] = (dist[y] - dist[x] + 3) % 3;
fa[fa[x]] = fa[y];
}
else if (dist[x] != dist[y])
ans++;
}
if (flag == 2)
{
if (find(x) != find(y))
{
dist[fa[x]] = (dist[y] - dist[x] + 4) % 3;
fa[fa[x]] = fa[y];
}
else if (dist[x] != ((dist[y] + 1) % 3))
ans++;
}
}
printf("%d", ans);
}