并查集

简介 #

并查集支持以下操作:

  • 往一个集合中加入元素;

  • 查询两个元素是否在同一集合;

  • 合并两个集合.

问题 #

$\{a,b\}$,$\{e,c\}$,$\{e,d\}$ 分别在同一个集合,则共有几个集合?$b$ 和 $d$ 是否同集?

构造 #

把同集的两节点相连,则集合数 $=$ 连通图数. 若两节点连通,则它们同集.

并查集在每个集合中选取一个代表元素作为根节点,构造树型结构. 如图,$a,e$ 分别为两集合的代表元素.

flowchart
a --> b
e --> c
e --> d

$fa[i]$ 为节点 $i$ 的父节点编号.上图中 $fa[b]=a$,$fa[c]=e$,$fa[d]=e$.

根节点的 $fa$ 值都设为 $0$,$fa[a]=0$,$fa[e]=0$.

查询代表元素 #

集合的代表元素就是并查集中的根节点.若节点 $x$ 没有父节点,则它自己是根节点,否则递归查询它的父节点.时间复杂度为 $O(\log{n})$.

int find(int x) { // 返回节点 x 的根节点
    if (!fa[x]) return x;
    return find(fa[x]);
}

查询是否同集 #

若两个节点所在树的根节点相同,则它们同集.

bool judge(int x, int y) {
    return find(x) == find(y);
}

合并集合 #

若 $\{b,c\}$ 也同集,则合并其所在的集合.

将两集合根节点相连,即新建一条从 $a$ 连向 $e$ 的边:$fa[e]=a$.

flowchart
a --> b
a --> e
e --> c & d
void merge(int x, int y) {
    if(find(x) != find(y)) // 如果 x 和 y 已经同集,则没必要再合并
        fa[find(x)] = find(y);
}

路径压缩 #

  • 每次查询出节点 $i$ 所在集合的根节点 $e$ 后,使 $fa[i]=e$.

  • 再次查询 $i$ 的根节点时,find(i) 函数就会直接返回 $e$,免去递归过程.

时间复杂度优化为 $O(1)$. 该做法类似于记忆化搜索.

int find(int x) {
    if(!fa[x]) return x;
    return fa[x] = find(fa[x]);
    // 最后一行等价于 { fa[x] = find(fa[x]); return fa[x]; }
}