原文链接:https://oierbbs.fun/blog/33-odt
作者:@“Resory”#56
推荐时间:2021年8月4日
珂朵莉树(ODT)是一种玄学数据结构,得名于Codeforces 896 C,可以在较快的时间复杂度内实现区间赋值和区间修改等操作。
0x00 Prologue
在太阳西斜的这个世界里,
置身天上之森。
等这场战争结束之后,
不归之人与望眼欲穿的众人,
人人本着正义之名,
长存不灭的过去、逐渐消逝的未来。
我回来了,纵使日薄西山,即便看不到未来,
此时此刻的光辉,盼君勿忘。
这几天闲着慌,就去学了学珂朵莉树,作为一个很有用的骗分数据结构,个人认为还是有必要学一学的。反正又不难
0x01 What is it?
珂朵莉树起源于Codeforces 896 C,由于出题人的CF id为Old Driver,因此又被称为Old Driver Tree(ODT)
。
原题需要我们实现一种数据结构,可以快速维护以下操作:(保证数据随机)
- 区间加;
- 区间赋值;
- 求区间第 k k k 小;
- 求区间 k k k 次方和。
很明显,普通的线段树等数据结构都很难实现这种要求,但是这种似乎很复杂的数据结构,在出题人这位毒瘤眼中竟然可以用…
暴力实现?
珂朵莉树的思想在于把一段连续的值相同的区间用一个结构体来存储,由于数据随机且有大量区间赋值操作,这样的结构体数量会大大减少,从而保证了珂朵莉树接近线性的时间复杂度。
乍一看,珂朵莉树似乎并不像是一个树形数据结构,但因为它一般基于 s t d : : s e t std::set std::set 来实现,而 s t d : : s e t std::set std::set 本质是红黑树,所以也跟“树”勉强搭得上边。
0x02 如何实现?
珂朵莉树的精髓在于区间推平操作会产生大量的元素值相同的区间,因此我们用三个变量 l , r , v l,r,v l,r,v 来表示一个下标为 [ l , r ] [l,r] [l,r] 中元素值为 v v v 的区间。接着我们把这些三元组存储到 s t d : : s e t std::set std::set 中。写成代码就是这样的:
struct node {
int l,r;
mutable int v; // 这里mutable关键字保证了可以直接在set中修改v的值
node() {
}
node(int L,int R,int V): l(L),r(R),v(V) {
}
const bool operator < (const node& x) const {
return l < x.l; } // 按照区间所在的位置排序
}; typedef set < node > :: iterator iter; // 简化迭代器
set < node > s; // 存储到std::set中
比如当输入数据为1 2 2 2 3 3 4 4
时, s e t set set 内存储的数据就是这样的:
操作1:split
然而,我们操作的区间并不总是正好覆盖我们存储的区间。因此我们需要一个函数来实现分裂开每个区间的操作,也就是split
函数。split
函数接受一个参数 p o s pos pos ,然后把包含 p o s pos pos 的区间 [ l , r ] [l,r] [l,r] “分裂”成 [ l , p o s ) [l,pos) [l,pos) 和 [ p o s , r ] [pos,r] [pos,r] :
iter split(ll pos) {
iter it = s.lower_bound(node(pos,0,0)); // lower_bound寻找包含pos的区间
if(it != s.end() && it -> l == pos) return it; // 如果已经存在就直接返回
it --; // 往前数一个才是我们想要的
ll l = it -> l,r = it -> r,v = it -> v;
s.erase(it); // 删除原来的
s.insert(node(l,pos - 1,v));
return s.insert(node(pos,r,v)).first; // 分裂成两个并返回后面一个的迭代器
}
对于刚才那个图,如果我们执行split(4)
,那么他就会变成这样:
这里的运行流程:lower_bound
找到区间 < 5 , 6 , 3 > <5,6,3> <5,6,3> ,往前回滚找到 < 2 , 4 , 2 > <2,4,2> <2,4,2> ,并将其修改成 < 2 , 3 , 2 > <2,3,2> <2,3,2> 和 < 4 , 4 , 2 > <4,4,2> <4,4,2> 。
在进行区间操作时,我们常常要左右端点各split
一次,此时我们要先split
右端点,否则左端点的迭代器可能会失效而导致错误。
操作2:assign
很明显,如果操作中全是split
的话,区间数会越来越多,复杂度必然爆炸。这时我们就需要区间赋值操作减少区间数量:assign
。
assign
函数接受三个参数,表示区间和要赋的值。赋值时,把左右端点split
一下,然后把中间的部分变成一个新的区间就行了:
void assign(ll l,ll r,ll v) {
iter itr = split(r + 1),itl = split(l); // 左右端点各split一遍
s.erase(itl,itr); // 两端点之间的节点全部删去
s.insert(node(l,r,v)); // 插入新的区间
}
还是刚刚那个图,如果我们执行assign(3,5,5)
,就变成了这个亚子:
其他操作:暴力出奇迹
**区间加:**一个一个加一遍就完事了
void add(int l,int r,int x) {
iter itr = split(r + 1),itl = split(l);
for(;itl != itr;itl ++) itl -> v += x;
}
**区间第 k k k 大:**塞到 v e c t o r vector vector 里排序就完事了
int kth(int l,int r,int k) {
vector < pair < int,int > > vec; // pair存储值以及区间长度
iter itr = split(r + 1),itl = split(l);
for(;itl != itr;itl ++) vec.push_back(make_pair(itl -> v,itl -> r - itl -> l + 1