朝田诗乃 的博客

朝田诗乃 的博客

我的数据结构不可能这么可爱!——珂朵莉树(ODT)详解

posted on 2018-12-08 21:20:26 | under 未分类 |

参考资料:

Chtholly Tree (珂朵莉树) (应某毒瘤要求,删除链接,需要者自行去Bilibili搜索)

毒瘤数据结构之珂朵莉树


全是珂学家的珂谷,你却不知道珂朵莉树?来跟诗乃一起学习珂朵莉树丫~

(挑战用最短的篇幅讲清楚一个毒瘤数据结构)

1、珂朵莉是什么?

珂朵莉·诺塔·瑟尼欧里斯是轻小说及改编动画《末日时在做什么?有没有空?可以来拯救吗?》中的女主角,五位成体妖精兵之一。最强圣剑“瑟尼欧里斯”的适合者。在第28号浮游岛上意外跌落而与威廉相遇,并受到他的帮助。 我永远喜欢珂朵莉 但是知道了珂朵莉对你学习珂朵莉树并没有什么帮助。

2、珂朵莉树是什么?

珂朵莉树又称老司机树(Old Driver Tree),可能是因为发明者是一个珂学家所以发明了这个数据结构?


废话到此结束。

珂朵莉树是基于C++STL库中的set的数据结构。与线段树、平衡树等树形结构类似,珂朵莉树是用来解决区间问题的很暴力的树形结构。

特色:珂朵莉树支持区间推平(将区间 $L$ ~ $R$ 全部赋值为同一个值 $x$ )

3、在学习之前你需要知道:set(若熟悉此STL请跳过)

set的本质是一棵平衡树。特性为插入到set中的元素会被自动排序并且不允许set中有相同的元素。

珂朵莉树中需要用到的函数:

set <Node> t; //定义一个名为t,类型为Node(结构体)的set
s.begin();    //返回指向第一个元素的迭代器(地址)
s.end();      //返回指向最后一个元素的迭代器
s.clear();    //清空s
s.insert(a);  //将a插入到s
s.erase(l, r);//将迭代器l~r之间的元素全部删除
s.lower_bound(a); //二分查找a在s中的位置,返回一个迭代器。
set<Node>::iterator pos; //定义一个迭代器pos。

4、珂朵莉树可以解决什么问题?

当我们需要推平一段区间时,可以使用珂朵莉树。珂朵莉树高效的基础是数据随机生成。也就是说,这个数据结构很容易被构造数据卡掉。但是在珂朵莉树依然鲜为人知的现在又有谁会去构造数据去卡一个知名度并不高的算法呢?你都愿意学SPFA了不是吗

来看一道例题:

CF869C Willem, Chtholly and Seniorious

题目大意:

我有一个可爱的序列。你需要编写程序支持以下操作:

$1$ $l$ $r$ $x$ :将 $[l,r]$ 区间所有数加上 $x$

$2$ $l$ $r$ $x$ :将 $[l,r]$ 区间所有数改成 $x$

$3$ $l$ $r$ $x$ :输出将 $[l,r]$ 区间从小到大排序后的第 $x$ 个数是的多少

$4$ $l$ $r$ $x$ $y$ :输出 $[l,r]$ 区间每个数字的 $x$ 次方的和模 $y$ 的值

5、珂朵莉树的构造

其实珂朵莉树被称作“树”大概仅仅是因为使用了set吧。在学习珂朵莉树时,你大可以不把它作为一个树来理解。

珂朵莉树的每一个节点由一个三元组( $l, r, val$ )组成,表示区间 $[l,r]$ 之间的值全是x。

例子: 我有一个可爱的序列:

$1$ $1$ $1$ $1$ $1$ $2$ $2$ $2$ $2$ $3$ $3$ $3$ $3$

则在珂朵莉树上这个序列被表示为 $(1, 5, 1)$ $(6, 9, 2)$ $(10, 14, 3)$ 三个节点。

建树实现:

struct node {
    int l, r;//区间左端点与右端点。
    mutable lint v;//mutable为“可变的”,使我们可以直接修改v的值
    node(int L, int R = -1, lint V = 0) : l(L), r(R), v(V) {}
    bool operator < (const node &o) const {
        return l < o.l;
    }
}; //定义一个结构体,重载<为按左端点排序。
set <node> s;//扔到set里

是不是非常的简单好懂鸭?

6、核心操作:区间分割

考虑刚刚那个可爱的序列:

$1$ $1$ $1$ $1$ $1$ $2$ $2$ $2$ $2$ $3$ $3$ $3$ $3$

现在我要强人锁男将 $2$ ~ $13$ 这个区间全部加上 $1$ 。

前文提到,在珂朵莉树上这个序列被表示为 $(1, 5, 1)$ $(6, 9, 2)$ $(10, 14, 3)$ 三个节点。也就是说我们根本没有办法直接对 $2$ ~ $13$ 这个区间加上 $1$ .

这个时候如果我们将 $(1, 5, 1)$ $(6, 9, 2)$ $(10, 14, 3)$ 三个区间分割为 $(1, 1, 1)$ $(2, 5, 1)$ $(6, 9, 2)$ $(9, 13, 3)$ $(14, 14, 3)$ 这几个区间就可以直接对区间 $2$ ~ $13$ 中所对应的节点直接进行操作啦!超快乐!

∴现在我们需要写一个函数spilit(pos),将pos所在的节点以pos为中心分为两个节点。

实现步骤:

  1. 找到pos所在的节点 $(l, r, val)$
  2. 删掉这个节点
  3. 把这个节点变成 $(l, pos-1, val)$ 和 $(pos, r, val)$ 扔回set。
  4. 返回右区间迭代器(以后有用)

代码实现:

#define IT set<node>::iterator 
//迭代器宏定义
IT spilit (int pos) {
    IT it = s.lower_bound(node(pos, -1, 0));//找到第一个l不小于pos的节点
    if(it != s.end() && it->l == pos) return it;//不需要分割直接退出
    it--;//pos一定在前一个区间中
    int L = it -> l, R = it -> r;
    lint V = it->v;
    s.erase(it);//删了
    s.insert(node(L, pos-1, V));//左区间丢进去
    return s.insert(node(pos, R, V)).first;//右区间丢进去,返回右区间的迭代器。
}

是不是特别简单?

7、降低复杂度的关键:区间赋值(推平)

实现步骤: 设要推平的区间为 $[l, r]$ 。

  1. 把l和r所在的节点分割。

  2. 把要推平的区间 $[l, r]$ 之间的节点全部删掉。

  3. 把 $(l, r, val)$ 扔进set

没了。诗乃觉得没有什么需要解释的了。

#define IT set<node>::iterator 
void tp(int l, int r, int val) {
    IT il = spilit(l), ir = spilit(r+1);
    s.erase(il, ir);
    s.insert(node(l, r, val));
}

8、当我们要对一段区间进行操作时:

设要操作的区间为 $[l, r]$ 。

我们先把l和r所在的节点分割,暴力对区间 $[l, r]$ 中的节点一个个取出来操作(修改或统计答案)即可,反正没有多少节点。这个东西视具体题目而定,这里不再赘述。

例子:区间加

#define IT set<node>::iterator 
void add(int l, int r, int val) {
    IT il = spilit(l), ir = spilit(r+1);
    for(; il != ir; il->v += val, il++);
}

9、我最喜欢暴力数据结构了。

惊不惊喜?现在你已经学会珂朵莉树了,可以做一做例题练一下手呢。(是NOI+/CTSC难度的黑题哦)。

其他例题: CF915E 、bzoj4293(权限题)

珂朵莉树的优点:码量少,好理解,调错快。

缺点:在数据随机的时候推平操作比较多,所以它的复杂度会趋近于mlogn(m为询问次数)。但当出题人想要卡珂朵莉树时肯定会T飞。珂朵莉这么可爱谁会卡呢

广告:来踩一下诗乃的博客呗~Aknote

10、我永远喜欢珂朵莉!