致敬传奇出题人lxl
珂朵莉树起源于CF896C ,这道题要求我们实现一种数据结构,可以较快地实现区间和、区间赋值、求区间第 k 大值和求区间 n 次方和。
使用场景
珂朵莉树是一种技巧,而不是一种特定的数据结构,可以用map实现也可以用set等实现,适用于区间赋值操作的数据结构题,在数据随机的情况下复杂度极其优秀,但如果数据不随机且出题人针对数据结构构造了特殊数据,那么复杂度会严重退化。
结构实现(set)
| struct Node_t { | 
这里v被声明为mutable,这是因为在set中,元素是const的,无法修改。但我们需要在split操作中修改v的值,所以使用mutable来解除const限制。
核心操作-split
split操作是珂朵莉树的核心,它的作用是将包含pos位置的区间分裂成两个区间:[l, pos-1]和[pos, r]。这样我们就可以精确地对任意区间进行操作。
| auto split(int pos) { | 
- s.lower_bound(Node_t{pos, 0, 0}):找到第一个左端点 大于等于- pos的区间。
- if (it != s.end() && it->l == pos):如果找到了一个区间且这个区间的左端点就是- pos,那么我们无需分裂,直接返回该区间的迭代器。
- it--:如果没找到,说明- pos在某个区间内部,我们需要回退一个迭代器,找到包含- pos的那个区间。
- s.erase(it):删除原来的大区间。
- s.insert(Node_t(l, pos - 1, v))和- s.insert(Node_t(pos, r, v)):将被删除的区间分裂成两个小区间并插入回- set中。
- 返回pos所在的新区间的迭代器。
核心操作-assign
assign操作用于将区间[l, r]的值全部修改为v。这是珂朵莉树保证复杂度的关键操作,因为它能将多个小区间合并成一个大区间,从而减少set中的元素数量。
| void assign(int l, int r, int v) { | 
- auto itr = split(r + 1), itl = split(l);:顺序很重要! 必须先- split(r + 1)再- split(l)。因为如果先- split(l),返回的迭代器- itl可能会在- split(r + 1)时因为- set的修改而失效。通过- split操作,我们将区间- [l, r]独立出来。
- s.erase(itl, itr):删除- [l, r]范围内的所有旧区间。
- s.insert(Node_t(l, r, v)):插入一个代表整个- [l, r]的新区间。
其他操作
在实现了split和assign之后,其他的区间操作就变得非常简单了。我们只需要利用split将要操作的区间[l, r]独立出来,然后遍历这个区间内的所有小块进行操作即可。
区间加
| void add(int l, int r, int val) { | 
查询区间第k小
| int query_kth(int l, int r, int k) { | 
查询区间幂次和
| long long query_pow_sum(int l, int r, int p, int mod) { | 
结构实现(map)
除了set,我们也可以用map来实现珂朵莉树。map的key可以存储区间的左端点l,value可以存储区间的右端点r和值v。
| 
 | 
核心操作-split
map版本的split与set版本类似,但操作map的迭代器和元素的方式有所不同。
| auto split(int pos) { | 
核心操作-assign
assign操作同样是先split再合并。
| void assign(int l, int r, int v) { | 
其他操作
与set版本类似,在map版本中,我们同样利用split函数来划定操作区间,然后遍历这个区间内的所有节点来完成相应操作。
区间加
| void add(int l, int r, int val) { | 
查询区间第k小
| int query_kth(int l, int r, int k) { | 
查询区间幂次和
| long long query_pow_sum(int l, int r, int p, int mod) { | 
复杂度分析
珂朵莉树的复杂度依赖于assign操作的次数。在数据随机的情况下,assign操作会使得区间数量大大减少,因此复杂度接近于O(n loglog n),其中n是操作次数。然而,在最坏情况下(没有assign操作),split操作会不断增加区间的数量,复杂度会退化到O(n log n),甚至更差。
- split(pos):- O(log n),其中- n是- set中的元素数量。
- assign(l, r, v):- O(k log n),其中- k是- [l, r]范围内的区间数量。由于- assign会合并区间,所以均摊复杂度很低。
- 其他操作: O(k log n),其中k是[l, r]范围内的区间数量。
