__gnu_pbds 中 tree 的扩展应用
在机房痛苦地调试了一整天后的一个初步成果。
如果不想看废话可以直接跳到最后一节:入土
本文语言标准为 C++20,C++11 及以上均能正常使用。
文中代码均有以下开头:
#include<bits/stdc++.h> #include<ext/pb_ds/assoc_container.hpp> #include<ext/pb_ds/tree_policy.hpp> using namespace std; using namespace __gnu_pbds;
本文中
前言——关于 tree
首先一般来说它的实现是红黑树,所以空间常数会偏大。大概是 Treap 的 1.5 倍。
同时为了实现迭代器和自定义模板等复杂的功能,时间常数也偏大,大概是 Treap 的 1.1~1.2 倍。
但是它的空间回收比较优秀,对于大部分平衡树相关的题目能有效解决。
同时其基础版本有相当简短的实现。
基础——建立与功能
建立
众所周知,__gnu_pbds
命名空间中的 tree
为我们提供了一个现成的平衡树。
一般来说这样定义一棵平衡树(以 int
型为例):
tree<int, null_type> tr;
第二项所填的 null_type
是指该树没有映射对象。
操作总览
此时它支持以下几种操作(常用):
操作 | 作用 | 效率 |
---|---|---|
insert(x) |
向 tr 中插入一个数 |
|
erase(...) |
删除 tr 中的元素。 |
|
lower_bound(x) |
查找第一个大于等于 |
|
upper_bound(x) |
查找第一个大于 |
其支持的标准 STL 容器函数有:size()
,empty()
,begin()
,end()
。
不支持 emplace()
。
操作概述
insert()
向 tree
中插入一个数
tr.insert(x);
因此还有一个扩展函数:copy_from_range()
。
tr.copy_from_range(itl, itr);
其功能是将 tr
中。时间复杂度
tree
中的数据是不可重的,和 set
一样。
erase()
erase()
常用的有两种语法:
erase(x)
:删除元素 。返回一个bool
类型的值。1
为删除成功,0
为删除失败。erase(it)
:删除迭代器 指向的元素。随后迭代器 会失效。但是树中指向别的元素的迭代器不变。请保证 是有效的,否则可能出现 Segmentation Fault。
lower_bound()
和 upper_bound()
用法与 set
基本一致。
在这里不在赘述。
begin()
和 end()
tree
是内部有序的,和 set
一样。
join()
和 split()
join(b)
:将
若值域相交,合并操作无法完成,并且抛出 join_error
错误。
慎用。
split(v,b)
:将大于
建议 split()
和 join()
搭配使用。
用来模拟 FHQ Treap
看完这一堆功能,我们发现这和 set
差不多的样子。
但是它的功能远强于 set
。
入门——tree
的实现以及 Cmp_Fn
的使用
实现
tree
类是这样定义的:
template<typename Key, typename Mapped, typename Cmp_Fn = std::less<Key>, typename Tag = rb_tree_tag, template<typename Node_CItr, typename Node_Itr, typename Cmp_Fn_, typename _Alloc_> class Node_Update = null_node_update, typename _Alloc = std::allocator<char> > class tree
我们可以在 Tag
这里更改它的实现。
内置的共有三种。
名称 | 实现方式 | 单次操作效率 |
---|---|---|
rb_tree_tag |
红黑树 | |
splay_tree_tag |
Splay | |
ov_tree_tag |
vector + sort |
以 洛谷 P3369 【模板】普通平衡树 为例。
ov_tree_tag
TLE on #9 #10 #11 #12 #14。直接毙掉。时间复杂度过高,基本过不去。
splay_tree_tag
TLE on #14。最好是不用。
rb_tree_tag
时间就很宽裕,最慢的点 #14 跑了 29ms。
所以还是用 rb_tree_tag
吧...
比较函子
如果 Cmp_Fn
由默认的 less<>
变为了 greater<>
,其函数的行为会发生变化。
lower_bound(x)
:查找第一个小于等于
upper_bound(x)
:查找第一个小于
之后的排名等操作就由从小到大变为了从大到小。
上手——映射与可重元素
映射
刚刚提到了一个东西:树的映射对象。
它其实和 map
容器基本相同。
建立
和 map
相似。
tree<int, int> tr;
其功能和 map<int, int> tr;
相同。
注意它没有 count()
函数。但是有 find()
函数。
操作
用法基本相同,只是少了 count()
函数。复杂度也与 map
相同。
在部分测试中,tree
的效率高于 map
;
有时前者又略低于后者。
可重操作(简单)
基本想法
如果为每个元素带一个时间戳 clockn
,那么原本的元素 pair<int, int>{x, clockn}
。
这时就可以实现元素的可重了。
这时需要对相应的操作进行修改。
实例
以 洛谷 P3369 【模板】普通平衡树 为例。
建立
tree<pair<int, int>, null_type> tr;
插入
插入一个数
tr.insert({x, ++clockn});
删除
删除一个数
auto it=tr.lower_bound({x,0}); if(it==tr.end()) return; tr.erase(it);
压行(保证数据合法):
tr.erase(tr.upper_bound({x,0}));
排名
下一节再说。
前驱
auto it=tr.lower_bound({x,0}); if(it==tr.begin()) return -INF; it--; return it->first;
压行(保证数据合法):
return (*--tr.upper_bound({x, 0})).first;
后继
auto it=tr.upper_bound({x, INF}); //INF 赋极大值即可。 if(it==tr.end()) return INF; return it->first;
压行(保证数据合法):
return (*tr.upper_bound({x, INF})).first;
进阶——排名
建立与操作
tree
类是这样定义的:
template<typename Key, typename Mapped, typename Cmp_Fn = std::less<Key>, typename Tag = rb_tree_tag, template<typename Node_CItr, typename Node_Itr, typename Cmp_Fn_, typename _Alloc_> class Node_Update = null_node_update, typename _Alloc = std::allocator<char> > class tree
如果我们将其中的 Node_Update
类由 null_node_update
更改为 tree_order_statistics_node_update
,那么它就能支持几个新的功能。
这时,它的定义应该长这样:(支持可重)
tree<pair<int, int>, null_type, less<pair<int, int> >, rb_tree_tag, tree_order_statistics_node_update> tr;
有以下的新操作:
操作 | 作用 | 效率 |
---|---|---|
order_of_key(x) |
在 tr 中查找 |
|
find_by_order(k) |
在 tr 中查找排名为 k 的元素。 |
注意:更改 Node_Update
类后会有子树大小的统计,所以会导致常数增大,效率降低。
实例
查找排名
tree
中的元素的排名是从
所以代码有所更改:
return tr.order_of_key({x,0})+1;
由排名查找元素
注意事项同上。
auto it=tr.find_by_order(k-1); if(it==tr.end()) return INF; return it->first;
压行(保证数据合法):
return (*tr.find_by_order(x-1)).first;
现在我们已经完成了平衡树的基本操作。
但是我们考虑如果一次性插入大量元素,比如一次加入
这就会导致时空超限。
入土——自定义
你都自定义类了为什么不写一棵 Treap 呢
前置知识:除了旋转之外的平衡树操作。
其实采用 tree
对我而言主要是白嫖 RBT 的平衡操作和它优秀的内存控制。
不然我为什么不手写
我们还是以 洛谷 P3369 【模板】普通平衡树 为例。
迭代器
我们通过 lower_bound()
等获得的迭代器是指向值的。
但是如果我们要得到指向节点的迭代器怎么办?
auto itn=it.m_p_nd;
之后对树结构的操作就与
自定义节点
首先我们定义节点类型 st
:
struct st { int num; mutable int cnt; bool operator<(const st &b) const {return num<b.num;} };
num
为节点的值,cnt
为该值的数目。
这里采用了和珂朵莉树差不多的处理方式,用 multable
以便于修改 cnt
的值。
重载 operator<()
以用于平衡树的更新。
自定义 Node_Update
类
模板
然后我们可以写一个 Node_Update
类,这个类的模板长这样:
template<class Node_CItr,class Node_Itr,class Cmp_Fn,class _Alloc> struct my_node_update {};
这样就定义了一个 my_node_update
类。
基本内容
但是这样的一个更新类还不能使用,需要再经过调整。
该类至少有以下内容:
template<class Node_CItr,class Node_Itr,class Cmp_Fn,class _Alloc> struct my_node_update { typedef int metadata_type; void operator()(Node_Itr it, Node_CItr end_it) { ; } virtual Node_CItr node_begin() const = 0; virtual Node_CItr node_end() const = 0; };
typedef int metadata_type;
声明了节点维护的额外信息,在这里维护的是子树的大小,所以为 int
。如果有像 Treap 维护序列用到的懒标记这类也可以放在这里。
operator()
更新子树
operator()
是平衡树的 push_up
操作,如果我们要更新子树大小,就会调用 operator()
。
对于我们的操作,就像这样:
void operator()(Node_Itr it, Node_CItr end_it) { Node_Itr itl=it.get_l_child(); Node_Itr itr=it.get_r_child(); int l=0,r=0; if(itl!=end_it) l=itl.get_metadata(); if(itr!=end_it) r=itr.get_metadata(); const_cast<int&>(it.get_metadata())=l+r+(*it)->cnt; }
get_l_child()
返回该迭代器指向的节点的左儿子的迭代器。
get_r_child()
同理。
end_it
是结尾迭代器,如果指向某节点的迭代器 end_it
,那么证明该节点不存在。
const_cast<int&>(it.get_metadata())
修改节点维护的额外信息。在这里就是左右子树大小之和加上该节点元素个数。
自定义操作
为了防止迭代器引用访问到非法内存,先定义一个 get()
函数,获取节点维护的额外信息。
int get(Node_CItr it) {return it==node_end()?0:it.get_metadata();}
查找排名
平衡树基本操作。
注意此时迭代器类型为 Node_CItr
。并且迭代器指向的是节点,节点指向的才是值。
int Rank(int x) { int ans=1; Node_CItr it=node_begin(); while(it!=node_end()) { Node_CItr itl=it.get_l_child(); Node_CItr itr=it.get_r_child(); if(x<=(*it)->num) it=itl; else ans+=(*it)->cnt+get(itl), it=itr; } return ans; }
由排名查找元素
平衡树基本操作。
注意事项同上。
int Find(int k) { Node_CItr it=node_begin(); while(it!=node_end()) { Node_CItr itl=it.get_l_child(); Node_CItr itr=it.get_r_child(); int lsiz=get(itl); if(k<=lsiz) it=itl; else if(k<=lsiz+(*it)->cnt) return (*it)->num; else {k-=lsiz+(*it)->cnt; it=itr;} } return -1; }
自此,自定义 Node_Update
类已经完成了,该考虑其他部分了。
自定义 Tree
类
构建
我选择直接继承 Tree
类,然后再搞事情。
struct RBT:tree<st,null_type,less<st>,rb_tree_tag,my_node_update> {};
自定义函数
更新额外信息
最抽象和恶心的地方来了。
由于每次修改都需要重新统计子树大小,但是对 cnt
的修改不会触发 push_up()
,也就导致了下面这份代码挂了。
void Insert(int x, int v=1) { auto it=lower_bound({x, 0}); if(it==end()||it->num!=x) insert({x, 1}); else it->cnt+=v; }
在 Insert()
过后父亲节点没有更新子树大小,所以排名相关的操作会挂掉。
每次 insert()
会调用 update_to_top()
操作来更新。
其实可以先 erase()
再 insert()
的,但是常数巨大
然后我上网找了很久找不到用法,文档里也没有...
然后自己试出来的。
void update(iterator x) {update_to_top(x.m_p_nd, (node_update*)this);}
这样就能正常更新子树大小了。
插入
能够插入任意数量的元素。
void Insert(int x, int v=1) { auto it=lower_bound({x, 0}); if(it==end()||it->num!=x) insert({x, 1}); else it->cnt+=v, update(it); }
原来的方式插入
现在时间复杂度为
删除
与插入基本相同。
void Erase(int x, int v=1) { auto it=lower_bound({x, 0}); if(it==end()||it->num!=x) return; else if(it->cnt<=v) erase(it); else it->cnt-=v, update(it); }
前驱
int Pre(int x) { auto it=lower_bound({x, 0}); it--; return it->num; }
后继
int Aft(int x) { auto it=upper_bound({x, 0}); return it->num; }
效率
在 洛谷 P6136 【模板】普通平衡树(数据加强版) 中测试。
均使用同样的快读快写,选择 C++20 O2。
tree_order_statistics_node_update
的 tree
:1252ms 67.63MB
自定义版的 tree
:1135ms 50.78MB
这可是红黑树空间常数大点怎么了
但是前者(1.38KB)明显短于后者(3.29KB)。
自行取舍。
额外操作
这些操作不建议手动调用。
否则会破坏 RBT 的性质导致时间复杂度退化。
旋转
rotate_left(itn)
:将左儿子旋到自己的位置。
rotate_right(itn)
:将右儿子旋到自己的位置。
rotate_parent(itn)
:将父亲节点旋到自己的位置。
本文作者:Jimmy-LEEE
本文链接:https://www.cnblogs.com/redacted-area/p/18063265
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步