珂朵莉树-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\) 即可

浙公网安备 33010602011771号