26.并查集

一、简介

并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。

顾名思义,并查集支持两种操作:

  • 合并(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 N221019729,α(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条指令,指令有两种格式:

  1. M i j i i i j j j 是两个整数( 1 ≤ i , j ≤ 30000 1 \le i,j \le 30000 1i,j30000),表示指令涉及的战舰编号。表示让第 i i i 号战舰所在列的全部战舰保持原有序列,接在第 j j j 号战舰所在列的尾部。

  2. C i j i i i j j j 是两个整数( 1 ≤ i , j ≤ 30000 1 \le i,j \le 30000 1i,j30000),表示指令涉及的战舰编号。表示查询第 i i i 号战舰和第 j j j 号战舰是否在同一序列,如果在则需要求出他们之间间隔了多少战舰。

数据范围: N ≤ 30000 , M ≤ 5 × 1 0 5 N\leq30000,M\leq5\times10^5 N30000,M5×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];
    }
}

四、作业

P3367 【模板】并查集

P2814 家谱

P2661 [NOIP2015 提高组] 信息传递

P1661 扩散

P1196 [NOI2002] 银河英雄传说

你可能感兴趣的:(算法竞赛讲义,算法,数据结构,c++,并查集)