蓝桥杯并查集|路径压缩|合并优化|按秩合并|合根植物(C++)

2024-03-18 11:04

本文主要是介绍蓝桥杯并查集|路径压缩|合并优化|按秩合并|合根植物(C++),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

并查集

并查集是大量的树(单个节点也算是树)经过合并生成一系列家族森林的过程。
可以合并可以查询的集合的一种算法
可以查询哪个元素属于哪个集合
每个集合也就是每棵树都是由根节点确定,也可以理解为每个家族的族长就是根节点。
元素集合的代表性元素,也是第一个被分到这个集合的元素
举个数字和字母的例子如下。
初始森林: 图片描述
经过的一系列合并后的状态(不唯一,举个栗子):
A T都是字母集合,把A和T放在同一个集合,A作为T的“介绍人(父亲)“
U T都是字母集合,把U和T放在同一个集合,T作为U的“介绍人(父亲)“
1 2都是数字集合,把1和2放在同一个集合,1作为2的“介绍人(父亲)“
C B都是字母集合,把C和B放在同一个集合,B作为C的“介绍人(父亲)“
2 4都是数字集合,把2和4放在同一个集合,2作为4的“介绍人(父亲)“
C R都是字母集合,把C和R放在同一个集合,C作为R的“介绍人(父亲)“
8 4都是数字集合,把8和4放在同一个集合,4作为8的“介绍人(父亲)“
90 8都是数字集合,把90和8放在同一个集合,8作为90的“介绍人(父亲)“
R I都是字母集合,把R和I放在同一个集合,R作为I的“介绍人(父亲)“
图片描述
此时A节点的父示是自己,我们杯这种元素为该集合的祖先。用于代表该集合,如A代表的是字母集合。
相似的,1的的父亲是自己,所以1为该集合的祖先,用于代表该数字集合。
我们会发现一个问题,如果查询I属于那个集合,我们要向上跳6次,这非常消耗我们的时间。链式的并查集存储的查询效率太低,在多次查询和大量数据时必定TLE,所以我们需要进行优化,引入路径压缩
最终合并后的状态:
注:示意图的位置与存储物理位置无关,只代表逻辑关系。
图片描述
我们每次查询都将路径压缩,使得Fa指针指向祖先节点而不是父节点。在每次合并两个集合时先查询(可选,如果合并过多插入很少可以只在查询时合并)那么最后形成的并查集数据结构会如左图这种形式,此时有着高效的查询效率能够在0(1)的时间复杂度内返回属于什么集合。
这里说的是最后的查询,如果考虑路径压缩的过程时间复杂度应该是 O ( log ⁡ n ) O(\log n) O(logn)

并查集的存储结构

并查集采用数组表示整个森林,初始时每个森林的树根为自己。
C++ 存储与初始化:

# define Maxn 200// 假设所需数量为200int fa[Maxn+1]void init()
{for(int i =0;i<=Maxn; i++)fa[i]=i;  //i的父亲是i,指向自己
}
查询

一般用递归法实现对代表元素的查询:递归访问父节点,直至根节点(根节点的标志就是父节点是本身)。
根节点相同的两个元素属于同一个集合
所以判断 AB 是否属于一个集合直接判断 find(A)find(B)是否相同即可。
C++ 查询:

int find(int x)
{if(fa[x] == x)return x;elsereturn find(fa[x]);
}

我们这里有一个问题,当树的链很长时,比如:
图片描述
如果每次都查询最后一个,那么他就要经过多次递归,非常消耗时间,这时候我们就要引入路径压缩。

路径压缩

路径压缩是为了解决当树的高度过高的时候,提高查询时效的方法。
解决方式也很简单,在递归的同时将路径压缩,那么上面的图经过一次查询后的效果如下。
图片描述
C++ 查询带路径压缩:

int find(int x)
{if(x == fa[x])return x;else{fa[x] = find(fa[x]);//父节点设为根节点return fa[x];//返回父节点}
}
合并

合并的方式很简单,就是把一颗树的根节点设置为另一棵树的根节点即可。
还有一种方式是按秩合并,但是我们使用路径压缩时间复杂度就已经很低了,如果在引入 rank 相对会有些复杂。而且对于我们的使用路径压缩一种方式就已经足够。并且路径压缩和按秩合并一起使用时会影响 rank 准确性,所以我们采用普通的合并与优化后的查找即可。
图片描述
合并后:
图片描述
C++ 合并:


void merge(int i, int j)
{fa[find(i)] = find(j);//把i的祖先挂到j的祖先上面
}

当合并操作远大于查询操作,直接把2的祖先连到12上,这样就只用做一次查询就可以,只用查2的祖先就可以了
不能把2和12简单链接,这样2和2的祖先1之间的线就断了,如果简单地连接过去,就相当于2和12是一个集合,2和原来的不是一个集合了
合并优化:
还有一个优化是启发式合并,有很多的合并算法都叫启发式合并
这里讲其中一种常用的启发式合并。合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。
按照子树大小去合并,小的合并到大的,以免发生退化(以免树的树高变得很高)。
所以启发式合并的原理是在集合合并时将小的集合合并到大的集合里,也可以使 find 操作复杂度降低到 O(logn),在集合合并时还要增加一个更新集合大小的操作。

C++
void merge(int x,int y)//启发式合并
{x=find(x);  //先查xy=find(y);  //再查yif(x!=y)  //如果x和y不属于同一个集合的话{if(sz[x]<sz[y])  //判断子树的大小swap(x,y);  //如果x小于子树y的话,x是个小树,y是个大树sz[x]+=sz[y];   //把y的个数加到x上fa[y]=x;      //默认把y树挂到x上,把小树挂到大树上}
}

无论子树是高的还是矮的,子树越大
如果把大树的顶点变成了另一棵小的树的分支,实际上这样不好,反而如果把小数的顶点挂到大树的顶点下面,成为分支,这棵挂上去的小树会跟这棵大树下面的其他小树差不多,
如果是一般的并查集题目用路径压缩就可以了,当然两种优化都用的话复杂度可以降得更低。两种优化都使用的话单次操作的复杂度才是 O(α)

按秩合并

除此之外还有一个优化是按秩合并,其实这个合并和启发式合并是有些相似的。合并时,同样会因为选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。但这次我们选择是树的秩。秩的意思就是树的高度,按秩合并就是每次合并两个的时候判断两边的树高,小的合并到大的上面去这样。可以将深度较小的树连到另一棵,以免发生退化,也可以使find操作复杂度降低到O(logn),在集合合并时还要增加一个更新树高的操作。

void merge(int x,int y)//按秩合并
{x=find(x);y=find(y);if(x!=y){if(rk[x]>rk[y])  //谁的秩更高一点swap(x,y);  //如果x的秩大于y的秩,交换f[x]=y;  //将x挂到y上if(rk[x]==rk[y])  //如果两个的秩相同fk[y]++;  //秩++}
}

两种合并的时间复杂度接近都可以认为O(logn)如果使用路径压缩和合并算法时间复杂度同样为 O(a)

合根植物

题目链接
难度: 简单
标签: 并查集, 2017, 国赛
题目描述:
w 星球的一个种植园,被分成 m×n 个小格子(东西方向 m 行,南北方向 n 列)。每个格子里种了一株合根植物。
这种植物有个特点,它的根可能会沿着南北或东西方向伸展,从而与另一个格子的植物合成为一体。
如果我们告诉你哪些小格子间出现了连根现象,你能说出这个园中一共有多少株合根植物吗?
输入描述:
第一行,两个整数 m,n,用空格分开,表示格子的行数、列数(1≤m,n≤1000)。
接下来一行,一个整数 k (0≤k≤10^5 ),表示下面还有 k 行数据。
接下来 k 行,每行两个整数 a,b,表示编号为 a 的小格子和编号为 b 的小格子合根了。
格子的编号一行一行,从上到下,从左到右编号。
比如:5×4 的小格子,编号:

行列
1234
5678
9101112
13141516
17181920

输出描述:

输出植物数量。

输入输出样例:
示例:
输入:

5 4
16
2 3
1 5
5 9
4 8
7 8
9 10
10 11
11 12
10 14
12 16
14 18
17 18
15 19
19 20
9 13
13 17

输出:

5

样例图例如下:
图片描述
运行限制:

    最大运行时间:1s最大运行内存: 256M

题目解析:
这个题就是一个模板并查集的题目,每次合根就是一次 Merge
最后答案就是看有多少个根即可,那么就是看有多少个fa[x]=x即可。
直接按照题目编写即可,部分解析直接写进题目。
答案解析:
C++ 描述:

#include <bits/stdc++.h>
using namespace std;
# define Maxn 2000000// 假设所需数量为200
int fa[Maxn+1];  //把0避过去
void init()  //初始化
{for(int i =0; i<=Maxn; i++)fa[i]=i;
}
int find(int x)  //查询操作,带路径压缩
{if(x == fa[x])return x;else{fa[x] = find(fa[x]);//父节点设为根节点return fa[x];//返回父节点}
}
void merge(int i, int j)  //合根
{fa[find(i)] = find(j);
}int n,m; //n行,m列
int k; //k次合根int main()
{init();cin>>n>>m>>k;int a,b;for(int i=1; i<=k; i++){cin>>a>>b;merge(a,b); //合根}int ans=0;for(int i=1; i<=n*m; i++)  //从1判断到n*m,判断自己是不是自己的根{if(fa[i]==i) //找根节点{ans++;  //如果是自己的根,就代表能找到那棵代表的合根植物}}cout<<ans;  //输出有多少根植物return 0;
}

这篇关于蓝桥杯并查集|路径压缩|合并优化|按秩合并|合根植物(C++)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/822138

相关文章

Python如何调用指定路径的模块

《Python如何调用指定路径的模块》要在Python中调用指定路径的模块,可以使用sys.path.append,importlib.util.spec_from_file_location和exe... 目录一、sys.path.append() 方法1. 方法简介2. 使用示例3. 注意事项二、imp

C#如何调用C++库

《C#如何调用C++库》:本文主要介绍C#如何调用C++库方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录方法一:使用P/Invoke1. 导出C++函数2. 定义P/Invoke签名3. 调用C++函数方法二:使用C++/CLI作为桥接1. 创建C++/CL

使用Python实现矢量路径的压缩、解压与可视化

《使用Python实现矢量路径的压缩、解压与可视化》在图形设计和Web开发中,矢量路径数据的高效存储与传输至关重要,本文将通过一个Python示例,展示如何将复杂的矢量路径命令序列压缩为JSON格式,... 目录引言核心功能概述1. 路径命令解析2. 路径数据压缩3. 路径数据解压4. 可视化代码实现详解1

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类

SpringBoot使用GZIP压缩反回数据问题

《SpringBoot使用GZIP压缩反回数据问题》:本文主要介绍SpringBoot使用GZIP压缩反回数据问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录SpringBoot使用GZIP压缩反回数据1、初识gzip2、gzip是什么,可以干什么?3、Spr

MySQL索引的优化之LIKE模糊查询功能实现

《MySQL索引的优化之LIKE模糊查询功能实现》:本文主要介绍MySQL索引的优化之LIKE模糊查询功能实现,本文通过示例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录一、前缀匹配优化二、后缀匹配优化三、中间匹配优化四、覆盖索引优化五、减少查询范围六、避免通配符开头七、使用外部搜索引擎八、分

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

C++ vector的常见用法超详细讲解

《C++vector的常见用法超详细讲解》:本文主要介绍C++vector的常见用法,包括C++中vector容器的定义、初始化方法、访问元素、常用函数及其时间复杂度,通过代码介绍的非常详细,... 目录1、vector的定义2、vector常用初始化方法1、使编程用花括号直接赋值2、使用圆括号赋值3、ve