目录

  • 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在笔者看来有很强的实用性,如STLsetmap无法做到Θ(log⁡n)\Theta(\log n)Θ(logn)求容器中的第k大元素(其中STL中的库函数nth_element是O(n)O(n)O(n)复杂度的)
  • 且比赛时,若遇到需要写比较简单的平衡树的情况,treapsplay都需要一定的时间才能写完其基本函数,而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【模板】普通平衡树

  • 维护的平衡树需要支持以下操作:
  1. 插入 x 数
  2. 删除 x 数(若有多个相同的数,因只删除一个)
  3. 查询 x 数的排名(排名定义为比当前数小的数的个数 +1)
  4. 查询排名为 x 的数
  5. 求 x的前驱(前驱定义为小于 x,且最大的数)
  6. 求 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:我们需要先查找到firstx的迭代器的位置,先找到大于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(log⁡n)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_boundupper_bound函数

此外,跟STLunorder_map哈希表对比,hash_table会更慢。但是unordered_map的哈希函数有缺陷,所以如果你在CF中使用,必然被HACK,所以unordered_map可以在除了CF以外的所以平台使用

  • 以下是洛谷中P1738 洛谷的文件夹这题,单单使用到“哈希”这一功能,三种容器的表现:

    • map566ms
    • gp_hash_table203ms
    • unordered_map241ms

总结

  • hash_table一般不常用?好像有map就足够了,某些CF的题目可以替代unordered_mapmap使用,来降低常数

参考

参考:

pbds初探(最短路)

学习一个pb_ds库

繁凡的博客

pbds库学习笔记(优先队列、平衡树、哈希表)相关推荐

  1. python标准库学习笔记

    原创:python标准库学习笔记 数据结构 bisect 模块里实现了一个向列表插入元素时也会顺便排序的算法. struct - 二进制数据结构:用途:在 Python 基本数据类型和二进制数据之间进 ...

  2. python xlwings 切片_Python xlwings库学习笔记(1)

    Python xlwings库学习笔记(1) Python是最近几年很火的编程语言,被办公自动化的宣传吸引入坑,办公自动化必然绕不开Excel的操作,能操作Excel的库有很多,例如: xlrd xl ...

  3. 深度学习常用python库学习笔记

    深度学习常用python库学习笔记 常用的4个库 一.Numpy库 1.数组的创建 (1)np.array() (2)np.zeros() (3)np.ones() (4)np.empty() (5) ...

  4. Huggingface Transformers库学习笔记(二):使用Transformers(上)(Using Transformers Part 1)

    前言 本部分是Transformer库的基础部分的上半部分,主要包括任务汇总.模型汇总和数据预处理三方面内容,由于许多模型我也不太了解,所以多为机器翻译得到,错误再所难免,内容仅供参考. Huggin ...

  5. STM32 HAL库学习笔记1-HAL库简介

    STM32 HAL库学习笔记1-HAL库简介 HAL库 SPL 库 和 HAL 库两者相互独立,互不兼容.几种库的比较如下 目前几种库对不同芯片的支持情况如下 ST 中文官网上有一篇<关于ST库 ...

  6. STM32 HAL库学习笔记4-SPI

    STM32 HAL库学习笔记4-SPI 前言 一.SPI协议简介 SPI物理层 SPI协议层 1.基本通讯过程 2. 通讯的起始和停止信号 3. 数据有效性 4. CPOL/CPHA 及通讯模式 二. ...

  7. Python_pygame库学习笔记(1):pygame的由来,特点以及模块简介

    Python_pygame库学习笔记 1 Pygame库的由来: Python适合用来开发游戏吗? Pygame的安装 Pygame模块简介 Pygame库的由来: 2000年,作者Pete Shin ...

  8. 7月16日数据科学库学习笔记——matplotlib 绘制散点图、条形图、直方图

    文章目录 前言 一.绘制散点图 二.绘制条形图 1.纵向条形图 2.横向条形图 三.绘制分组条形图 四.绘制直方图 1.plt.hist 方法 2.plt.bar 方法绘制直方图 前言 本文为7月16 ...

  9. colly爬虫库学习笔记

    colly爬虫库学习笔记 前言 稍微的学习了一下Go语言的基础知识(错误处理和协程通道这些还没看),想着能不能做点东西,突然想到自己当时学了python之后就是专门为了写爬虫(虽然后来也咕了,只会一个 ...

最新文章

  1. 灰度图像--图像分割 Scharr算子
  2. SpringBoot中@ControlAdvice的使用
  3. 经典!工业界深度推荐系统与CTR预估必读的论文汇总
  4. ARM处理器:开放者的逆袭
  5. 30个HTML标签,HTML常用标签的使用 --2019年8月30日
  6. php 如何快速判断一个数字属于什么范围
  7. linux分配内核,linux 内核分配算法
  8. 编译android 7.1 jdk版本,ubuntu14.04 安装 open-jdk-1.8,下载编译 android nougat 7.1.1
  9. spring mvc常用注解@Component @Controller @Service @Repository
  10. c++——block_type_is_valid怎么解决
  11. 站立会议01(冲刺2)
  12. python曲线和直线的交点_求直线与分段线性曲线的交点
  13. linux服务端下的c++ udp socket demo
  14. 目前见过最好的豆瓣电台第三方程序
  15. java教程51_java基础视频教程
  16. 【2020-10-28】DS12C887+驱动
  17. C++一周学习总结(2021/05/03)
  18. 项目成本管理__计划价值_挣值_实际成本三者关系与应对措施
  19. 新注册公众号没有留言评论功能怎么办?如何开通公众号留言功能?
  20. Java、JSP基于Web的师生互动系统的设计与实现

热门文章

  1. 网络看不到计算机和设备,局域网中设备查找不到怎么办
  2. 如何利用二维码分享会议资料
  3. android中的热更新
  4. 定义产品的一些考虑因素
  5. Java-swing坦克大战游戏
  6. 利用74ls147编码器实现病房呼叫系统
  7. 你觉得Siri是智障?这有三个套路让它赶超亚马逊Alexa | 分析
  8. 各种基底的PtSe2,SnS2,WSe2 (孤立晶粒、单层/多层连续薄膜)
  9. 基于网站html文件查找恶意外链
  10. 第 30 章 ADC—电压采集