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