Disjoin-Set
Disjoin-Set
并查集:是一种可以动态维护若干个不重叠的集合,支持合并和查询的数据结构。
存储:定义一个 f a [ ] fa[] fa[] 数组保存父节点,根的父节点设为自己。
int fa[size];
初始化:设有 n n n 个元素,起初所有元素各自构成一个独立的集合,即有 n 棵 1 个点的树。
for(int i=1;i<=n;++i) fa[i]=i;
查询:若 x x x 是树根,则 x x x 就是集合代表,否则递归查询 f a [ x ] fa[x] fa[x] 直至根节点。
int findroot(int x)
{
if(x==fa[x]) return x;
return fa[x]=findroot(fa[x]); //路径压缩, fa 直接赋值为代表元素
}
合并:合并元素 x x x 和 元素 y y y 所在的集合,等价于让 x x x 的树根作为 y y y 树根的子节点。
void merge(int x,int y)
{
int fax=findroot(x);
int fay=findroot(y);
fa[fax]=fay;
}
习题
连通块中点的数量
题目大意
给定一个包含n个点(编号为1 ∼ \sim ∼ n n n)的无向图,初始时图中没有边。现在要进行m个操作,操作共有三种:“ C C C a a a b b b”,点 a a a 和点 b b b 之间连一条边, a a a 和 b b b 可能相等;“ Q 1 Q1 Q1 a a a b b b”,询问点 a a a 和点 b b b 是否在同一个连通块中, a a a 和 b b b 可能相等;“ Q 2 Q2 Q2 a a a”,询问点 a a a 所在连通块中点的数量;
输入格式
第一行输入整数n和m。接下来m行,每行包含一个操作指令,指令为“ C C C a a a b b b”,“ Q 1 Q1 Q1 a a a b b b”或“ Q 2 Q2 Q2 a a a”中的一种。
输出格式
对于每个询问指令” Q 1 Q1 Q1 a a a b b b”,如果 a a a 和 b b b 在同一个连通块中,则输出“ Y e s Yes Yes”,否则输出“ N o No No”。对于每个询问指令“ Q 2 Q2 Q2 a a a”,输出一个整数表示点 a a a 所在连通块中点的数量;每个结果占一行。
输入样例
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例
Yes
2
3
解题思路
并查集,开个数组记录根节点有多少个元素
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn=1e5+7;
int fa[maxn],n,m,Size[maxn];
char op[5];
void init() //初始化
{
for(int i=1;i<=maxn;i++) fa[i]=i,Size[i]=1;
}
int findroot(int x) //查询 返回 x 的祖宗节点 + 路径压缩
{
if(x==fa[x]) return x; //x 是根节点 返回父节点
return fa[x]=findroot(fa[x]); // x 不是根节点, 就让它的父节点等于祖宗节点
}
void merge(int a,int b) //合并
{
int ta=findroot(a);
int tb=findroot(b);
if(ta != tb)
{
fa[ta]=tb; // b是 a 的根
Size[tb]+=Size[ta];
}
}
int main()
{
init();
scanf(" %d%d",&n,&m);
while(m--)
{
int a,b;
scanf(" %s",op);
if(op[0]=='C')
{
scanf(" %d%d",&a,&b);
merge(a,b);
}
if(op[1] =='1')
{
scanf(" %d%d",&a,&b);
if(findroot(a)==findroot(b)) puts("Yes");
else puts("No");
}
if(op[1] == '2')
{
scanf(" %d",&a);
printf("%d\n",Size[findroot(a)]);
}
}
return 0;
}
带权并查集
带权并查集是结点存有权值信息的并查集。权值代表着当前节点与父节点的某种关系,通过两者关系,也可以将同一棵树下两个节点的关系表示出来。而一般并查集只能判断属于某个集合。
食物链
题目大意
动物王国中有三类动物 A A A, B B B, C C C,这三类动物的食物链构成了有趣的环形。 A A A 吃 B B B, B B B 吃 C C C, C C C 吃 A A A。现有 N N N个动物,以 1 1 1- N N N编号。每个动物都是 A A A, B B B, C C C中的一种,但是我们并不知道它到底是哪一种。有人用两种说法对这 N N N 个动物所构成的食物链关系进行描述:
第一种说法是”1 X X X Y Y Y”,表示 X X X 和 Y Y Y 是同类。
第二种说法是”2 X X X Y Y Y”,表示 X X X 吃 Y Y Y。
此人对 N N N 个动物,用上述两种说法,一句接一句地说出K句话,这 K K K 句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
1)当前的话与前面的某些真的话冲突,就是假话;
2)当前的话中 X X X 或 Y Y Y 比 N N N 大,就是假话;
3)当前的话表示 X X X 吃 X X X,就是假话。
你的任务是根据给定的 N N N 和 K K K 句话,输出假话的总数。
输入格式
第一行是两个整数 N N N 和 K K K,以一个空格分隔。
以下K行每行是三个正整数 D D D, X X X, Y Y Y,两数之间用一个空格隔开,其中 D D D 表示说法的种类。
若 D D D =1,则表示 X X X 和 Y Y Y 是同类。
若 D D D =2,则表示 X X X 吃 Y Y Y。
输出格式
只有一个整数,表示假话的数目。
输入样例
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
输出样例
3
解题思路
带权并查集:3号吃2号,2号吃1号,1号吃3号,形成一个环。定义两个数组 fa[ ] 和 dis[ ], fa 用来判断集合关系,dis用来描述其与根节点的关系。因为关系满足传递性,所以可以推导出给出条件下的当前关系,在判断与之前已有关系是否矛盾。题目说:若 D=1,则表示X和Y是同类。x吃y或者 y 吃 x不成立,即 x 的捕食域与 y 的同类域在一起;y 的捕食域与 x 的同类域在一起。若D=2,则表示X吃Y。x 和 y 是同类 或者 y 吃 x;y 的捕食域与 x 的同类域在一起;x的同类域与y的同类域在一起。对于处理所有点与点之间的关系可以取模处理,让余数为0表示同类;余数是1表示吃其同类;余数是2表示其捕食域。此时和题目叙述略有不同,因此传参时需要让 D 先减 1处理。
#include <cstdio>
using namespace std;
const int maxn=5e4+7;
int fa[maxn],dis[maxn];
int n,k,ans;
int d,a,b;
void Init() //初始化
{
for(int i=1;i<=n;++i) fa[i]=i,dis[i]=0;
ans=0;
}
int findroot(int x) //查询 + 路径压缩
{
if(x==fa[x]) return x; // x 是根节点 返回父节点;x 不是根节点 让父节点等于祖宗结点
int t=findroot(fa[x]);
dis[x]=(dis[x]+dis[fa[x]]+3)%3;
fa[x]=t;
return fa[x];
}
//0类 是同类,即根,1类吃0类,2类被0类吃
void merge(int r,int x,int y)
{
int tx=findroot(x),ty=findroot(y);
if(tx!=ty)
{
fa[tx]=ty; //x根并入y根集合
//更新x根到新根节点关系
//x根到新根节点关系=根到x的关系(~dis[x])+x与y关系(r)+y到根关系
dis[tx]=(dis[y]-dis[x]+r+3)%3;
}
}
bool judge(int r,int x,int y)
{
if(x>n||y>n || (r==1 && x==y)) return false;
if(findroot(x)==findroot(y)) //判断x与y关系(r)等于x到根关系+根到y关系(~dis[y])
return r == (dis[x]-dis[y]+3)%3;
return true;
}
int main()
{
scanf(" %d%d",&n,&k);
Init();
while(k--)
{
scanf(" %d%d%d",&d,&a,&b);
d--;
if(judge(d,a,b)) merge(d,a,b);
else ans++;
}
printf("%d\n",ans);
return 0;
}
种类并查集
一般并查集可以判断一种关系,即属于这种关系或不属于这种关系。比如朋友的朋友是朋友。但比如敌人的敌人是朋友这种涉及两种以上的关系就会用到种类并查集,其实就是用多个并查集来模拟种类。但是如果关系(种类)过多,那么种类并查集写起来很繁琐,带权并查集更占优势。种类并查集还需要足够的空间,对空间限制小的题是硬伤。但是种类并查集更容易理解和模拟,带权并查集需要充分理解其向量思维。