pbds库学习笔记(优先队列、平衡树、哈希表)
目录
- pbds库学习笔记(优先队列、平衡树、哈希表)
- 前言
- 概述
- priority_queue优先队列
- 概述
- 参数
- 堆的基本操作的函数
- 对比STL新增函数
- modify修改
- Dijkstra最短路径演示
- join合并
- 与自定义仿函数类的结合使用
- tree平衡树
- 概述
- 参数
- 基本操作的方法
- 对比STL中新增的函数
- find_by_order(k):求平衡树内排名为`k`的值是多少
- order_of_key(x):求x的排名
- lower_bound(x):求大于等于x的最小值
- upper_bound(x):求大于x的最小值
- join(b)合并
- split(v, b)分裂
- 例题
- 洛谷3369【模板】普通平衡树
- hash_table哈希表
- 概述
- 对比map的优缺点
- 总结
- 参考
pbds库学习笔记(优先队列、平衡树、哈希表)
UPD:22/2/10 增加了平衡树的例题洛谷P3369
;增添了前言部分笔者的看法
前言
- 这篇笔记的由来:一年前系统学过一遍了,但是没做笔记,以至于现在很多东西基本忘光了,于是今天又学了一遍(这像极了某人学
KMP
的样子) pbds
在笔者看来有很强的实用性,如STL
中set
、map
无法做到Θ(logn)\Theta(\log n)Θ(logn)求容器中的第k
大元素(其中STL中的库函数nth_element
是O(n)O(n)O(n)复杂度的)- 且比赛时,若遇到需要写比较简单的平衡树的情况,
treap
、splay
都需要一定的时间才能写完其基本函数,而pbds
仅需要一行定义声明即可 - 不过缺点也是有的,虽然
pbds
中支持你对其的splay
进行更改它的更新函数node_update
及其他函数,但是学习成本还是很高的,你需要搞懂去读它的源码,知道左儿子的变量名是什么,右儿子的变量名什么。若遇到这种情况,笔者认为还没有自己手敲一遍splay
来的快(仅指Code
的速度)。
概述
- 算法竞赛用到的一个非常强大的库
- 头文件和命名空间如下:
#include <bits/extc++.h>
using namespace __gnu_cxx;
using namespace __gnu_pbds;
记得使用编译器为
g++
,用chang++
会报错本文测试代码中,可能还有笔者的一些习惯性的
define
以及简化声明typedef
,大致如下:
#define el '\n'
#define cl putchar('\n');
#define pb push_back
#define x first
#define y second
#define rep(i, a, b) for (int i = (a); i <= (b); i++)typedef pair<int, int> PII;
priority_queue优先队列
概述
依旧是同
STL
中"反过来"的仿函数排序规则声明形式大致如下,为了跟
std
冲的名称冲突,故需要加上命名空间__gnu_pbds::priority_queue<PII, greater<PII>, pairing_heap_tag> q;
- 小顶堆
- 采用的为配对堆(各项效果最好)
- 跟
STL
中的声明不一样,STL中是第三个参数是仿函数类,而pbds中则是第二个参数是仿函数类,第三个参数是配对堆 - 对比
std
中的声明:priority<int, vector<int>, greater<int> >
参数
定义时不需要
vector<T>
共三个参数,分别为:
- 元素类型
- 仿函数类
- 堆的类型
其中仿函数类可以使用
greater<T>,less<T>
,也可以自己手写一个结构体myCmp
struct myCmp {//按照b降序bool operator()(node x, node y){//此时要将y想象成 排在优先队列前面的元素即可return x.b < y.b;} };
堆的类型有:
pairing_heap_tag
:配对堆thin_heap_tag
:斐波那契堆binomial_heap_tag
:二项堆binary_heap_tag
:二叉堆
一般采用配对堆
pairing_heap_tag
和斐波那契堆thin_heap_tag
效果比较好。
堆的基本操作的函数
push入堆
pop出堆
top堆顶
empty判空
size个数
clear清空
对比STL新增函数
modify修改
- 需要采用迭代器
point_iterator
才能修改 - 因为
pbds
库中的push
函数是有返回值的(与STL
不同),返回的类型就是迭代器,这样用一个迭代器数组保存所有push进优先队列的元素的迭代器,就可以随时修改优先队列内部元素了 - 声明的方式是:
__gnu_pbds::priority_queue::point_iterator it;
- 修改的函数为
q.modify(迭代器it, 新元素);
- 表示将迭代器
it
所指向的元素,修改为指定的值
- 表示将迭代器
- 当然初始化声明的时候,
it
的值还是NULL
,我们也可以用it
是否为NULL
来判断我们需要的某个元素是否在堆中。- 比如我们特定一个
it
指向某个元素,它入堆的时候it = q.push(元素);
,这个时候it
就不为NULL
了,为NULL
则表示它不在堆中
- 比如我们特定一个
Dijkstra最短路径演示
类型和变量声明
typedef pair<int, int > PII;
__gnu_pbds::priority_queue<PII, greater<PII>, pairing_heap_tag> q;
__gnu_pbds::priority_queue<PII, greater<PII>, pairing_heap_tag>::point_iterator its[N];
int dis[N];
vector<PII> g[N];
使用方法如下
void dijkstra(int sta)
{q.clear();rep(i, 1, n)dis[i] = INF;its[sta] = q.push({0, sta});dis[sta] = 0;int u;
#define v e.first
#define w e.secondwhile (!q.empty()){u = q.top().second;q.pop();for (auto e : g[u]){if(dis[v] > dis[u] + w){dis[v] = dis[u] + w;if(its[v] != NULL){q.modify(its[v], {dis[v], v});}else {its[v] = q.push({dis[v], v});}}}}
#undef v
#undef wreturn ;
}
join合并
pb_ds
库的优先队列支持合并操作,pairing_heap
的合并时间复杂度是O(logn)O(logn)O(logn)的,可以说基本上完美代替了左偏树- 可以将两个优先队列中的元素合并(无任何约束
- 使用方法为
a.join(b)
- 此时优先队列b内所有元素就被合并进优先队列a中,且优先队列b被清空
- 演示代码如下:
__gnu_pbds::priority_queue<node, myCmp, pairing_heap_tag> q1, q2;
__gnu_pbds::priority_queue<node, myCmp, pairing_heap_tag>::point_iterator its[N];
int main()
{q1.push({1, 2});q2.push({3, 4});q2.push({3, 6});q1.push({1, 5});q2.join(q1);cout << q2.size() << ' ' << q1.size() << el;cl;while(!q2.empty()){auto u = q2.top();q2.pop();cout << u.a << ' ' << u.b << el;}
}
输出如下
4 03 6
1 5
3 4
1 2
与自定义仿函数类的结合使用
- 仿函数类的作用:自定义容器内的排序规则
结构体声明如下:
struct node
{int a, b;
};
自定义仿函数类声明如下:
struct myCmp
{//按照b降序bool operator()(node x, node y){//此时要将y想象成 排在优先队列前面的元素即可return x.b < y.b;}
};
使用的代码大致如下:
int main()
{q.push({2, 9});q.push({1, 2});q.push({2, 4});while(!q.empty()){auto u = q.top();q.pop();cout << u.a << ' ' << u.b << el;}
}
输出如下:
2 9
2 4
1 2
tree平衡树
概述
pbds
的平衡树的常数稍微大了一点点pbds
的平衡树的速度会比手写的稍微慢一丢丢,但不会很多,测试出来大概慢1/6
左右(treap
)
参数
声明大致如下
tree<int, null_type, less<int>, rb_tree_tag, tree_order_statistics_node_update>
第一个参数: 键(key)的类型, int
第二个参数:值(value)的类型,null_type表示无映射,简单地理解就是表明这是set而不是map
这样写就行了,基本不会改
较低的g++版本写为null_mapped_type
第三个参数:仿函数类,表示比较规则,less<int>
第四个参数:平衡树的类型,rb_tree_tag,红黑树,这个会比其他的快一点,共以下的几种:
- rb_tree_tag
- splay_tree_tag
- ov_tree_tag
第五个参数:代表元素的维护策略,只有当使用tree_order_statistics_node_update时才可以求kth和rank,此外还有null_tree_node_update(默认值)等
基本操作的方法
以下中x表示关键字的类型
insert(x):插入x
erase(x):删除x
对比STL中新增的函数
以下中x表示关键字的类型,size表示平衡树内元素的个数
find_by_order(k):求平衡树内排名为k
的值是多少
find_by_order(k):求平衡树内排名为k
的值是多少
- 返回的为迭代器
it
- 其中
k
的值域为[0, size - 1]
,即0-index
,若不在这个范围,则直接返回end()
- 所以若以常人思维,你需要求解这棵平衡树里面排位为
k
的元素,需要调用*tr.find_by_order(k - 1)
- 但是这样调用有一个致命的缺陷,如果我们没有在平衡树没有找到这个值,那么就返回迭代器
tr.end()
,而*tr.end()
是个未知数,如果元素的类型为int
,则其为0
,万一我们平衡树里面存在值为0
的元素,就会混淆掉,所以我们还要特判一下,故我们可以将查询操作封装起来,演示如下: - 平衡树定义如下:
typedef tree<int, null_type, less<int>, rb_tree_tag, tree_order_statistics_node_update> Tree;
Tree tr;
- 演示代码如下,其中
getValue
对应的排名为1-index
:
template<typename T>
T getValue(int k)//查询平衡树里面排名第k的值
{auto it = tr.find_by_order(k - 1);if(it != tr.end())return *it;elsereturn INF;//此处需返回一个值表示k不合法
}
void testValue()
{tr.insert(11);tr.insert(22);cout << getValue<int>(1) << el;cout << getValue<int>(2) << el;cout << getValue<int>(3) << el;cout << getValue<int>(-1) << el;
}
- 输出结果:
11
22
1061109567
1061109567
order_of_key(x):求x的排名
order_of_key(x):求x的排名
- 返回的为整数
- 此处不同上面
find_by_order
的是,此处的x
不一定需要存在在平衡树中 - 即该方法的作用为,求解平衡树中严格比
x
小的元素的个数,即使仿函数(比较函数)myCmp(v, x)
的值为true
的元素的个数 - 所以最后求得的时候,我们需要再加上
1
才是答案 - 演示代码如下:
template<typename T>
int getOrder(T x)
{return tr.order_of_key(x) + 1;
}
void testOrder()
{tr.insert(11);tr.insert(22);cout << getOrder<int>(11) << el;cout << getOrder<int>(22) << el;cout << getOrder<int>(-1) << el;cout << getOrder<int>(15) << el;cout << getOrder<int>(25) << el;
}
- 输出结果如下:
1
2
1
2
3
lower_bound(x):求大于等于x的最小值
此处的大于等于,是基于仿函数
less<int>
下的,若有需要,也可以自定义仿函数myCmp
返回的为迭代器
没有找到时候,返回
end()
测试代码如下:
void testLower()
{tr.insert(11);tr.insert(22);cout << *tr.lower_bound(10) << el;cout << *tr.lower_bound(11) << el;cout << *tr.lower_bound(15) << el; cout << *tr.lower_bound(22) << el;cout << *tr.lower_bound(25) << el;
}
输出如下
11
11
22
22
0
upper_bound(x):求大于x的最小值
此处的大于,是基于仿函数
less<int>
下的,若有需要,也可以自定义仿函数myCmp
返回的为迭代器
没有找到时候,返回
end()
测试代码如下:
void testUpper()
{tr.insert(11);tr.insert(22);cout << *tr.upper_bound(10) << el;cout << *tr.upper_bound(11) << el;cout << *tr.upper_bound(15) << el; cout << *tr.upper_bound(22) << el;cout << *tr.upper_bound(25) << el;
}
- 输出结果为:
11
22
22
0
0
join(b)合并
join操作的前提是两棵树的key的取值范围不相交,否则会抛出一个异常
允许时抛出异常,编译阶段检测不出来的
合并后平衡二叉树b被清空
演示代码:
void testJoin()
{Tree a, b;a.insert(12);a.insert(15);b.insert(20);b.insert(25);a.join(b);cout << "a : {";for(int v : a){cout << v << ' ';}cout << "}" << el;cout << "b : {";for(int v : b){cout << v << ' ';}cout << "}" << el;
}
- 输出结果:
a : {12 15 20 25 }
b : {}
split(v, b)分裂
- split操作中,v是一个与key类型相同的值
- 表示key小于等于v的元素属于平衡二叉树a,其余的属于平衡二叉树b
- 注意此时后者已经存有的元素将被清空。
- 演示代码如下:
void testSplit()
{Tree a, b;a.insert(12);a.insert(15);a.insert(20);a.insert(25);a.split(15, b);cout << "a : {";for(int v : a){cout << v << ' ';}cout << "}" << el;cout << "b : {";for(int v : b){cout << v << ' ';}cout << "}" << el;
}
- 输出结果:
a : {12 15 }
b : {20 25 }
例题
洛谷3369【模板】普通平衡树
- 维护的平衡树需要支持以下操作:
- 插入 x 数
- 删除 x 数(若有多个相同的数,因只删除一个)
- 查询 x 数的排名(排名定义为比当前数小的数的个数 +1)
- 查询排名为 x 的数
- 求 x的前驱(前驱定义为小于 x,且最大的数)
- 求 x 的后继(后继定义为大于 x,且最小的数)
pbds
中的平衡树有一个缺陷,就是里面不能有重复的值,相当于一个set
,所以我们需要手动解决这个问题。- 网上用
pbds
过这题的方法一般都是左移20
位,然后加上一个特殊值 - 不过博主认为这种方法的通用性不强,且每次计算左移右移
20
位后,容易算错算混掉 - 那如果只用
int
如何解决这个问题呢?我们可以使用pair<int, int>
来解决这个问题,我们让其second
参数每个值都在[1,n]
之间且两两不同即可,即记录一个tr_clock
- 以下有约定:
#define mpa make_pair
#define el '\n'
typedef pair<int, int> PII;
typedef tree<PII, null_type, less<PII>, rb_tree_tag, tree_order_statistics_node_update> Tree;
Tree tr;
操作1:插入
mpa(x, tr_clock)
操作2:我们需要先查找到
first
为x
的迭代器的位置,先找到大于mpa(x, 0)
的迭代器位置,即是first
等于x
的节点了,然后我们erase(it)
删除这个迭代器即可操作3:我们求
x
的排名,即求有多少个数比mpa(x, 0)
小即可,调用我们封装好的getOrder<PII>(mpa(x, 0))
即可template<typename T> int getOrder(T x) {return tr.order_of_key(x) + 1; }
操作4:查询树中排名为
x
的数,其实此处的x
范围必然合法,故不可能不存在。我们只需查询调用我们的封装后的getValue
函数,然后输出所指向的节点的first
值即可template<typename T> T getValue(int k) {auto it = tr.find_by_order(k - 1);if(it != tr.end())return *it;else return mpa(INF, 0); }
调用代码如下:
case 4: {auto v = getValue<PII>(x);cout << v.first << el;break; }
操作5:找到第一个大于
mpa(x, 0)
的位置,然后迭代器自减,即是x
前驱的位置操作6:然后找到第一个大于
mpa(x, INF)
的,即是x
的后继最后,全部代码如下:
int T, n, m;
#define mpa make_pair
#define el '\n'
typedef pair<int, int> PII;
typedef tree<PII, null_type, less<PII>, rb_tree_tag, tree_order_statistics_node_update> Tree;
Tree tr;
int tr_clock;
template<typename T>
int getOrder(T x)
{return tr.order_of_key(x) + 1;
}template<typename T>
T getValue(int k)
{auto it = tr.find_by_order(k - 1);if(it != tr.end())return *it;else return mpa(INF, 0);
}
int main()
{read(n);int op, x;while (n--){read(op), read(x);++ tr_clock;switch (op){case 1:{tr.insert(mpa(x, tr_clock));break;}case 2:{auto it = tr.upper_bound(mpa(x, 0));tr.erase(it);break;}case 3:{int k = getOrder<PII>(mpa(x, 0));cout << k << el;break;}case 4:{auto v = getValue<PII>(x);cout << v.first << el;break;}case 5:{auto it = tr.upper_bound(mpa(x, 0));-- it;auto v = *it;cout << v.first << el;break;}case 6:{auto v = *tr.upper_bound(mpa(x, INF));cout << v.first << el;break;}}}
}
hash_table哈希表
概述
- 拥有两种哈希表
cc_hash_table<string, int> mp1
拉链法gp_hash_table<string, int> mp2
查探法(快一些,以后都用这个)
- 实测下来,均摊Θ(1)\Theta(1)Θ(1)的
对比map的优缺点
首先,map是内嵌红黑树,所以内部也是平衡树,均摊操作是O(logn)O(\log n)O(logn)的
只用到映射哈希的时候,hash_table更快
hash_table不支持排序,即不对内部元素排序,
即遍历它的时候是无序的
(也不是按照插入的顺序,也不是排序后的顺序)
演示代码如下:
gp_hash_table<string, int> hs; int main() {for(int i = 10;i >= 1; i -- ){char s[15];itoa(i, s, 12); //将i转换为12进制的数字后存放在s中// string str(s);hs[string(s)] = 1;}for(pair<string ,int> it : hs){cout << it.first << ' ' << it.second << '\n';} }
输出结果:
3 1 4 1 8 1 6 1 a 1 5 1 9 1 2 1 7 1 1 1
hash_table不支持
lower_bound
和upper_bound
函数
此外,跟STL
中unorder_map
哈希表对比,hash_table
会更慢。但是unordered_map
的哈希函数有缺陷,所以如果你在CF
中使用,必然被HACK
,所以unordered_map
可以在除了CF
以外的所以平台使用
- 以下是洛谷中
P1738 洛谷的文件夹
这题,单单使用到“哈希”这一功能,三种容器的表现:map
:566ms
gp_hash_table
:203ms
unordered_map
:241ms
总结
hash_table
一般不常用?好像有map
就足够了,某些CF
的题目可以替代unordered_map
和map
使用,来降低常数
参考
参考:
pbds初探(最短路)
学习一个pb_ds库
繁凡的博客
pbds库学习笔记(优先队列、平衡树、哈希表)相关推荐
- python标准库学习笔记
原创:python标准库学习笔记 数据结构 bisect 模块里实现了一个向列表插入元素时也会顺便排序的算法. struct - 二进制数据结构:用途:在 Python 基本数据类型和二进制数据之间进 ...
- python xlwings 切片_Python xlwings库学习笔记(1)
Python xlwings库学习笔记(1) Python是最近几年很火的编程语言,被办公自动化的宣传吸引入坑,办公自动化必然绕不开Excel的操作,能操作Excel的库有很多,例如: xlrd xl ...
- 深度学习常用python库学习笔记
深度学习常用python库学习笔记 常用的4个库 一.Numpy库 1.数组的创建 (1)np.array() (2)np.zeros() (3)np.ones() (4)np.empty() (5) ...
- Huggingface Transformers库学习笔记(二):使用Transformers(上)(Using Transformers Part 1)
前言 本部分是Transformer库的基础部分的上半部分,主要包括任务汇总.模型汇总和数据预处理三方面内容,由于许多模型我也不太了解,所以多为机器翻译得到,错误再所难免,内容仅供参考. Huggin ...
- STM32 HAL库学习笔记1-HAL库简介
STM32 HAL库学习笔记1-HAL库简介 HAL库 SPL 库 和 HAL 库两者相互独立,互不兼容.几种库的比较如下 目前几种库对不同芯片的支持情况如下 ST 中文官网上有一篇<关于ST库 ...
- STM32 HAL库学习笔记4-SPI
STM32 HAL库学习笔记4-SPI 前言 一.SPI协议简介 SPI物理层 SPI协议层 1.基本通讯过程 2. 通讯的起始和停止信号 3. 数据有效性 4. CPOL/CPHA 及通讯模式 二. ...
- Python_pygame库学习笔记(1):pygame的由来,特点以及模块简介
Python_pygame库学习笔记 1 Pygame库的由来: Python适合用来开发游戏吗? Pygame的安装 Pygame模块简介 Pygame库的由来: 2000年,作者Pete Shin ...
- 7月16日数据科学库学习笔记——matplotlib 绘制散点图、条形图、直方图
文章目录 前言 一.绘制散点图 二.绘制条形图 1.纵向条形图 2.横向条形图 三.绘制分组条形图 四.绘制直方图 1.plt.hist 方法 2.plt.bar 方法绘制直方图 前言 本文为7月16 ...
- colly爬虫库学习笔记
colly爬虫库学习笔记 前言 稍微的学习了一下Go语言的基础知识(错误处理和协程通道这些还没看),想着能不能做点东西,突然想到自己当时学了python之后就是专门为了写爬虫(虽然后来也咕了,只会一个 ...
最新文章
- 灰度图像--图像分割 Scharr算子
- SpringBoot中@ControlAdvice的使用
- 经典!工业界深度推荐系统与CTR预估必读的论文汇总
- ARM处理器:开放者的逆袭
- 30个HTML标签,HTML常用标签的使用 --2019年8月30日
- php 如何快速判断一个数字属于什么范围
- linux分配内核,linux 内核分配算法
- 编译android 7.1 jdk版本,ubuntu14.04 安装 open-jdk-1.8,下载编译 android nougat 7.1.1
- spring mvc常用注解@Component @Controller @Service @Repository
- c++——block_type_is_valid怎么解决
- 站立会议01(冲刺2)
- python曲线和直线的交点_求直线与分段线性曲线的交点
- linux服务端下的c++ udp socket demo
- 目前见过最好的豆瓣电台第三方程序
- java教程51_java基础视频教程
- 【2020-10-28】DS12C887+驱动
- C++一周学习总结(2021/05/03)
- 项目成本管理__计划价值_挣值_实际成本三者关系与应对措施
- 新注册公众号没有留言评论功能怎么办?如何开通公众号留言功能?
- Java、JSP基于Web的师生互动系统的设计与实现