珂朵莉树-ODT

这是一种内含均摊思想的暴力,为什么叫树?可能和 set 的使用有关吧......

核心思想:

将一段相同权值的区间压缩成一个结点,在 set 中保存

它的用处:对于随机数据,如果含有区间赋值,我们就有机会让结点数大幅度下降,在求值操作时可以快速操作

当然,对于精心构造的数据,珂朵莉树可以说是束手无策的(如没有区间赋值操作,一直让你求值,复杂度就会退化到 \(O(n^2)\)

在保证数据随机下,珂朵莉树的复杂度有严格的证明:珂朵莉树的复杂度分析
(我太弱了看不懂),用 set 实现为 \(O(nlog^2n)\) ,链表实现为 \(O(logn)\)


具体做法:算法来源 CF896C

首先,我们要定义结点保存的方式:

struct Node
{
	int l, r;
	mutable LL v;
	bool operator < (const Node &a) const {return l < a.l;}
};

\(l\) , \(r\) 表示结点代表的区间,\(v\) 表示这段区间的值

mutable 关键字:这表示我们定义的 \(v\) 永远处于可变的状态,即使是在一个 const 函数中;这使我们可以直接修改 set 中的 \(v\) 值,方便我们区间赋值的操作,不需要“取出-删除-重新插入”


核心操作:\(split(pos)\)

inline auto split(int pos)
{
	if(pos > n) return f.end();
	auto it = --f.upper_bound((Node){pos, 0, 0});
	if(it -> l == pos) return it;
	int l = it -> l, r = it -> r; LL v = it -> v;
	f.erase(it);
	f.insert((Node{l, pos - 1, v}));
	return f.insert((Node){pos, r, v}).first;
}

这个操作就是在 set 中分裂出以 \(pos\) 为左端点的区间,并返回这个区间的迭代器

当我们 \(split(l)\)\(split(r+1)\) 后,set 中就会有以 \(l\) 为左端点的区间,以及以 \(r\) 为右端点的区间

当我们需要对 \([l,r]\) 进行操作时,我们就可以直接遍历这些区间,并进行修改、求值

注意:实际操作时,我们要先 \(split(r+1)\),再 \(split(l)\)

原因:如果我们先 \(split(l)\),这时候 \(r+1\) 还在以 \(l\) 为左端点的区间内,\(split(r+1)\) 就会导致我们 \(split(l)\) 时返回的迭代器因为被删除而失效,然后RE


珂朵莉树的基础:\(assign(l,\ r,\ v)\)

即:将区间 \([l,r]\) 的值改为 \(v\)

很简单,我们只需要 \(itr=split(r+1)\) , \(itl=split(l)\) 后,遍历 \([itl,itr)\) 进行逐一修改即可

其他操作同理,即暴力操作


代码

再次警示:如果一个题目没有区间赋值操作,或者数据很不随机,请不要把珂朵莉树当正解打


例题:CF817F MEX Queries

这道题本来是离散化+线段树板子题,终究是数据水了

我们只需要将 \(op=1\) 的操作看成将 \([l,r]\) 标记为 1 ,\(op=2\) 的操作看成将 \([l,r]\) 标记为 0 ,\(op=3\) 的操作看成将 \([l,r]\) 的值翻转(0 变 1,1 变 0)。

每次询问后找出第一个值为 0 的区间,输出 \(l\) 即可

posted @ 2022-03-10 13:42  zuytong  阅读(69)  评论(0)    收藏  举报