Day1并查集
1.定义
并查集是一种维护集合的数据结构,包括“并”、“查”、“集”分别取自Union(合并)、Find(查找)、Set(集合)三个单词。也就是说并查集支持一下两个操作:
- 合并:合并两个集合。
- 查找:判断两个元素是否在一个集合。
那么要用什么来实现呢其实就是用一个数组:
int father[N];
其中表示元素
的父亲节点,而父亲节点本身也是这个集合内的元素
。
例如
就表示元素1的父亲节点是2,以这种关系来表示元素所属的集合。
另外,如果
,则说明元素
是该集合的根节点。但一个集合只有一个根节点,且将其作为所属集合的标识。
如下图所示,数组的情况如下:
2.基本操作
总体来讲,并查集使用要先初始化数组,然后在进行查找和合并的操作。
(1).初始化
一开始,每一个元素都是独立的一个集合,因此需要令每一个
for(int i=1; i <= N; i++)
{
father[i] = i; // 令father[i]为-1也可
}
显然,数组起到关键作用,他在后面的无论是带权并查集和扩展域并查集中会有不同操作,后面会有展开解释。
(2).查找
由于规定同一个集合中有且仅有一个根节点,那么查找的对象就是对给定的结点寻找其根结点的过程。可以采用递推或递归的思想,直到找到根节点。先来看递推:
int Find(int x)
{
while(x != father[x]) //如果不是根结点,继续循环
x = father[x]; //反复找父亲的父亲结点
return x;
}
当然这个循环的过程也可以用递归来实现:
int Find(int x)
{
if(father[x] == x) return x; //如果找到根结点,就回溯
else Find(father[x]); //否则,继续找 x 的父亲的父亲节点
}
会发现,我们为了每次去找父亲节点就必须每次都循环一遍或每次都要重新递归,这样的计算量查找显然无法接受。
所以考虑路径压缩 :
问题:总共有个元素形成一条链如上图,要得知每一个元素的父亲结点,那么怎么优化查询操作呢?不妨把他们顺着路径,直接一个个连到各自的祖先结点上。
对应的图形变化如下:
然后对应刚刚的递归程序:
int Find(int x)
{
if(father[x] == x) return x;
else{
int F = Find(father[x]); //递归寻找father[x]的根结点F
father[x] = F; //把根结点的F赋值给father[x]
return F; //返回根结点
}
}
当我们熟练后可以合并上面复杂的代码,这样会既整洁又美观:
int find(int x)
{
if(fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
(3).合并
这里考虑到以后很少用到判断两个元素是否在一个集合函数就直接跳过给出代码:
合并操作其实就是把两个集合合并成一个,分两种讲法:
标准版:先判断两者是否在同一个集合当中,只有当两个元素属于不同集合时才合并,而合并的过程一般是把其中的一个集合的根结点的父亲指向另一个集合的根结点。
简易版: 直接合并就可以了,并且
同
本质上没有区别。
int Union(int x,int y)
{
fa[find(x)] = find(y);
}
例题:
P1551 亲戚
考虑到自己也是自己的亲戚就好了
P2814 家谱
只需维护数组就好了,因为每次询问自己和父亲
完结散花
生而为人,你且修身,你且渡人,你且如水,居恶渊而为善,无尤也 —— —— 老子《道德经》