玄学数据结构——珂朵莉树
关于珂朵莉
こんなにも、たくさんの幸せをあの人に分けてもらった
だから、きっと
今の、私は
誰が何と言おうと
世界一、 幸せな女の子だ
————クトリ
美图欣赏:
(图片来源:萌娘百科)
中国珂学院
好了好了说正事
关于这奇怪的名字
本来是起源于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提出