魔法之 pb_ds

点击查看更新日志

2024.8.25 写完文章
2024.10.8 更新少量内容
2024.10.18 重写了关于 hash表 的内容并 完善了整篇文章,修改了几处错误

pb_ds 简介 与 使用

Part0 pb_ds 简介

pb_ds 是一个基于策略的模板库

pb_ds 库封装了很多数据结构,比如哈希(Hash)表,平衡二叉树,字典树(Trie 树),堆(优先队列)等。

就像 vectorsetmap 一样,其组件均符合 STL 的相关接口规范。部分(如优先队列)包含 STL 内对应组件的所有功能,但比 STL 功能更多。

注意 pb_ds 只在使用 libstdc++ 为标准库的编译器下可以用。

由于 pb_ds 库的主要内容在以下划线开头的 __gnu_pbds 命名空间中,在 NOI 系列活动中的合规性一直没有确定。

2021 年 9 月 1 日,根据 《关于 NOI 系列活动中编程语言使用限制的补充说明》,允许使用以下划线开头的库函数或宏(但具有明确禁止操作的库函数和宏除外),在 NOI 系列活动中使用 pb_ds 库的合规性有了文件上的依据。

以上内容主要来自 OIwiki

pb_ds 包含的头文件有如下几个

#include <ext/pb_ds/assoc_container.hpp>  //容器库
#include <ext/pb_ds/tree_policy.hpp>      //各种树 
#include <ext/pb_ds/priority_queue.hpp>   //优先队列
#include <ext/pb_ds/hash_policy.hpp>       //哈希表
#include <list_update_policy.hpp>
#include <trie_policy.hpp>                //trie 树
#include <exception.hpp>
#include <list_update_policy.hpp>

Part1 堆(优先队列)

OIwiki

Part2 平衡树

所需头文件

#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/tree_policy.hpp>
__gnu_pbds ::tree<Key, 
		  Mapped, 
		  Cmp_Fn = std::less<Key>, 
 		  Tag = rb_tree_tag,
          Node_Update = null_tree_node_update,
          Allocator = std::allocator<char> >

Key : 储存的元素类型,如果想要存储多个相同的 Key 元素,则需要使用类似于 std::pair 和 struct 的方法,并配合使用 lower_bound 和 upper_bound 成员函数进行查找

Mapped: 映射规则(Mapped-Policy)类型,如果要指示关联容器是 集合,类似于存储元素在 std::set 中,此处填入 null_type,低版本 g++ 此处为 null_mapped_type;如果要指示关联容器是 带值的集合,类似于存储元素在 std::map 中,此处填入类似于 std::map<Key, Value> 的 Value 类型

Cmp_Fn: 关键字比较函子,例如 std::less

Tag: 选择使用何种底层数据结构类型,默认是 rb_tree_tag。__gnu_pbds 提供不同的三种平衡树,分别是:
rb_tree_tag:红黑树,一般使用这个,后两者的性能一般不如红黑树
splay_tree_tag:splay 树
ov_tree_tag:有序向量树,只是一个由 vector 实现的有序结构,类似于排序的 vector 来实现平衡树,性能取决于数据想不想卡你

Node_Update:用于更新节点的策略,默认使用 null_node_update,若要使用 order_of_key 和 find_by_order 方法,需要使用 tree_order_statistics_node_update

Allocator:空间分配器类型 一般不用管


点击查看 pb_ds 的平衡树 的成员函数

image
image

使用例子
构造一棵可以按元素排名查找的存储字符串的平衡树

#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
#include <bits/stdc++.h>
using namespace std;
__gnu_pbds::tree<
    std::string,                                  // key值
    __gnu_pbds::null_type,                        /*这是映射到的值,我们现在不需要,所以填null_type*/
    std::less<std::string>,                       /*比较规则,对于自己写的结构体,可以使用仿函数的形式*/
    __gnu_pbds::rb_tree_tag,                      /*树的类型 红黑树一般为最优*/
    __gnu_pbds::tree_order_statistics_node_update /*节点更新类型*/
    >  k;

int main()
{
    int cnt = 0;
    k.insert("man");
    k.insert("what");
    k.insert("can");
    k.insert("i");
    k.insert("say");
    // 树上元素 {"can","i","man","say","what"}
    auto it = k.find("can");
    cout << *it << endl;
    it = k.find_by_order(0);//按排名查找 key 值
    cout << *it << endl;
    it = k.find_by_order(1);
    cout << *it << endl;
    it = k.find_by_order(2);
    cout << *it << endl;
    //输出结果
    //can
    //i
    //man
    int f=k.order_of_key("man");//按key值查找排名
    cout<<f<<endl;
    //输出结果 2
    for(auto kk=k.begin();kk!=k.end();kk++)//用迭代器遍历
    {
        cout<<(*kk)<<" ";
    }
    //输出结果 can i man say what
    return 0;
}
点击查看 例子2 来自 $OIwiki$
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
typedef pair<int, int> pii;
#define pb push_back
#define mp make_pair
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
__gnu_pbds ::tree<pair<int, int>, __gnu_pbds::null_type, less<pair<int, int>>,
                  __gnu_pbds::rb_tree_tag,
                  __gnu_pbds::tree_order_statistics_node_update>
    trr;

int main()
{
    int cnt = 0;
    trr.insert(mp(1, cnt++));
    trr.insert(mp(5, cnt++));
    trr.insert(mp(4, cnt++));
    trr.insert(mp(3, cnt++));
    trr.insert(mp(2, cnt++));
    // 树上元素 {{1,0},{2,4},{3,3},{4,2},{5,1}}
    auto it = trr.lower_bound(mp(2, 0));
    trr.erase(it);
    // 树上元素 {{1,0},{3,3},{4,2},{5,1}}
    auto it2 = trr.find_by_order(1);
    cout << (*it2).first << endl;
    // 输出排名 0 1 2 3 中的排名 1 的元素的 first:1
    int pos = trr.order_of_key(*it2);
    cout << pos << endl;
    // 输出排名
    decltype(trr) newtr;
    trr.split(*it2, newtr);
    for (auto i = newtr.begin(); i != newtr.end(); ++i)
    {
        cout << (*i).first << ' ';
    }
    cout << endl;
    // {4,2},{5,1} 被放入新树
    trr.join(newtr);
    for (auto i = trr.begin(); i != trr.end(); ++i)
    {
        cout << (*i).first << ' ';
    }
    cout << endl;
    cout << newtr.size() << endl;
    // 将 newtr 树并入 trr 树,newtr 树被删除。
    return 0;
}

Part3 哈希表

这个 OIwiki 上倒是没有讲,
所需头文件

#include <ext/pb_ds/assoc_container.hpp>  
#include <ext/pb_ds/hash_policy.hpp> 

定义hash表

__gnu_pbds::cc_hash_table<Key,Value> xxx;//拉链法
__gnu_pbds::gp_hash_table<Key,Value> xxx;//探寻法(略快)

点击查看成员函数列表

有用的成员函数

使用
用法和 map 一样
[] 可以访问元素,若元素不存在会创建元素(和map一样)
还可用 .find() 来查找元素
但是 要注意的是
若找到了元素 .find() 会返回 pair<Key,Value> 并且需要使用 C 语言风格的 -> 而不是 . 来访问 firstsecond
若没有找到元素,会返回 .end()
也可以使用 .insert() 来插入元素

用途
可以很方便的用于 离散化映射 等各种场合而不用手写哈希(表)

具体的看示例代码

#include <bits/stdc++.h>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/hash_policy.hpp>
using namespace std;
__gnu_pbds::gp_hash_table<string, int>::iterator it;//是的你没看错,这个hash表有迭代器!但是我并不知道这有什么用
__gnu_pbds::gp_hash_table<string, int> hash_; // string -> int
string hh;
int n, a, m;
int main()
{
    ios::sync_with_stdio(false);
    cin >> n >> m;
    for (int yy = 1; yy <= n; yy++)
    {
        cin >> hh;
        hash_[hh] = yy; //可以像 map 一样直接访问元素
    }
    it = hash_.begin();//是的你没看错,这个hash表有迭代器!但是我并不知道这有什么用
    for (int ww = 1; ww <= m; ww++)
    {
        cin >> hh;
        //若哈希表中不存在查找的元素,.find() 会返回 .end()
        if (hash_.find(hh) != hash_.end()) //如果找的到这个元素
        {
            cout << hash_.find(hh)->first << " " << hash_.find(hh)->second << endl;
            //first对应的是Key , second 对应的是 Value
            //hash_.erase(hh);  删掉 hh 这里不做演示
        }
        else
        {
            cout << "No found!\n";
        }
    }
    return 0;
}

运行结果
输入

输出

时间复杂度: \(O(n)\)

优于 map 的 \(O(n \log n)\)

\({\color{red} 注意:}\) 哈希表有时会被 构造数据 卡出很多冲突从而导致运行速度低下(最劣时比 map 还慢)

而且 gp_hash_table 虽然一般情况下比较快,但是可以卡到 单次操作 \(O(n)\)
cc_hash_table 常数比较大,但是是严格 \(O(1)\)

另外,附上性能测评

点击查看性能测评

在 插入 \(1e6\) 个 长度为 \(1 \sim 100\) 的字符串并进行 \(1e6\) 次查询的数据下(输入高达 110MB)(已通过离线的方式消除了硬盘I/O的影响)
各种方法耗时如下

\[\left\{\begin{matrix} cc\_hash\_table(拉链法) & 1100ms \\ gp\_hash\_table(探寻法) & {\color{red} 900ms \sim 950ms}\\ map & 3100ms\\ unordered\_map & 960ms \sim 1000ms \end{matrix}\right.\]

在 插入 \({\color{red}1e7}\) 个 int 类型的数字 并进行 \({\color{red}1e7}\) 次查询的数据下(输入高达 330MB)(同上,已消除 I/O 影响)

各种方法耗时如下:

\[\left\{\begin{matrix} cc\_hash\_table(拉链法) & 3.9s \\ gp\_hash\_table(探寻法) & {\color{red}1000ms}\\ map & 25s\\ unordered\_map & 7.0s \end{matrix}\right.\]

在 插入 \({\color{red}1e6}\) 个 int 类型的数字 并进行 \({\color{red}1e7}\) 次查询的数据下(输入高达 140MB)(同上,已消除 I/O 影响)

各种方法耗时如下:

\[\left\{\begin{matrix} cc\_hash\_table(拉链法) & 587ms \\ gp\_hash\_table(探寻法) & {\color{red}320ms}\\ map & 9.4s\\ unordered\_map & 2.5s \end{matrix}\right.\]

但是,要注意的是,pb_ds 的哈希表和 unordered_map 由于本质上是哈希表,默认支持 C++ 自带的朴素数据类型,如 int,long long ,string 之类,对于自定义的结构体和非朴素数据类型需要自己重载 "==" 和 "()"(哈希函数) 运算符

而 map 由于本质上是红黑树,所以可以支持任意数据类型

应用

一种基于 pb_ds 的更好写且常数更小的离散化方式


可能我写的有问题的内容

Part0.5 前置之仿函数

pb_ds 的容器大多要提供一个比较函数,对于C++原生的类型,用 std::less<Key>std::greater<Key> 就好了

但是对于结构体等自定义的类型,则需要自己重载运算符或者使用仿函数了

那么讲一下仿函数
例子

struct mycmp
{
    public:
    bool operator()(const string a,const string b) const
    {
        return a<b;
    }
};
mycmp cmp;
sort(xxx,xxx,cmp);
posted @   sea-and-sky  阅读(603)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示