__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;
本文中 \(x\) 代表一种元素,\(\textrm{it}\) 代表迭代器,\(n\) 一般为树的大小。
前言——关于 tree
首先一般来说它的实现是红黑树,所以空间常数会偏大。大概是 Treap 的 1.5 倍。
同时为了实现迭代器和自定义模板等复杂的功能,时间常数也偏大,大概是 Treap 的 1.1~1.2 倍。
但是它的空间回收比较优秀,对于大部分平衡树相关的题目能有效解决。
同时其基础版本有相当简短的实现。
基础——建立与功能
建立
众所周知,__gnu_pbds 命名空间中的 tree 为我们提供了一个现成的平衡树。
一般来说这样定义一棵平衡树(以 int 型为例):
tree<int, null_type> tr;
第二项所填的 null_type 是指该树没有映射对象。
操作总览
此时它支持以下几种操作(常用):
| 操作 | 作用 | 效率 |
|---|---|---|
insert(x) |
向 tr 中插入一个数 \(x\)。 |
\(O(\log{n})\) |
erase(...) |
删除 tr 中的元素。 |
\(O(\log{n})\) |
lower_bound(x) |
查找第一个大于等于 \(x\) 的元素。 | \(O(\log{n})\) |
upper_bound(x) |
查找第一个大于 \(x\) 的元素。 | \(O(\log{n})\) |
其支持的标准 STL 容器函数有:size(),empty(),begin(),end()。
不支持 emplace()。
操作概述
insert()
向 tree 中插入一个数 \(x\)。
tr.insert(x);
因此还有一个扩展函数:copy_from_range()。
tr.copy_from_range(itl, itr);
其功能是将 \([\textrm{it}_l, \textrm{it}_r)\) 间的数据插入 tr 中。时间复杂度 \(O(T\log n)\)。\(T\) 是插入元素的个数。
tree 中的数据是不可重的,和 set 一样。
erase()
erase() 常用的有两种语法:
erase(x):删除元素 \(x\)。返回一个bool类型的值。1为删除成功,0为删除失败。erase(it):删除迭代器 \(\textrm{it}\) 指向的元素。随后迭代器 \(\textrm{it}\) 会失效。但是树中指向别的元素的迭代器不变。请保证 \(\textrm{it}\) 是有效的,否则可能出现 Segmentation Fault。
lower_bound() 和 upper_bound()
用法与 set 基本一致。
在这里不在赘述。
begin() 和 end()
tree 是内部有序的,和 set 一样。
join() 和 split()
join(b):将 \(b\) 树并入本树,要求是元素类型相同,且值域互相不交。
若值域相交,合并操作无法完成,并且抛出 join_error 错误。
慎用。
split(v,b):将大于 \(v\) 的元素移到 \(b\) 树中,\(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 |
红黑树 | \(O(\log{n})\) |
splay_tree_tag |
Splay | \(O(\log{n})\) |
ov_tree_tag |
vector + sort | \(O(n)\) |
以 洛谷 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):查找第一个小于等于 \(x\) 的元素。
upper_bound(x):查找第一个小于 \(x\) 的元素。
之后的排名等操作就由从小到大变为了从大到小。
上手——映射与可重元素
映射
刚刚提到了一个东西:树的映射对象。
它其实和 map 容器基本相同。
建立
和 map 相似。
tree<int, int> tr;
其功能和 map<int, int> tr; 相同。
注意它没有 count() 函数。但是有 find() 函数。
操作
用法基本相同,只是少了 count() 函数。复杂度也与 map 相同。
在部分测试中,tree 的效率高于 map;
有时前者又略低于后者。
可重操作(简单)
基本想法
如果为每个元素带一个时间戳 clockn,那么原本的元素 \(x\) 变为了一个 pair<int, int>{x, clockn}。
这时就可以实现元素的可重了。
这时需要对相应的操作进行修改。
实例
以 洛谷 P3369 【模板】普通平衡树 为例。
建立
tree<pair<int, int>, null_type> tr;
插入
插入一个数 \(x\)。
tr.insert({x, ++clockn});
删除
删除一个数 \(x\)(若有多个相同的数,应只删除一个)。
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 中查找 \(x\) 的排名。 |
\(O(\log{n})\) |
find_by_order(k) |
在 tr 中查找排名为 k 的元素。 |
\(O(\log{n})\) |
注意:更改 Node_Update 类后会有子树大小的统计,所以会导致常数增大,效率降低。
实例
查找排名
tree 中的元素的排名是从 \(0\) 开始的。务必注意。
所以代码有所更改:
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;
现在我们已经完成了平衡树的基本操作。
但是我们考虑如果一次性插入大量元素,比如一次加入 \(114514\) 个 \(1919810\)。
这就会导致时空超限。
入土——自定义
你都自定义类了为什么不写一棵 Treap 呢
前置知识:除了旋转之外的平衡树操作。
其实采用 tree 对我而言主要是白嫖 RBT 的平衡操作和它优秀的内存控制。
不然我为什么不手写
我们还是以 洛谷 P3369 【模板】普通平衡树 为例。
迭代器
我们通过 lower_bound() 等获得的迭代器是指向值的。
但是如果我们要得到指向节点的迭代器怎么办?
auto itn=it.m_p_nd;
\(\textrm{itn}\) 就是指向节点的迭代器。
之后对树结构的操作就与 \(\textrm{itn}\) 相关。
自定义节点
首先我们定义节点类型 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 是结尾迭代器,如果指向某节点的迭代器 \(\textrm{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);
}
原来的方式插入 \(v\) 个值时间复杂度为 \(O(v \log{n})\),空间复杂度为 \(O(v)\)。
现在时间复杂度为 \(O(\log{n})\),空间复杂度为 \(O(1)\)。
删除
与插入基本相同。
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 的性质导致时间复杂度退化。
\(\textrm{itn}\) 是指向节点的迭代器。
旋转
rotate_left(itn):将左儿子旋到自己的位置。
rotate_right(itn):将右儿子旋到自己的位置。
rotate_parent(itn):将父亲节点旋到自己的位置。

浙公网安备 33010602011771号