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

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

HDFS—存储优化(纠删码)

纠删码原理 HDFS 默认情况下,一个文件有3个副本,这样提高了数据的可靠性,但也带来了2倍的冗余开销。 Hadoop3.x 引入了纠删码,采用计算的方式,可以节省约50%左右的存储空间。 此种方式节约了空间,但是会增加 cpu 的计算。 纠删码策略是给具体一个路径设置。所有往此路径下存储的文件,都会执行此策略。 默认只开启对 RS-6-3-1024k

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

hdu1565(状态压缩)

本人第一道ac的状态压缩dp,这题的数据非常水,很容易过 题意:在n*n的矩阵中选数字使得不存在任意两个数字相邻,求最大值 解题思路: 一、因为在1<<20中有很多状态是无效的,所以第一步是选择有效状态,存到cnt[]数组中 二、dp[i][j]表示到第i行的状态cnt[j]所能得到的最大值,状态转移方程dp[i][j] = max(dp[i][j],dp[i-1][k]) ,其中k满足c

hdu2241(二分+合并数组)

题意:判断是否存在a+b+c = x,a,b,c分别属于集合A,B,C 如果用暴力会超时,所以这里用到了数组合并,将b,c数组合并成d,d数组存的是b,c数组元素的和,然后对d数组进行二分就可以了 代码如下(附注释): #include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<que

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

hdu2544(单源最短路径)

模板题: //题意:求1到n的最短路径,模板题#include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<queue>#include<set>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#i

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�