玄学数据结构——珂朵莉树

关于珂朵莉

こんなにも、たくさんの幸せをあの人に分けてもらった
だから、きっと
今の、私は
誰が何と言おうと

世界一、 幸せな女の子だ

————クトリ

美图欣赏:

玄学数据结构——珂朵莉树

玄学数据结构——珂朵莉树

玄学数据结构——珂朵莉树
(图片来源:萌娘百科)

中国珂学院

好了好了说正事

关于这奇怪的名字

本来是起源于CF896C Willem, Chtholly and Seniorious(一个CF在国内的镜像网站 ),当时CF上有一名叫做Old Driver的用户(就是lxl)在提交了线段树的正解之后又补充了这么一份奇怪的数据结构,得以命名为珂朵莉树(又称ODT(老司机树))

在某谷上的评级是一道黑题,但是日常被爆切

前置知识

std::set的简单操作,一点区间操作的知识,没了

正篇

珂朵莉树可以用来解决含有区间覆盖(或者是区间统一赋值)的操作,本质上是基于(暴力)std::set的数据结构,对于随机数据来说效率很高甚至有时候比正解还高

注意这种数据结构只在数据随机的情况下表现良好

珂朵莉树的定义非常简单,就是把一段有着相同值的区间变成set里面的一个元素,以左区间位置为关键字维护的一个set。所以我们就可以写出珂朵莉树的节点定义和初始化:

struct node {
    int l, r;//左右区间
    mutable ll val;//值,这里的mutable意味着在集合的任何位置上都是课修改的
    node(int l_ = -1, int r_ = -1, ll val_ = 0) {//构造函数
        l = l_, r = r_, val = val_;
    }
    bool operator < (const node &a) const {//运算符重载
        return l < a.l;
    }
};
set<node> st;//搞一个集合

split操作

split操作是珂朵莉树里面最重要的操作,其实很简单。对于某一个位置pos,我们将其所处的区间分为[l,pos),[pos,r]并且返回后面那个区间的迭代器,具体的看代码吧:

IT split(int pos) {
    IT it = st.lower_bound(node(pos));//查找后继
    if (it != st.end() && it->l == pos) return it;//如果此时这个区间的左边界就是pos的话就直接返回
    --it;//否则就是前面的区间
    node tmp = *it;//保存这个区间的信息
    st.erase(it);//删除这个区间
    st.insert(node(tmp.l, pos - 1, tmp.val));//插入两个新区间
    return st.insert(node(pos, tmp.r, tmp.val)).first;
}

assign操作

assign操作就是区间赋值/区间推平(像远野志贵那样),有了split操作之后我们就可以很简单地写出assign的代码了。

珂朵莉树的复杂度全部来源于assign操作,如果我们只有split操作的化复杂度就螺旋上天

由于数据随机,所以有1/4的操作都是assign, assign操作会合并具有相同值的区间,使set的大小下降,最后趋近于logn, 所以时间复杂度是mlogn

void assign(int l, int r, ll val) {
    IT itr = split(r + 1), itl = split(l);
    st.erase(itl, itr);
    st.insert(node(l, r, val));
}

这里要注意一点,就是但凡我们要split的话肯定是优先split(r+1),这里应该是为了保证不让在split(l)的时候把r的位置也给搞掉了。

接下来就是一些非常暴力的运算方式了,这里也给一下代码:

区间加:

void add(int l, int r, ll val) {
    IT itr = split(r + 1), itl = split(l);
    for (IT it = itl; it != itr; ++it) {
        it->val += val;
    }
}

这里要注意,由于我们set存储的是区间,所以在解决区间求和之类的问题的时候不要忘记考虑区间长度

区间求和:

ll querySum(int l, int r) {
    IT itr = split(r + 1), itl = split(l);
    ll res = 0;
    for (IT it = itl; it != itr; ++it) {
        res += (it->r - it->l + 1) * it->val;
    }
    return res;
}

区间第k小:

ll queryKth(int l, int r, int k) {
    vector< pair<ll, int> > vec(0);
    IT itr = split(r + 1), itl = split(l);
    for (IT it = itl; it != itr; ++it) {
        vec.push_back(make_pair(it->val, it->r - it->l + 1));
    }
    sort(vec.begin(), vec.end());
    for (vector< pair<ll, int> >::iterator it = vec.begin(); it != vec.end(); ++it) {
        if ((k -= it->second) <= 0) return it->first;
    }
    return -1;
}

这里我们其实就是直接把整个区间都push到一个vector里面,然后再暴力sort接着输出。。。注意我们要把值放在pair的第一个关键字哪里,因为我们是要给值排序的嘛

这里给出一个板子:

//Chtholly tree
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct node {
    int l, r;
    mutable ll val;
    node(int l_ = -1, int r_ = -1, ll val_ = 0) {
        l = l_, r = r_, val = val_;
    }
    bool operator < (const node &a) const {
        return l < a.l;
    }
};
typedef set<node>::iterator IT;
set<node> st;
IT split(int pos) {
    IT it = st.lower_bound(node(pos));
    if (it != st.end() && it->l == pos) return it;
    --it;
    node tmp = *it;
    st.erase(it);
    st.insert(node(tmp.l, pos - 1, tmp.val));
    return st.insert(node(pos, tmp.r, tmp.val)).first;
}

void assign(int l, int r, ll val) {
    IT itr = split(r + 1), itl = split(l);
    st.erase(itl, itr);
    st.insert(node(l, r, val));
}

void add(int l, int r, ll val) {
    IT itr = split(r + 1), itl = split(l);
    for (IT it = itl; it != itr; ++it) {
        it->val += val;
    }
}

ll querySum(int l, int r) {
    IT itr = split(r + 1), itl = split(l);
    ll res = 0;
    for (IT it = itl; it != itr; ++it) {
        res += (it->r - it->l + 1) * it->val;
    }
    return res;
}

ll queryKth(int l, int r, int k) {
    vector< pair<ll, int> > vec(0);
    IT itr = split(r + 1), itl = split(l);
    for (IT it = itl; it != itr; ++it) {
        vec.push_back(make_pair(it->val, it->r - it->l + 1));
    }
    sort(vec.begin(), vec.end());
    for (vector< pair<ll, int> >::iterator it = vec.begin(); it != vec.end(); ++it) {
        if ((k -= it->second) <= 0) return it->first;
    }
    return -1;
}

int main(void) {

}

复杂度证明

虽然前面我们估算(瞎猜)了一下含有assign操作复杂度,但是这里我找了一份相对比较完全的复杂度证明

复杂度证明
(在骗分数据结构底下寻找复杂度证明是不是搞错了什么)

后记

还有很多别的操作,其本质就是对分离出的那一段区间进行暴力运算,这里就不再赘述了

题目的话这里有一道:P4344 [SHOI2015]脑洞治疗仪
2020/03/09的时候还没有卡珂朵莉树,但是如果要用珂朵莉树的话需要吸氧(开O2)才能过

好多题目珂朵莉树都没法AC(可见还是好好打线段树才是王道)

总的来说,珂朵莉树就是用来骗分的(一题骗到84分还是很可观的),学会打了之后可以应对某些题目的时候拿稍微多一点的分数

可能有些地方讲的有点问题,欢迎dalao提出

原文链接: https://www.cnblogs.com/jrdxy/p/12449102.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    玄学数据结构——珂朵莉树

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/372712

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月3日 上午11:23
下一篇 2023年3月3日 上午11:23

相关推荐