热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

学习一个pb_ds库

pb_ds库大概是GNU对C++的一个扩展库,地位上必然是不如TR1这种基本成为官方标准的扩展库,但也是G++编译器默认附带的库。我在少数几个OJ上做了测试,CF和SPOJ都可以成

pb_ ds库大概是GNU对C ++的一个扩展库,地位上必然是不如TR1这种基本成为官方标准的扩展库,但也是G++编译器默认附带的库。我在少数几个OJ上做了测试,CF和SPOJ都可以成功编译,但POJ和HDU都找不到头文件令我大失所望(事实上经我测试,连TR1的扩展如unordered_map都无法支持,我估计boost库也全都无法使用)。其实我是寄希望于在ACM现场赛可以使用……
pb_ds库全称是Policy-Based Data Structures,可见是一些数据结构的集合,主要是Hash表,平衡二叉树、Trie树,优先队列(堆)等。英文官方文档传送门,里面有对各种数据结构的接口说明以及部分操作的性能测试。我现在只是试用了不多的几种结构,先写出来好了。

优先队列(Priority Queue)

我们知道在标准模板库(STL)中自带了priority_queue,在相当意义上替代了堆(Heap)的作用(我会说我已经好久没有手写堆了么)。但是在某些情况下,我们发现它并不能很好地应对某些需求,尤其是在一些图论算法中。比方说堆优化的Dijkstra,需要对优先队列中的元素的值进行修改;比方说一些题目需要对若干个堆进行合并。对于前者我们往往是记录了每个点对应的当前最短dist,然后将每次更新都入队(换言之存在重复入队),出队的值与当前dist不同的元素直接忽略掉(见这份板子中的代码),其实是存在时空浪费的;对于后者我们往往要手写左偏树或斜堆等数据结构来代替STL优先队列。此外在一些情况下,我们对STL自带优先队列的速度并不满意,甚至对手写堆的速度也不满意,如BZOJ3040就需要手写斐波那契堆或配对堆(恶心题还是要找中学生的题库啊),但斐波那契堆的难写大家也都是明白的。
所以,我们需要一个方便易用不需手打的替代品。pb _ ds库基本上满足了这些需求。pb _ ds库包含了配对堆(pairing_heap)、二叉堆(binary_heap)、二项堆(binomial_heap)、冗余计数二项堆(redundant-counter binomial_heap,没找到通用译名,故自行翻译)、经改良的斐波那契堆(thin_heap)。各项操作复杂度在这里都能看到,可见pairing_heap和thin_heap的理论效率非常之高,某些操作达到均摊O(1),正是上面那道题的上上之选。

#include 
#include
using namespace std;
using namespace __gnu_pbds;
int main() {
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
int n,x;
while(~scanf("%d",&n)) {
__gnu_pbds::priority_queue<int,greater<int> > pq;
while(n--) {
scanf("%d",&x);
pq.push(x);
}
int ans=0;
while(pq.size()>1) {
int x=pq.top();
pq.pop();
int y=pq.top();
pq.pop();
ans+=x+y;
pq.push(x+y);
}
print

首先pb_ds库统一使用__gnu_pbds命名空间,这和hash_map使用__gnu_cxx命名空间是类似的。声明部分比较奇怪,经我测试必须加上__gnu_pbds::前缀,我想这和我同时使用了std命名空间,导致编译器无法确认应该使用STL还是pb_ds库导致的。第二个参数是比较函数,和STL的priority_queue类似,默认是less,第三个参数代表使用的堆类型,默认为pairing_heap_tag。
这次测试每种堆都运行了5遍,记录时间的上下界;此外STL的priority_queue所用时间大致是5.4s~6.2s,我手写的堆(用数组模拟,使用位运算的普通二叉堆)所用时间大致是1.2s~1.4s。有个很奇怪的地方是pb_ds库的binary_heap的效率令人发指地低下,不明原因。总而言之,pairing_heap最具可用性。
此外pb_ds库的优先队列支持迭代器,声明方法是:

__gnu_pbds::priority_queue::point_iterator it;  

然后就可以按照一般的迭代器使用了;pb_ds库的push操作是有返回值的(与STL不同),返回的类型就是迭代器,这样用一个迭代器数组保存所有push进优先队列的元素的迭代器,就可以随时修改优先队列内部元素了。
此外,pb_ds库的优先队列支持合并操作,pairing_heap的合并时间复杂度是O(logn)的,可以说基本上完美代替了左偏树。合并的调用方式是:

__gnu_pbds::priority_queue a,b;  
// 对两优先队列进行一些操作
a.join(b);

此时优先队列b内所有元素就被合并进优先队列a中,且优先队列b被清空。
很遗憾我查看了文件,发现了public方法里有用于分离的split函数,但split函数需要两个参数,我始终没有找到第一个参数的含义是什么,也就暂时无法使用split函数了。何况对于堆而言,分离的意义并不是十分明确,实用性并不大,也就不再探究了。

平衡二叉树(Balanced Binary Tree)

其实这才是更让我感兴趣的东西。在刷题过程中我们经常使用到set和map,也知道它们的内部实现是红黑树,但我们显然无法按照操作平衡二叉树的方式操作它们,甚至连平衡二叉树最基本的求kth和求rank操作都无法在O(logn)内完成。好在pb_ds库给了一个并不是很强大,但在一些时候足够用的解决方案。
pb_ds库这次内置了红黑树(red-black tree)、伸展树(splay tree)和排序向量树(ordered-vector tree,没找到通用译名,故自行翻译)。这些封装好的树都支持插入(insert)、删除(erase)、求kth(find_by_order)、求rank(order_of_key)操作,于是我找来正好需要这四种操作的一道题

SPOJ3273进行效率测试。

#include  
#include
using namespace std;
using namespace __gnu_pbds;
int main() {
/**
* spoj3273, G++4.3.2, 4.66s
* tree,rb_tree_tag,tree_order_statistics_node_update> bbt;
*/


/**
* spoj3273, G++4.3.2, TLE
* tree,splay_tree_tag,tree_order_statistics_node_update> bbt;
*/


/**
* spoj3273, G++4.3.2, TLE
* tree,ov_tree_tag,tree_order_statistics_node_update> bbt;
*/

tree<int,null_type,less<int>,rb_tree_tag,tree_order_statistics_node_update> bbt;
int n,num;
char c;
scanf("%d",&n);
while(n--) {
scanf(" %c%d",&c,&num);
switch(c) {
case 'I':
bbt.insert(num);
break;
case 'D':
bbt.erase(num);
break;
case 'K':
num<=bbt.size()?printf("%d\n",*bbt.find_by_order(num-1)):puts("invalid");
break;
case 'C':
printf("%d\n",bbt.order_of_key(num));
break;
}
}
}

此外,我手写了Treap和Size Balanced Tree(代码在此)并经过测试,前者3.94s,后者4.06s,可见手写数据结构的速度总归要比封装好的要快,而封装好的实现中红黑树速度最快,这也是意料之中的。关于声明变量时的参数,第一个是键(key)的类型;第二个是值(value)的类型,null_type表示没有值,简单地理解就是表明这是set而不是map,注意SPOJ的G++版本稍旧(4.3.2),需要写成null_mapped_type才可以,我本地的G++版本为4.7.1;第三个表示比较函数,默认为less;第四个为平衡二叉树的类型,默认为红黑树rb_tree_tag;第五个代表元素的维护策略,只有当使用tree_order_statistics_node_update时才可以求kth和rank,此外还有null_tree_node_update(默认值)等,具体的区别在官方文档中有介绍(好吧我承认我没看懂= =)。
这里要注意的是,求kth(find_by_order)返回的是迭代器,求rank返回的是值,两者都是从0开始计算的。
此外,它们也支持合并(join)和分离(split)操作。用法如下。

tree a,b;  
// 对两平衡二叉树进行一些操作

a.join(b);
// 对两平衡二叉树进行一些操作

a.split(v,b);

这里需要进行一些解释。join操作的前提是两棵树的key的取值范围不相交,否则会抛出一个异常;合并后平衡二叉树b被清空。split操作中,v是一个与key类型相同的值,表示key小于等于v的元素属于平衡二叉树a,其余的属于平衡二叉树b,注意此时后者已经存有的元素将被清空。

红黑树(red-black tree)

定义一颗红黑树
int 关键字类型
null_type无映射(低版本g++为null_mapped_type)
less从小到大排序
rb_tree_tag 红黑树(splay_tree_tag)
tree_order_statistics_node_update结点更新
插入t.insert();
删除t.erase();
Rank:t.order_of_key();
第K值:t.find_by_order();
前驱:t.lower_bound();
后继t.upper_bound();
a.join(b)b并入a 前提是两棵树的key的取值范围不相交
a.split(v,b)key小于等于v的元素属于a,其余的属于b
T.lower_bound(x) >=x的min的迭代器
T.upper_bound((x) >x的min的迭代器
T.find_by_order(k) 有k个数比它小的数

一些题

1:COGS 2.旅行计划
题目地址:
http://cogs.pro/cogs/problem/problem.php?pid=2
求起点到所有点的最短路,可以用pb_ds中的priority_queue优化Dijkstra
代码:
http://cogs.pro/cogs/submit/code.php?id=149462
tips:
①声明格式必须是__gnu_pbds::priority_queue,因为STL里还有另一个priority_queue
②pb_ds包含的这些堆的迭代器是point_iterator而非iterator(见代码16行)
③push函数有返回值,返回指向插入元素的迭代器
④可以用Q.modify(iter,val)这样的用法(iter是迭代器,val是新值)来修改堆中元素,不必像用原先那种priority_queue时那样非常奇怪地新插元素再记used数组

2:BZOJ 3040.最短路(road)
题目地址:
http://www.lydsy.com:808/JudgeOnline/problem.php?id=3040
李煜东出的题,猥琐地卡堆优化Dijkstra的堆效率,一般堆过不去,出题人给的题解是用Fibonacci堆,我们可以用pb_ds中的配对堆(标签:pairing_heap_tag)或thin_heap(据说像是Fibonacci堆的玩意,标签:thin_heap_tag)通过
代码:
http://fayaa.com/code/view/28244/
用邻接表写的,不同于上一题的vector存边

3:COGS 1829/Tyvj 1728.普通平衡树
题目地址:
http://cogs.pro/cogs/problem/problem.php?pid=1829
需要实现一个支持点插入,点删除,查询rank,求第k大,求前驱/后继的数据结构
正好可以用pb_ds中的tree实现
pb_ds中tree的功能:set/map支持的所有功能,还包括查询rank,求第k大,按值分割(把值>=x的割到另一棵树中),合并值域不相交的两棵树
为了实现查询rank和求第k大,必须加上tree_order_statistics_node_update
代码:
http://cogs.pro/cogs/submit/code.php?id=149421
tips:
①null_mapped_type是用来占位的,如果它是一个真正的数据类型那就是map,如果像这样就是set。在一些编译器上(如COGS的编译器)必须用null_mapped_type,在另一些编译器上必须用null_type
②即使键类型是long long,你仍然可以强行把比较器定义成less或者greater,当然会出bug
③pb_ds不资瓷类似multiset/multimap的玩意,你必须自行搞一个偏移量。我的方法是把每个元素乘以2^20后加上偏移量,偏移量是‘当前重复了几次’,再用一个map来维护“每个元素出现了几次”,前驱后继直接在map上计算

4:COGS 314.[NOI2004]郁闷的出纳员
题目地址:
http://cogs.pro/cogs/problem/problem.php?pid=314
要求实现:点插入,求第k大,删除小于某个值的元素(需要记录删了多少)
“删除小于某个值的元素”用split实现,用greater比较,删除的命令类似于T.split(low,TE),其中TE是一棵无用的树
代码:
http://cogs.pro/cogs/submit/code.php?id=149424
tips:find_by_order和order_of_key中的“order”都是从0开始数的

5:COGS 194.[HAOI2008]排名系统
题目地址:
http://cogs.pro/cogs/problem/problem.php?pid=197
要求:点插入/更新,查询rank,求第k大,但元素是一个结构体(名字,得分,更新时间)
用到了pb_ds中的tree和hash_table
tree中存放结构体,为了比较我们需要自定义伪函数,所谓伪函数就是一个内部重载了()运算符的class,见代码中的Compare类。对于这道题,我们在Compare类中重载()运算符,实现“<”的功能,并将其放在上几题中greater的位置上。
需要注意,Compare类中重载()运算符时,一定要在函数体前加一个const(见代码24行花括号前的const),代表该函数不会改变传入的元素,否则会编译失败。
pb_ds中的hash_table作用跟map差不多,有cc_hash_table和gp_hash_table两种,前者是用链表存冲突元素,后者是用试探法解决冲突。
hash_table用法和map类似(广义数组),但注意它的迭代器叫point_iterator而非iterator,和priority_queue一样
代码:
http://cogs.pro/cogs/submit/code.php?id=149448


推荐阅读
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文讨论了一个数列求和问题,该数列按照一定规律生成。通过观察数列的规律,我们可以得出求解该问题的算法。具体算法为计算前n项i*f[i]的和,其中f[i]表示数列中有i个数字。根据参考的思路,我们可以将算法的时间复杂度控制在O(n),即计算到5e5即可满足1e9的要求。 ... [详细]
  • Java程序设计第4周学习总结及注释应用的开发笔记
    本文由编程笔记#小编为大家整理,主要介绍了201521123087《Java程序设计》第4周学习总结相关的知识,包括注释的应用和使用类的注释与方法的注释进行注释的方法,并在Eclipse中查看。摘要内容大约为150字,提供了一定的参考价值。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • 本文介绍了pack布局管理器在Perl/Tk中的使用方法及注意事项。通过调用pack()方法,可以控制部件在显示窗口中的位置和大小。同时,本文还提到了在使用pack布局管理器时,应注意将部件分组以便在水平和垂直方向上进行堆放。此外,还介绍了使用Frame部件或Toplevel部件来组织部件在窗口内的方法。最后,本文强调了在使用pack布局管理器时,应避免在中间切换到grid布局管理器,以免造成混乱。 ... [详细]
author-avatar
hobeson_861
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有