珂朵莉树的易懂教学

删除回忆录丶 提交于 2020-02-07 23:34:34

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;
    }
};

我们逐行解答:

首先,各个变量的意思:这个节点表示从lr这个区间内的数的值都是val

为什么要mutablemutable与从const相对,因为这个valset中是要更改的,不开这个会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大于等于posl不等于pos当然大于pos),那么要找的那个节点一定在“上一个”节点那里对不对?于是,--iter来到上一个节点

将这所找到的这个节点的数值赋值给nlnrnval然后原来的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!
    }
}

五、复杂度

珂朵莉树是用来骗分的一种暴力数据结构!

珂朵莉树是用来骗分的一种暴力数据结构!!

珂朵莉树是用来骗分的一种暴力数据结构!!!

这里摘用了niiick巨佬的博客

数据纯随机的情况下,可以证明每次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\)~

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!