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

这篇博客详细介绍了PBDS库在算法竞赛中的应用,包括优先队列(配对堆、斐波那契堆等)、平衡树(红黑树、Treap等)的操作和性能,并提供了Dijkstra最短路径的实现。此外,还探讨了哈希表的使用,如cc_hash_table和gp_hash_table。博客指出PBDS库在某些场景下能提供比STL更好的效率,但学习成本较高。
摘要由CSDN通过智能技术生成

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.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【模板】普通平衡树
  • 维护的平衡树需要支持以下操作:
  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库

繁凡的博客

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值