OierBBS文章推荐 #1 - 浅谈珂朵莉树(ODT)

原文链接: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
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值