0xff:前置芝士
在阅读本文前,请先了解并会熟练掌握:set、set的迭代器、结构体struct的相关操作,
不会的先去了解并使用熟练后再来看本文
一、什么是珂朵莉树?
珂朵莉树是一种数据结构,又称Old Driver Tree(ODT老司机树),是一种基于set的暴力数据结构,等一下会讲解。
二、为什么叫珂朵莉树?
这个数据结构诞生于一道cf毒瘤题CF896C Willem, Chtholly and Seniorious,命名为珂朵莉树的原因很明显,题目名字翻译过来是威廉,珂朵莉和瑟尼欧里斯,然后题目中附带了一张动漫中的截图(威廉帮珂朵莉调整瑟尼欧里斯),出题人ODT在题解里写到了这个新数据结构,命名为Chtholly Tree,据说能艹翻标程
珂朵莉·诺塔·瑟尼欧里斯出自TV动画末日时在做什么?有没有空?可以来拯救吗?,珂学家指的就是珂朵莉厨(没错我就是)
中国珂学院:https://www.chtholly.ac.cn/
好回归正题:
三、为什么要用珂朵莉树?
珂朵莉树的关键操作在于它可以把一段区间赋值成同一个数x,
并且数据要随机,因为它本身就很玄学
四、怎么写珂朵莉树呢?
1.节点初始化
一棵树肯定有节点啊,这里用一个struct存
struct Node
{
int l,r;
mutable int val;
Node(int _l,int _r=-1,int _val=0):l(_l),r(_r),val(_val){}
inline bool operator < (const Node &nt) const
{
return l<nt.l;
}
};
我们逐行解答:
首先,各个变量的意思:这个节点表示从l到r这个区间内的数的值都是val
为什么要mutable?mutable与从const相对,因为这个val在set中是要更改的,不开这个会CE
然后Node(int , , ,)这个就是一种比较标准的给结构体赋值的方式,当然直接(Node){x,y,z}也可以
接下来是重载小于号,意味着这个set的排序按照l从小到大排
2.核心操作之一:split
split(pos)操作是将原来含有pos位置的节点分成两部分:\([l,pos-1]\)和\([pos,r]\)
举个栗子理解一下:这里用(l,r,val)来表示一个节点
设原节点按照set的排序顺序为(1,2,1), (3,6,2), (7,7,3)
那么split(5)操作后的节点序列就变为(1,2,1), (3,4,2), (5,6,2), (7,7,3)
这个看上去非常暴力对不对
上Code然后逐行分析:
#define IT set<Node>::iterator
inline IT split(int pos)
{
reg IT iter=tr.lower_bound(Node(pos));
if(iter!=tr.end()&&iter->l==pos)
return iter;
--iter;
reg int nl=iter->l,nr=iter->r,nval=iter->val;
tr.erase(iter);
tr.insert((Node){nl,pos-1,nval});
return tr.insert((Node){pos,nr,nval}).first;
}
宏定义不说,手打那么多迭代器会死人
函数定义这行,为什么有返回值?这里返回的是代表\([pos,r]\)区间的节点的迭代器,方便于其他操作
行4,在珂朵莉树中找到第一个l大于等于pos的节点,简单来说就是找到包含pos下标(比如(3,6,x)这个节点就包含了下标3~6)的节点
这里切记,在set里进行lower_bound之类的操作千万不能用lower_bound(tr.begin(),tr.end(),x);这样的写法!正确的写法是tr.lower_bound(x);,不要以为这只是什么码风问题,错误写法的复杂度是\(O(nlog^2n)\)而正确的写法复杂度只有\(O(logn)\),因为错误写法是按照正常数组的遍历方法去二分,而set是不支持随机访问的,然后迭代器的++和--操作又是\(O(logn)\)的,所以总的错误复杂度为\(O(n)×O(logn)×O(logn) = O(nlog^2n)\),这和正确写法的\(O(logn)\)可是有着天壤之别
如果迭代器不等于珂朵莉树的end()并且找到的这个节点(迭代器)的l恰好为pos,这代表什么?我们知道,“上一个”节点的r一定是小于“下一个”节点的l的,于是,既然已经找到了\([a,b], [c,d]\)中的c,那么直接返回这个迭代器就好了,其他什么多别做
如果不呢?那么找到的这个节点的l一定大于pos的(l大于等于pos,l不等于pos当然大于pos),那么要找的那个节点一定在“上一个”节点那里对不对?于是,--iter来到上一个节点
先将这所找到的这个节点的数值赋值给nl,nr,nval,然后原来的iter就可以删除了(因为是迭代器,先后顺序不能反),插入两个节点:(nl,pos-1,nval)和(pos,nr,nval),这就是我们所想要的split操作
最后那个return是什么鬼?
那个是一个奇怪的语法,你可以理解为:1.插入节点(pos,nr,nval);2.返回节点(pos,nr,nval)的迭代器
3.核心操作之一:assign
只有split操作怎么行?把set的节点数不断增多复杂度还不是爆炸??
那么这时候就要用assign操作减小节点数量,降低set的规模
注意:这个操作是珂朵莉树玄学复杂度的保证,如果题目要求不包含这个操作,那么不可用珂朵莉树
assign(l,r,val)的意思就是把原来数组下标\([l,r]\)的地方在珂朵莉树中全部赋值为val
怎么写呢?特别暴力。。
直接上Code,逐行讲解:
inline void assign(int l,int r,int val)
{
reg IT iterr=split(r+1),iterl=split(l);
tr.erase(iterl,iterr);
tr.insert(Node(l,r,val));
return;
}
首先我们要把\(l-1,l\) “切”开,\(r,r+1\) “切"开,切就用到了split操作
然后用两个迭代器存下split操作的返回值
iterl对应的是(l,...,...)的节点,iterr对应的是(r+1,...,...)的节点
这里注意,必须先split(r+1)再split(l),因为在split(r+1)的时候可能会让split(l)的迭代器失效,这时候会导致程序RE
然后我们先要把\([l,r]\)这段的所有节点删除掉
erase有一种用法为void erase(iterator fr,iterator se),作用是把\([fr,se)\)区间删除(注意左闭右开),那么这里只需要erase(iterl,iterr)就可以删除\([l,r]\)区间了(对应erase的操作其实是erase(l,r+1))
给张图理解一下:
在同一框框内的就是同一个节点,框框内的数字就是val
这段操作对应的是assign(5,10,5)
第一步,split,棕色的线就是split的痕迹
第二步,删除\([l,r]\)区间
第三步,insert插入新的\([l,r]\)区间
其他操作(区间加什么的)很暴力而且很简单,我会在cf896c的题解中写道
大概就是这样子,Code:
inline void work(int l,int r)
{
IT iterr=split(r+1),iterl=split(l);
for(;iterl!=iterr;++iterl)
{
//iterl->val+=...;
// just do it!
}
}
五、复杂度
珂朵莉树是用来骗分的一种暴力数据结构!
珂朵莉树是用来骗分的一种暴力数据结构!!
珂朵莉树是用来骗分的一种暴力数据结构!!!
在数据纯随机的情况下,可以证明每次assign的区间长度期望为N/3
于是set的规模迅速下降,最后接近\(O(qlogn)\)的玄学非正常复杂度
注:q为操作询问数目,n为原数组大小
各位巨佬也可以去看cf上面的证明贴
六、总代码
注意插入完所有n个节点以后还要做一次tr.insert(Node(n+1,n+1,0))操作
Code:
#include <cstdio>
#include <cctype>
#include <set>
#include <algorithm>
#define reg register
#define int long long
#define IT set<Node>::iterator
using namespace std;
struct Node
{
int l,r;
mutable int val;
Node(int _l,int _r=-1,int _val=0):l(_l),r(_r),val(_val){}
inline bool operator < (const Node &nt) const
{
return l<nt.l;
}
};
set<Node> tr;
int n,m;
template <class t> inline void rd(t &s)
{
s=0;
reg char c=getchar();
while(!isdigit(c))
c=getchar();
while(isdigit(c))
s=(s<<3)+(s<<1)+(c^48),c=getchar();
return;
}
inline IT split(int pos)
{
reg IT iter=tr.lower_bound(Node(pos));
if(iter!=tr.end()&&iter->l==pos)
return iter;
--iter;
reg int nl=iter->l,nr=iter->r,nval=iter->val;
tr.erase(iter);
tr.insert((Node){nl,pos-1,nval});
return tr.insert((Node){pos,nr,nval}).first;
}
inline void assign(int l,int r,int val)
{
reg IT iterr=split(r+1),iterl=split(l);
tr.erase(iterl,iterr);
tr.insert(Node(l,r,val));
return;
}
inline void addx(int l,int r,int val)
{
reg IT iterr=split(r+1),iterl=split(l);
for(;iterl!=iterr;++iterl)
iterl->val+=val;
return;
}
signed main(void)
{
rd(n);rd(m);
reg int x;
for(int i=1;i<=n;++i)
rd(x),tr.insert(Node(i,i,x)); //暴力插点
tr.insert(Node(n+1,n+1,0));
for(int i=1;i<=m;++i)
{
//just do it
}
return 0;
}
\(End\)~
来源:https://www.cnblogs.com/micromakerblog/p/12274725.html