【算法编程】并查集

【算法编程】并查集

并查集是一个非常简单高效的数据结构,其可以快速地对集合进行合并和查询,通常可用于集合的合并、图连通域的合并等。一个对并查集非常形象的解释可参见算法学习笔记(1) : 并查集

基本原理:

  • 给定一组集合,每个集合可以用一棵树表示,树根的编号可以表示为整个集合的编号,每个结点保存其父结点的编号。可以维护一个数组 F [ i d x ] F[idx] F[idx],其表示编号为第 i d x idx idx 个结点的父节结点。
  • 初始化时,每个结点 i d x idx idx 自身可以独立为一个集合,即定义为 F [ i d x ] = i d x z F[idx] = idxz F[idx]=idxz
  • 进行集合的合并时,如果两个集合不是同一个,则只需要将所在的两个集合的根结点进行合并即可

基本操作:

  • 判断当前结点是不是所在树的根结点,只需要判断 i d x idx idx 是否与 F [ i d x ] F[idx] F[idx] 相等;
  • 获取某个元素所在集合的编号(即获得结点所在树的根结点),只需要不断地根据数组 F F F 向上查询即可,当满足 i d x = = F [ i d x ] idx == F[idx] idx==F[idx] 时即可;
  • a , b a, b a,b 分别所在的集合进行合并,只需要将其所在的两个根结点合并即可。

例如给定已有的两个集合 { 1 , 2 , 3 , 4 } , { 5 , 6 , 7 } \{1,2,3,4\}, \{5, 6, 7\} {1,2,3,4},{5,6,7},其分别可以表示为树结构,合并时候,只需要合并两个根结点即可:
在这里插入图片描述

优化:

  • 并查集是一种树结构,因此可能会退化为很长的单链表,因此对于离根结点较远的结点,每次都需要不断地向上查询。一种简单的优化方法叫做路径压缩,即将每个结点直接与根结点相连,代码可以表示为:
// 查询编号为x的结点所在的树的根结点编号
int find(int x) {
    // 路径压缩,每次在递归时,每个结点的父结点直接指向根结点
    if(x != F[x]) F[x] = find(F[x]); 
    return F[x];
}
  • 另外还有按秩合并,即在对集合进行合并的时候,将深度小的子树合并到深度大的子树上

应用1:集合的合并与查询

一共有n个数,编号是1~n,最开始每个数各自在一个集合中。现在要进行m个操作,操作共有两种:
“M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
“Q a b”,询问编号为a和b的两个数是否在同一个集合中;
输入输出要求:第一行输入整数n和m。接下来m行,每行包含一个操作指令,指令为“M a b”或“Q a b”中的一种。

#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int F[N]; // 模拟并查集

// 初始化每个元素各自代表一个集合
void init(int n) {
    for(int i = 1; i <= n; i ++) F[i] = i;
}

// 寻找x所在的集合编号
int find(int x) {
    if(x != F[x]) F[x] = find(F[x]); // 路径压缩
    return F[x];
}

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    init(n);
    // 接下来m个操作
    while(m --) {
        char q;
        int a, b;
        scanf("%s%d%d", &q, &a, &b);
        if(q == 'M') F[find(a)] = find(b);
        else {
            if(find(a) == find(b)) printf("Yes\n");
            else printf("No\n");
        }
    }
    return 0;
}

应用2:查询连通子图的结点个数

给定一个包含n个点(编号为1~n)的无向图,初始时图中没有边。现在要进行m个操作,操作共有三种:
“C a b”,在点a和点b之间连一条边,a和b可能相等;
“Q1 a b”,询问点a和点b是否在同一个连通块中,a和b可能相等;
“Q2 a”,询问点a所在连通块中点的数量;
输入输出要求:第一行输入整数n和m。接下来m行,每行包含一个操作指令,指令为“C a b”,“Q1 a b”或“Q2 a”中的一种。

思路: 使用并查集建模,当两个结点连通时,则说明这两个结点所在的连通域(集合)进行合并。维护一个F数组,用于记录x元素的父结点F[x],当且仅当F[X]==X时说明其为根结点;维护一个数组cnt,用于保存每个元素作为根结点时其所包含所有结点的个数

#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int F[N]; // 并查集
int cnt[N]; // 用于记录每个结点作为根结点时集合的元素个数

// 初始化F,别忘了初始化cnt(每个元素自己代表一个连通域,元素为1)
void init(int n) {
    for(int i = 1; i <= n; i ++) F[i] = i, cnt[i] = 1;
}

int find(int x) {
    if(x != F[x]) F[x] = find(F[x]);
    return F[x];
}

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    init(n);
    while(m --) {
        char op[2];
        int a, b;
        scanf("%s", op);
        if(*op == 'C') {
            scanf("%d%d", &a, &b);
            int aa = find(a), bb = find(b);
            F[aa] = bb; // 将a所在的连通图与b所在的连通图进行合并
            if(aa != bb) cnt[bb] += cnt[aa]; // 如果a和b不在一个连通图里,则将a和b对应的元素个数累计起来
        }else if(op[1] == '1') {
            
            scanf("%d%d", &a, &b);
            if(find(a) == find(b)) printf("Yes\n");
            else printf("No\n");
        }else {
            scanf("%d", &a);
            printf("%d\n", cnt[find(a)]);
        }
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

华师数据学院·王嘉宁

$感谢支持$

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值