蓝桥杯并查集|路径压缩|合并优化|按秩合并|合根植物(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

相关文章

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

如何高效移除C++关联容器中的元素

《如何高效移除C++关联容器中的元素》关联容器和顺序容器有着很大不同,关联容器中的元素是按照关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的,本文介绍了如何高效移除C+... 目录一、简介二、移除给定位置的元素三、移除与特定键值等价的元素四、移除满足特android定条件的元

Python获取C++中返回的char*字段的两种思路

《Python获取C++中返回的char*字段的两种思路》有时候需要获取C++函数中返回来的不定长的char*字符串,本文小编为大家找到了两种解决问题的思路,感兴趣的小伙伴可以跟随小编一起学习一下... 有时候需要获取C++函数中返回来的不定长的char*字符串,目前我找到两种解决问题的思路,具体实现如下:

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

Python通过模块化开发优化代码的技巧分享

《Python通过模块化开发优化代码的技巧分享》模块化开发就是把代码拆成一个个“零件”,该封装封装,该拆分拆分,下面小编就来和大家简单聊聊python如何用模块化开发进行代码优化吧... 目录什么是模块化开发如何拆分代码改进版:拆分成模块让模块更强大:使用 __init__.py你一定会遇到的问题模www.

Java图片压缩三种高效压缩方案详细解析

《Java图片压缩三种高效压缩方案详细解析》图片压缩通常涉及减少图片的尺寸缩放、调整图片的质量(针对JPEG、PNG等)、使用特定的算法来减少图片的数据量等,:本文主要介绍Java图片压缩三种高效... 目录一、基于OpenCV的智能尺寸压缩技术亮点:适用场景:二、JPEG质量参数压缩关键技术:压缩效果对比

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

SpringBoot首笔交易慢问题排查与优化方案

《SpringBoot首笔交易慢问题排查与优化方案》在我们的微服务项目中,遇到这样的问题:应用启动后,第一笔交易响应耗时高达4、5秒,而后续请求均能在毫秒级完成,这不仅触发监控告警,也极大影响了用户体... 目录问题背景排查步骤1. 日志分析2. 性能工具定位优化方案:提前预热各种资源1. Flowable