珂朵莉树(odt)学习笔记
代码中可能带了一些个人缺省源里的东西。
前置知识
std::set。
简述
珂朵莉树(Chtholly Tree),又名老司机树(Old Driver Tree, ODT),是一种非常暴力的维护序列信息的数据结构。
其通过维护值相同的连续段来保证效率,在特殊构造的数据下会退化为普通暴力算法。
其起源于CF896C。
引入
你需要维护一个序列,并支持如下几种操作:
- 给区间 \([l,r]\) 内的所有数字加上 \(x\)。
- 将区间 \([l,r]\) 内的所有数字赋值为 \(x\)。
- 求区间 \([l,r]\) 内所有数字中第 \(x\) 小的数字(重复数字多次计算)。
- 求 \(\sum \limits_{i=l}^r a^{x}_i \bmod y\) 的值。
题目保证数据随机。
前三个操作使用主席树可以解决,但并不能解决第四个。于是就有了珂朵莉树。
基本操作
定义
珂朵莉树会把序列分为若干连续的段。
struct node
{
int l,r;
mutable long long val;
//mutable修饰之后,就可以直接在set中修改该变量的值而不是取出修改后再重新插入
node (int L,int R=-1,long long v=0)
{
l=L,r=R,val=v;
}
bool operator < (const node&a) const
{
return l<a.l;
}
};
sd set<node> odt;
分割(split)
给出区间 \([l,r]\) 和 \(pos\in [l,r]\),将区间分割为 \([l,pos-1]\) 和 \([pos,r]\)。
实现过程:
- 找到含有 \(pos\) 位置的区间。
- 如果 \(pos=l\),则无需分割。否则删除原区间,插入两个新区间。
复杂度单 \(\log\)。
auto split(int pos)
{
auto it=odt.lower_bound(node(pos));//找到左端点不小于 pos 的区间
if(it!=odt.end()&&it->l==pos)return it;//pos 是区间左端点时无需分割
it--;//pos 一定在前一个区间中
int l=it->l,r=it->r,val=it->val;
odt.erase(it);//删除原来的区间
odt.insert(node(l,pos-1,val));
return odt.insert(node(pos,r,val)).X;//插入两个新区间
//这里的返回值是后半段区间对应的迭代器
}
分割操作后保证两个区间无交,这是保证正确性的前提。
合并(assign)
给出区间 \([l,r]\) 以及 \(val\),将 \([l,r]\) 覆盖为 \(val\),即将 \([l,r]\) 合并成一个区间。
实现过程:
- 找到 \(l,r\) 所在区间。分裂掉。
- 删除 \([l,r]\) 原本的若干个小区间。
- 插入一个大区间。
注意,分裂时应先分裂右端点再分裂左端点,否则可能导致 RE。
原因?
因为如果先
split(l),返回的迭代器会位于所对应的区间以 \(l\) 为左端点,此时如果 \(r\) 也在这个节点内,就会导致split(l)返回的迭代器被erase掉,导致 RE。
void assign(int l,int r,int val)
{
auto itr=split(r+1),itl=split(l);
odt.erase(itl,itr);//删除[itl,itr)区间内的所有元素(注意左闭右开区间)
odt.insert(node(l,r,val));//将原来的诸多小区间用一个大区间代替
}
因为odt树的本质是暴力处理每个段,所以合并操作是用来保证复杂度的。
其他操作
所有区间操作都可以套这样的一个模板:
- 先
split右端点,再split左端点,获得两个端点(左闭右开)的迭代器。 - 对两个端点之间的所有区间暴力更改。
代码差不多长这样:
void update(int l,int r)
{
auto itr=split(r+1),itl=split(l);
for(auto it=itl;it!=itr;it++) ;//do sth.
}
比如区间加:
void add(int l,int r,long long val)
{
auto itr=split(r+1),itl=split(l);
for(auto it=itl;it!=itr;it++) *it.val+=val;
}
比如区间第 \(k\) 小,暴力取出所有数排序:
long long kth(int l,int r,int k)
{
sd vector<pii> a;
auto itr=split(r+1),itl=split(l);
for(auto it=itl;it!=itr;it++)
a.push_back(pii{*it.val,(*it.r)-(*it.l)+1});
sort(a.begin(),a.end());
for(auto it=a.begin();it!=a.end();it++)
{
k-=*it.Y;
if(k<=0) return it->first;
}
return -1;
}
比如是区间幂次和,暴力取出区间内所有段累加求和:
long long sum(int l,int r,int x,int y)
{
long long ans=0;
auto itr=split(r+1),itl=split(l);
for(auto it=itl;it!=itr;it++)
ans=(ans+ksm(*it.val,x,y)*((*it.r)-(*it.l)+1))%y;
return ans;
}
效率
ODT 的做法看起来非常暴力,但是在随机数据的情况下,它的表现其实非常优秀。
可以证明,一个有 \(n\) 个数的序列,在经过 \(k\) 次随机的区间赋值操作后,期望段数大约在 \(O(\frac{n}{k})\) 的级别。
证明可以点击 这里 查看,这里不再展开。
在模板题中,因为数据随机,有 \(\dfrac{1}{4}\) 的操作均为随机的区间赋值操作,因此 ODT 的实际段数很低,效率当然不错。

浙公网安备 33010602011771号