当前位置: 首页 > news >正文

蓝桥杯高频考点——并查集(心血之作)

并查集

  • TA Can Do What & why learning
    • what
    • why
  • 原理和结构
  • 路径压缩
  • 例题讲解
    • 题解
      • solution 1(50分)
      • solution 2(100分)
  • 按秩(树高)合并
  • 按大小合并

TA Can Do What & why learning

what

在这里插入图片描述

并查集主要是解决连通块的问题,例如对于上面的这个由5个村子若干条路构成的简易地图,如果问你1----->5是否是连通的,显然是的,那如果问你3——>5是否连通,显然是false,因为没有任何一条路能从3指向5,这不是很简单吗

why

那我们需要并查集干嘛?

好问题 如果这个图如果需要你自己构造而不是直接给你(动态构造集合关系),你很难或者说没法直接通过给的数据去画出每一个图的时候,并查集就能帮助我们迅速判断是否连通,而往往算法竞赛中的题目都是动态构造的(后面会附上例题),所以学习并查集是很有必要的,不然的话很大概率会超时
传统方式:
假设你有 100 万个村庄,每次新增一条路(动态添加边),如果每次都用 DFS/BFS 重新遍历整个图来判断两个村庄是否连通,时间复杂度会高达 O (N),效率极低。而并查集的find和union操作经过路径压缩和按秩合并优化后,时间复杂度几乎是O(1)

原理和结构

我们需要知道两个概念父节点和祖先,有一点像二叉树章节里的但又不太一样,尤其是对于祖先这个概念,
祖先代表的是:某一个节点不断去寻找他的父节点(递归),直到某一个节点的父节点是他本身(出口)

题外话:我在学习的时候感觉有点像 二叉树章节里面的 求最大深度问题,其实后来我想了一下是很正常的
毕竟树从某种意义上来说是特殊的图

言归正传:我们用一个pre数组来保存每个节点的父亲,我们的数组如下图所示,这里特意需要说的就是 1的父亲是他本身,所以1对应pre数组里头存储的父节点就是1

这句话怎么来理解呢:
其实可以把1看做是上面这个集合(1,2,4,5)的代表元素,也就是根节点,打个比方来说,这个1就是这个集合(家族)里面最年长的,就像家族的 “掌门人” 一样,没有比他更年长的父亲了,所以他的父亲就是他自己。
成员 1 作为这个家族的代表元素(根节点),就像是整个家族的源头,其他成员都是从他这里 “衍生” 出来的,就像一棵大树,成员 1 是树干最顶端的那个起始点,其他成员是从树干上长出来的树枝和树叶。

在这里插入图片描述

通过上面的例子大家应该会有一个比较基础的了解,但是在一开始每一个节点都是跟自己是一个连通关系(即指向自身)
在这里插入图片描述
就比如在添加1-2这条边的时候,我们就会让2的祖先指向1,在添加2-4这条边的时候,我们就需要注意,打个比方来说
由于2的“钱”已经交给1了,4想要找2要钱是找不到的,只能去找1了,体现在图中就是让4的祖先指向1,
总结来说就是:对于任何非根节点的相连都必须转换成它们各自的根节点相连

那现在我们已经可以用个构建的这个表来判断节点之间是否连通了,就比如1一直找,找到他的祖先是4,这个时间复杂度是O(N)的,但是这只是一次查询的情况如果有n次查询,那么时间复杂度就是O(n方),
那么有没有办法去优化它呢?
其实是有的——路径压缩(没学之前听着恐怖 其实是纸老虎)
在这里插入图片描述

路径压缩

我对于路径压缩的理解(可能不完全准确哈):
就是好比你是一个失忆的人,现在知道四个地方,A B C D,你通过不断探索知道了A是经过B C 能走到D的,也就是说你现在知道A到D是连通的,但是每过一段时间 如果一个人问你,你都需要重新走一遍,路径压缩好比就是,你用笔记本记下来了,A到D是通的并且D是终点,(这就引出了路径压缩的核心思想:通过直接记录最终结果(根节点),避免重复计算路径)
同理B,C到D也是通的,我们这里并不关心,A怎么到D,只关心从A能不能到D

压缩完成就是这个情况:
在这里插入图片描述

本质:
牺牲空间(存储父指针)换取时间(快速查询)
将树的高度 “压扁”,使得后续的find操作时间复杂度接近 O (1)

这里压扁的是啥?不就是我们寻找的路径path吗

例题讲解

在这里插入图片描述

题解

题目说的很直白就是让你用并查集的思路,其中有一个flag Z当它变化的时候,分别对应两种操作:1.合并(merge)2.判断(isCon)

solution 1(50分)

还有50%的测试点,数据非常大,即便关闭同步流,还是超时了吗,还是做不到吗,哈基霜你这家伙…
在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 9;
int pre[maxn];//存储父节点

int root(int x)
{
    return pre[x] == x ? x : pre[x] = root(pre[x]);
}
//一切操作都在根上
void Merge(int x, int y)
{
    pre[root(x)] = root(y);
}

bool isCon(int x, int y)
{
    return root(x) == root(y);
}

void init(int N)
{
    for (int i = 1; i <= N; ++i) pre[i] = i;
}

signed main()
{
    ios::sync_with_stdio(0);
    int N, M;
    cin >> N >> M;
    init(N);
    while (M--)
    {
        int Z, X, Y;
        cin >> Z >> X >> Y;
        if (Z == 1)
            Merge(X, Y);
        else if (Z == 2)
            printf("%c\n", isCon(X, Y) ? 'Y' : 'N');
    }
    return 0;
}

solution 2(100分)

之前代码仅实现路径压缩,未结合按树高或者大小合并,在数据量大 时,树可能因合并顺序不当变得高度很高,导致find操作时间复杂度退化为接近O(n),最终超时。而当前代码通过两种优化,将单次操作的时间复杂度优化至几乎常数级

#include <bits/stdc++.h>
using namespace std;

const int maxn = 1e5 + 9;
int pre[maxn];// 存储父节点
int siz[maxn];// 存储集合大小(模拟秩)

// 初始化并查集
void init(int N) {
    for (int i = 1; i <= N; ++i) {
        pre[i] = i;
        siz[i] = 1; // 初始时每个集合大小为 1
    }
}

// 查找根节点并路径压缩
int root(int x) {
    return pre[x] == x ? x : pre[x] = root(pre[x]);
}

// 合并两个集合,按集合大小(秩)优化
void Merge(int x, int y) {
    int rx = root(x);
    int ry = root(y);
    if (rx == ry) return; // 已在同一集合,无需合并
    
    // 保证将较小集合合并到较大集合
    if (siz[rx] > siz[ry]) swap(rx, ry); 
    pre[rx] = ry;
    if (siz[rx] == siz[ry]) siz[ry]++; // 若大小相等,合并后新集合秩+1
}

// 判断两个元素是否连通
bool isCon(int x, int y) {
    return root(x) == root(y);
}

signed main() {
    ios::sync_with_stdio(0);
    int N, M;
    cin >> N >> M;
    init(N);
    while (M--) {
        int Z, X, Y;
        cin >> Z >> X >> Y;
        if (Z == 1) {
            Merge(X, Y);
        } else if (Z == 2) {
            printf("%c\n", isCon(X, Y) ? 'Y' : 'N');
        }
    }
    return 0;
}

这就引出了下面的优化方法按秩合并和启发式合并

按秩(树高)合并

在这里插入图片描述
在这之前啊,我们是不是通过解决 斜树查找退化成链表的问题 学习过平衡二叉树的概念,
这个其实跟这里的按秩合并非常像,解决的问题也很像

比如说上面这张图,如果新增的边是4->3,
那拿3这个节点举例,那他走到根只需要两步,那如果是3->4,那就需要走3步,根节点向右倾斜了

对于一棵树 我们更希望它变成 矮墩墩 而不是长竹竿,所以就需要我们添加边的时候就需要进行比较 矮的指向高的
在这里插入图片描述

按大小合并

跟上面跟类似用小的集合指向大的集合 也就是比较少的点会多走一步

void Merge(int x, int y) {
    int rx = root(x);
    int ry = root(y);
    if (rx == ry) return; // 已在同一集合,无需合并
    
    // 保证将较小集合合并到较大集合
    if (siz[rx] > siz[ry]) swap(rx, ry); 
    pre[rx] = ry;
    if (siz[rx] == siz[ry]) siz[ry]++; // 若大小相等,合并后新集合秩+1
}

相关文章:

  • CI/CD(三) 安装nfs并指定k8s默认storageClass
  • 【C++】深入理解list迭代器的设计与实现
  • Java对象的hashcode
  • Fiddler抓包工具最快入门
  • 【005安卓开发方案调研】之Flutter+Dart技术开发安卓
  • 【PromptCoder + Trae】三分钟复刻 Spotify
  • 洛谷 P3228 [HNOI2013] 数列
  • 深度解读DeepSeek:开源周(Open Source Week)技术解读
  • 机器学习——KNN模型评价
  • 【用 Trace读源码】PlanAgent 执行流程
  • AMD公司
  • 附——教6
  • Windows faster whisper GUI-v0.8.5-开源版[AI支持超过100种语言的人声分离/声音转文本字幕]
  • 【Java篇】静动交融,内外有别:从静态方法到内部类的深度解析
  • STM32复位
  • 小米AX6000上安装tailscale
  • 【机器学习】机器学习工程实战-第2章 项目开始前
  • Lineageos 22.1(Android 15)制定应用强制横屏
  • Redis Cluster 详解
  • 维普AIGC降重方法有哪些?
  • 中华人民共和国和肯尼亚共和国关于打造新时代全天候中非命运共同体典范的联合声明
  • 创单次出舱活动时长世界纪录,一组数据盘点神十九乘组工作成果
  • 中国与柬埔寨签署多领域合作文件
  • 柬埔寨人民党中央外委会副主席:柬中友谊坚如钢铁,期待更多合作
  • 科普书单·新书|鸟界戏精观察报告
  • 话剧《门第》将开启全国巡演:聚焦牺牲、爱与付出