一、简介
并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。
顾名思义,并查集支持两种操作:
- 合并(Merge):合并两个元素所属集合(合并对应的树)
- 查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合
并查集在经过修改后可以支持单个元素的删除、移动;使用动态开点线段树还可以实现可持久化并查集。并查集的使用范围非常广泛,将作为栈和队列这样的基础数据结构在很多题目当中结合应用。而如 K r u s k a l Kruskal Kruskal算法等也需要借助并查集来实现。
二、基本运用
1.初始化
初始时,每个元素都位于一个单独的集合,表示为一棵只有根结点的树。方便起见,我们将根节点的父亲设为自己。
for(ll i=1;i<=n;i++)
fa[i]=i;
2.查询与路径压缩
为了确认两个元素是否属于同一集合,我们需要判定他们是否拥有同一个祖先。判断是否拥有同一个祖先是一个经典的问题,我们称之为最近公共祖先问题(LCA),我们在后续会专门讲解此问题。
在并查集这里我们并不需要这么复杂的方式。我们可以简化地判断他们是否拥有一个公共的根结点即可。沿着树向上移动,直至找到根结点。
对于层数较大的树而言,要找到根结点花费的时间将比较长。因为一个结点的从属情况到最后实际上只与根结点有关,所以我们可以采用路径压缩的方式,将结点直接连到根结点以加快后续查询。这个过程可以整合到查询当中。
ll get(ll x)
{
if(x!=fa[x])
return fa[x]=get(fa[x]);
return fa[x];
}
3.合并
当需要合并两棵树时,我们只需要将一棵树的根节点连到另一棵树的根节点。
void merge(ll x,ll y)
{
ll fx=get(x),fy=get(y);
fa[fx]=fy;
}
在合并的时候还有一种优化方式,称之为启发式合并。因为哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵,以免发生退化。
同时采用路径压缩和启发式合并对并查集进行优化的话,每次查询的均摊时间复杂度为 O ( α ( N ) ) O(\alpha(N)) O(α(N)), α ( N ) \alpha(N) α(N)为反阿克曼函数, ∀ N ≤ 2 2 1 0 19729 , α ( N ) < 5 \forall N\leq2^{2^{10^{19729}}},\alpha(N)<5 ∀N≤221019729,α(N)<5。若只采用一种,则均摊时间复杂度为 O ( l o g N ) O(logN) O(logN)
三、带权并查集
1.简介
如果树边不只是表示两者同属一个集合的关系,还涉及到边权的问题,那么这样的并查集就变成了带权并查集,查询与合并时需要进行的操作都有很多的不同。
2.例题:P1196
(1)题目大意
有一个划分为 N N N列的战场,每列依次编号为 1 , 2 , … , N 1,2,\ldots,N 1,2,…,N。有 N N N艘战舰,序号依次为 1 , 2 , … , N 1,2,\ldots,N 1,2,…,N,让第 i i i 号战舰处于第 i i i 列。现有 M M M条指令,指令有两种格式:
-
M i j
: i i i 和 j j j 是两个整数( 1 ≤ i , j ≤ 30000 1 \le i,j \le 30000 1≤i,j≤30000),表示指令涉及的战舰编号。表示让第 i i i 号战舰所在列的全部战舰保持原有序列,接在第 j j j 号战舰所在列的尾部。 -
C i j
: i i i 和 j j j 是两个整数( 1 ≤ i , j ≤ 30000 1 \le i,j \le 30000 1≤i,j≤30000),表示指令涉及的战舰编号。表示查询第 i i i 号战舰和第 j j j 号战舰是否在同一序列,如果在则需要求出他们之间间隔了多少战舰。
数据范围: N ≤ 30000 , M ≤ 5 × 1 0 5 N\leq30000,M\leq5\times10^5 N≤30000,M≤5×105
(2)题目分析
- 此处涉及到判断是否在同意序列以及合并序列的问题,所以很容易想到并查集来求解
- 唯一的问题是,我们需要求出两个战舰之间隔了多少战舰
- 如果我们直接进行路径压缩,所有直接与根相连,则无法得出答案
- 如果不进行路径压缩,则将边权设置为 1 1 1,两者的距离之差减 1 1 1 就是答案了,但这不满足时间复杂的要求
- 所以,我们在考虑路径压缩的情况下,新建一个数组 d i s t dist dist, d i s t [ x ] dist[x] dist[x] 表示战舰 x x x 与 f a [ x ] fa[x] fa[x] 之间的边权,也就是之间的战舰数量
- 如此一来,我们需要对合并与查询的代码做相应的修改
ll get(ll x)
{
if(x==fa[x])
return fa[x];
ll y=fa[x];
fa[x]=get(y);
dist[x]+=dist[y];
return fa[x];
}
void merge(ll a,ll b)
{
a=get(a);b=get(b);
if(a!=b)
{
fa[a]=b;
dist[a]=size[b];
size[b]+=size[a];
}
}