暴力数据结构——ODT 珂朵莉树

Origin

ODT (Old Driver Tree),中文名珂朵莉树。

有人为了 CF896C 发明了这个算法,这道题又和珂朵莉有关,所以这个算法叫做珂朵莉树。

另外,由于发明者 lxl 的原因,也珂叫 ODT (Old Driver Tree)

也有个正统名字叫颜色段均摊但是还是叫珂朵莉树好听

0x00 前言

之所以突然想学这个数据结构是因为之前做了一道题 SP13015,看到的第一眼就是线段树,等到把这道题 A 了之后打开标签才发现是珂朵莉树,出于好奇心的驱使,才有了这篇博客。

珂朵莉可爱捏!

前置芝士:set(现在知道为什么上一篇博客是 set 的学习笔记了吧)

Warning!

ODT 可以 可以解决一些线段树不能解决的问题,如区间次幂求和。

但要求数据随机,随机下跑得很快,开了 O2 更快。

数据不随机时间复杂度就是 O(nm),开了 O2 也没用。

0x01 珂朵莉树珂以解决什么问题

对一个序列,进行一个区间推平操作。就是把一个范围内,比如 [l,r] 范围内的数字变成同一个。可能除了推平以外,还夹杂其他操作。如果数据是随机的,就可以用珂朵莉树啦。

0x02 基本结构

由于区间推平操作,所以序列中的数是一段一段的,而且每一段是同一个数。

所以我们将每个这样的区间打包成一个三元组 [l,r,val],分别代表:左端点、右端点和值。然后将所有这样整合后的三元组插到一个 set 里去维护。

这样的话,只需要一个结构体就行了。

struct node{
	int l, r;
	mutable ll val; //mutable 方便以后修改
	node(int l_ = 0, int r_ = -1, ll val_ = 0) :l(l_), r(r_), val(val_) {}
	bool operator <(const node &other) const{ //重载成小于
		return l < other.l;
	}
};
typedef set<node>::iterator IT;
set <node> s;

借大佬的图一用

0x03 基本操作

核心操作: split

在推平操作的进行中,一些区间可能要被合并成一个区间,也可能被分成几个新区间。

这时候就需要用到 split 函数,它的作用是给定一个位置 pos,然后找到包含 pos 的区间,将其分成 [l,pos1][pos,r] 两个区间,最后返回指向 [pos,r] 的迭代器。当然,如果 pos 本身就是一个区间的开头,就不用切割了,直接返回这个区间的迭代器。

IT split(int pos) {
	IT it = s.lower_bound(node(pos)); //按 l 找包含 pos 的 node
	if(it != s.end() && it -> l == pos) return it; //若 pos 是一个区间的开头,就直接返回该区间迭代器
   --it; //由于找到的是 pos 后面的区间,所以要先--才是包含它的区间
	int l = it -> l, r = it -> r;
	ll val = it -> val;
	s.erase(it); //先把此区间删除
	s.insert(node(l, pos - 1, val)); //插入切割后的第一个区间
	return s.insert(node(pos, r, val)).first; //插入切割后的第二个区间并返回值
	//insert函数返回pair,其中的first是新插入结点的迭代器
}

那么我们按照 pos 创建一个 node ,然后去查询,就找到了 it 这个位置。这个时候有三种情况,一种是我们正好找到了一个区间,它是以 pos 开头的,所以就对应了代码中的第一个 if 判断,这时候直接返回这个区间的迭代器 it

还有两种情况是,我们找到的这个区间是正好比包含 pos 的区间大一点点的,或者 pos 太大了,超过了最后一个区间的右端点。不管怎样先把 it 往前挪一个格,然后这时候看看 it 的右端点,如果比 pos 小,说明是 pos 太大了,就直接返回 s.end()。否则的话,现在 it 就是应该包含了 pos 的那个区间。这时候,我们要把它一分为二,把原来的那个区间删掉,然后插入两个新区间,分别是 [l,pos1][pos,r]

assign

对应区间推平操作。因为区间 [l,r] 可能与其他节点代表的区间有交集,所以我们要先把 lr split 出来,将其中的节点全删掉,最后再插入{l,r,val}

void assign(int l, int r, ll val) {
	IT itr = split(r + 1), itl = split(l);
	s.erase(itl, itr);
	s.insert(node(l, r, val));
}

注意: 必须要先 split(r+1),再 split(l)

比如现在要从 [3,5] 中分离出 [3,4],则要执行 split(3)split(5)。若先执行后者,再执行前者,则 itl 指向代表 [3,5]nodeitr 指向代表 [5,5]node,但是这时代表 [3,5]node 已经被删除了,调用 erase 时就会出问题,导致 RE。

其他操作

void change(int l, int r, ll val) { //暴力套就完事了
	IT itr = split(r + 1), itl = split(l);
	for(IT it = itl; it != itr; it++) {
		//to do
	}
}

0x04 时间复杂度

珂朵莉树只有当数据随机时才有比较好的表现,这是因为她的时间复杂度是期望复杂度。比如一共有 4 种操作,在随机数据下,进行区间推平的概率为 14,对于每次区间推平操作,都相当在一整条线段上选取一段,期望长度为整个线段长度的 13,而每次平推操作都会删除掉此区间内的其他区间节点,所以每次操作后 set 的长度都会变成原来的 23。所以一波操作下来,set 的长度就变为了 O(logn),所以时间复杂度约为 O(qlogn)

以上只是我方便自己理解的粗略证明,严谨、正确性方面不保证。

好好好,正确又严谨的证明如下:(再次借大佬的图)



posted @   Brilliant11001  阅读(101)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示