pbds库学习笔记(优先队列、平衡树、哈希表)
UPD:22/2/10 增加了平衡树的例题洛谷P3369
;增添了前言部分笔者的看法
前言
- 这篇笔记的由来:一年前系统学过一遍了,但是没做笔记,以至于现在很多东西基本忘光了,于是今天又学了一遍(这像极了某人学
KMP
的样子) pbds
在笔者看来有很强的实用性,如STL
中set
、map
无法做到 Θ ( log n ) \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.second
while (!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 w
return ;
}
join合并
pb_ds
库的优先队列支持合并操作,pairing_heap
的合并时间复杂度是 O ( l o g n ) 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 0
3 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;
else
return 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 ( 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_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
使用,来降低常数
参考
参考: