并查集就是维护每一个树的过程。
836. 合并集合
一共有 n𝑛 个数,编号是 1∼n1∼𝑛,最开始每个数各自在一个集合中。
现在要进行 m𝑚 个操作,操作共有两种:
M a b
,将编号为 a𝑎 和 b𝑏 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;Q a b
,询问编号为 a𝑎 和 b𝑏 的两个数是否在同一个集合中;
输入格式
第一行输入整数 n𝑛 和 m𝑚。
接下来 m𝑚 行,每行包含一个操作指令,指令为 M a b
或 Q a b
中的一种。
输出格式
对于每个询问指令 Q a b
,都要输出一个结果,如果 a𝑎 和 b𝑏 在同一集合内,则输出 Yes
,否则输出 No
。
每个结果占一行。
数据范围
1≤n,m≤1051≤𝑛,𝑚≤105
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
将区间合并成一个集合:
将两个集合合并成一个:
代码:
/*
并查集就是让整个区间用一颗数表示
每次并集的根节点就是整个区间的尾部
整棵大树的根节点就是所有区间最长的尾部
*/
#include<bits/stdc++.h>
using namespace std;
int m,n;
//每一个区间点的指向(指向它的父类)
int p[100050];
//经过此操作可让所有的子叶的指向都指向整个大树的根节点
int find(int x)
{
/*
“超级加辈”
让它的父类(下一指向) 指向祖宗(根节点)
化祖宗为父类进而压缩路径
*/
//当p[x]==x时,也就让p[x]指向它的祖宗节点了
//否则就让p[x]去寻找它的指向
if(p[x]!=x) p[x]=find(p[x]);
//返回x的父类
return p[x];
}
int main()
{
cin >> m >> n;
//一开始的指向就是它本身,所以整个大树的根节点就是它本身
for(int i=1;i<=m;i++) p[i]=i;
while(n--)
{
char op;
int a,b;
cin >> op >> a >> b;
if(op=='M')
//让a祖宗的指向(find(a))指向(p[])b的祖宗(find(b))
p[find(a)]=find(b);
else
{
//如果二者的祖宗相同,那即代表在同一区间
if(find(a)==find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
837. 连通块中点的数量
给定一个包含 n𝑛 个点(编号为 1∼n1∼𝑛)的无向图,初始时图中没有边。
现在要进行 m𝑚 个操作,操作共有三种:
C a b
,在点 a𝑎 和点 b𝑏 之间连一条边,a𝑎 和 b𝑏 可能相等;Q1 a b
,询问点 a𝑎 和点 b𝑏 是否在同一个连通块中,a𝑎 和 b𝑏 可能相等;Q2 a
,询问点 a𝑎 所在连通块中点的数量;
输入格式
第一行输入整数 n𝑛 和 m𝑚。
接下来 m𝑚 行,每行包含一个操作指令,指令为 C a b
,Q1 a b
或 Q2 a
中的一种。
输出格式
对于每个询问指令 Q1 a b
,如果 a𝑎 和 b𝑏 在同一个连通块中,则输出 Yes
,否则输出 No
。
对于每个询问指令 Q2 a
,输出一个整数表示点 a𝑎 所在连通块中点的数量
每个结果占一行。
数据范围
1≤n,m≤1051≤𝑛,𝑚≤105
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
代码:
注意未合并之前的每个元素长度初始化唯一
#include<bits/stdc++.h>
using namespace std;
int p[100050],size1[100050];
int find(int x)
{
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int main()
{
int m,n;
cin >> m >> n;
for(int i=1;i<=m;i++)
{
p[i]=i;
//一开始每个元素都未联通,则长度为1
size1[i]=1;
}
while(n--)
{
string op;
int a,b;
cin >> op;
if(op=="C")
{
cin >> a >> b;
if(find(a)==find(b)) continue;
/*
大家看y总这段代码时要注意,在C操作时,y总先把a,b的根结点取出来了:a = find(a), b = find(b);
因此接下来是先将集合a接到集合b下再把a的连通块大小加到b上,
还是先把a的连通块大小加到b上再操作集合都是可以的,
如果大家没有提前一步的处理,就必须要先加连通块大小再操作集合,
否则操作完集合后,a和b的根结点将会重叠,导致输出错误!
*/
//先取出来即代表将a和b的根节点转化为静态变量,a就是find(a)
//否则经过集合的操作后,find(a)会指向find(b)导致根节点重叠
//让根节点a所在的集合数值的数量加到b上去
size1[find(b)]+=size1[find(a)];
p[find(a)]=find(b);
}
else if(op=="Q1")
{
cin >> a >> b;
if(find(a)==find(b)) puts("Yes");
else puts("No");
}
else
{
cin >> a;
cout << size1[find(a)] << endl;
}
}
return 0;
}
240. 食物链
a,b,c三类共有两种情况,同类,异类。
异类又分为捕食与被捕食的关系,所以能形成下图所示的杀戮关系
不难发现随便两个动物的关系都可以用它到根节点的距离%3来推断。
若a%3=b%3则a与b为同类
若a%3=1,b%3=2则a吃b
注意,让根节点被吃,这样距离%3的值才能达到统一
动物王国中有三类动物 A,B,C𝐴,𝐵,𝐶,这三类动物的食物链构成了有趣的环形。
A𝐴 吃 B𝐵,B𝐵 吃 C𝐶,C𝐶 吃 A𝐴。
现有 N𝑁 个动物,以 1∼N1∼𝑁 编号。
每个动物都是 A,B,C𝐴,𝐵,𝐶 中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N𝑁 个动物所构成的食物链关系进行描述:
第一种说法是 1 X Y
,表示 X𝑋 和 Y𝑌 是同类。
第二种说法是 2 X Y
,表示 X𝑋 吃 Y𝑌。
此人对 N𝑁 个动物,用上述两种说法,一句接一句地说出 K𝐾 句话,这 K𝐾 句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
- 当前的话与前面的某些真的话冲突,就是假话;
- 当前的话中 X𝑋 或 Y𝑌 比 N𝑁 大,就是假话;
- 当前的话表示 X𝑋 吃 X𝑋,就是假话。
你的任务是根据给定的 N𝑁 和 K𝐾 句话,输出假话的总数。
输入格式
第一行是两个整数 N𝑁 和 K𝐾,以一个空格分隔。
以下 K𝐾 行每行是三个正整数 D,X,Y𝐷,𝑋,𝑌,两数之间用一个空格隔开,其中 D𝐷 表示说法的种类。
若 D=1𝐷=1,则表示 X𝑋 和 Y𝑌 是同类。
若 D=2𝐷=2,则表示 X𝑋 吃 Y𝑌。
输出格式
只有一个整数,表示假话的数目。
数据范围
1≤N≤500001≤𝑁≤50000,
0≤K≤1000000≤𝐾≤100000
输入样例:
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
输出样例:
3
当不在一个区域,化为同一区域时:
①同类:
②捕食关系
代码:
//两种做法,这里用并查集来维护整个区间
#include<bits/stdc++.h>
using namespace std;
int p[50050],ans[50050];
int find(int x)
{
//在p[x]指向其根节点的过程中,x到根节点的距离会被逐渐算出来了
//不递归到最后是不会执行加法语句的,所以是从最上面依次向下执行加法语句
if(p[x]!=x)
{
int u=find(p[x]);
//算出每一个值到根节点的距离(压缩路径)
//x到父节点的距离加上父节点到根节点的距离之和即为x到根节点的距离
/*
父节点一定指向根节点吗?
一开始x的父节点不一定指向根节点,但是经过不断的递归
最终是从根节点的儿子开始回溯
先是根节点到儿子的距离被加进去,接着是孙子到儿子的距离被加进去
在是重孙到孙子的距离被加进去......最后加到x的父节点到爷爷的距离被加进去
由于一开始的ans[x]即为x到父节点的距离,所以递归、追溯完毕之后ans[x]就是x到根节点的距离
*/
ans[x]+=ans[p[x]];
//让x指向根节点(以便判断 与其他区域是否在同一片区域)
p[x]=u;
}
return p[x];
}
int main()
{
int m,n;
cin >> m >> n;
//一开始没形成杀戮领域前都是独立的,自己就是单独一片区域
for(int i=1;i<=m;i++) p[i]=i;
int ant=0;
//模拟题目要求
while(n--)
{
int x,a,b;
cin >> x >> a >> b;
//先记录下新加入的动物a、b的根节点指向
int px=find(a),py=find(b);
//“当前的话中 X或 Y比 N大,就是假话;”
if(a>m||b>m)
ant++;
else
{
if(x==1)
{
//判断是否为假语句的前提是px与py在同一个区域里(同一个根节点)
//若满足是同一类,那么他俩到根节点的距离对3取模都为0
if(px==py && (ans[a]-ans[b])%3) ant++;
//如果是同一类而不再同一片区域内,表示之前从来没出现过,那么创建一个区域,b为根节点
else if(px!=py)
{
//让y的根节点作为x与y所在区域的新的根节点
p[px]=py;
//(ans[px]+ans[a])%3=ans[b]%3;(同一类)
/*
ans[px]是待计算的距离(因为合并后这段距离未知)。
意义是计算出a的根节点到b的根节点的距离
*/
ans[px]=ans[b]-ans[a];
}
}
//同理
else
{
if(px==py && (ans[a]-ans[b]-1)%3) ant++;
else if(px!=py)
{
p[px]=py;
//(ans[x]+ans[px])%3=ans[y]%3+1(x吃y),x比y高一级
ans[px]=ans[b]+1-ans[a];
}
}
}
}
cout << ant;
return 0;
}
tips
注意递归函数的应用!!!
递归不触碰到某一条件是不会进行追溯的。
并查集很好的运用的树的思想