珂朵莉树/ODT 学习笔记
起源自 CF896C。珂朵莉yyds!
核心思想
把值相同的区间合并成一个结点保存在 set 里面。
用处
骗分。只要是有区间赋值操作的数据结构题都可以用来骗分。在数据随机的情况下一般效率较高,但在不保证数据随机的场合下,会被精心构造的特殊数据卡到超时。
如果要保证复杂度正确,必须保证数据随机。详见 Codeforces 上关于珂朵莉树的复杂度的证明。
更详细的严格证明见 珂朵莉树的复杂度分析。对于 add,assign 和 sum 操作,用 set 实现的珂朵莉树的复杂度为 O ( n log log n ) O(n \log \log n) O(nloglogn),而用链表实现的复杂度为 O ( n log n ) O(n \log n) O(nlogn)。
正文
存储:每个树上节点都是一个区间,因为权值相同,于是要记录 l , r , v a l l,r,val l,r,val 。
struct node{
I l,r;
mutable int val;//可变的 val ,这么写是习惯,为了后续修改/访问操作
friend bool operator <(node a,node b){
return a.l<b.l;}//在 set 里面,应该比较区间左端点。以便之后查找区间并且 split 。
};set<node>s;
核心操作:split ,含义:把原来为 [l,r] 的这个节点分离成 [l,x) [x,r] ,并且返回这个 [x,r] 区间的迭代器。(可以直接使用 lower_bound)
auto split(I x){
auto it=s.lower_bound({x,0,0});//在 set 之中查找 x ,因为只比较左端点所以另外两项可以随便填
if(it!=s.end()&&it->l==x)return it;//如果正好找到了一个左端点为 x 的就直接返回。
--it;auto p=*it;s.erase(it);//迭代器向左移动一位的区间才有可能被分裂,用 p 记录该迭代器,并且删除现在代表的区间
s.insert(node{p.l,x-1,p.val});//把这个区间(存在 p 里面了)分裂
return s.insert(node{x,p.r,p.val}).first;//同上,返回右边所指的迭代器,insert 的返回值是一个 pair ,它的返回值 first 是迭代器,second 是一个 bool ?
}
核心操作:assign,含义:把区间 [l,r] 赋值成为 val 。
void assign(I l,I r,I x){
auto itr=split(r+1),itl=split(l);//先分裂出 [r+1,+inf] 的右边界,才能分离出 [l,r] 的可行部分,itl->l 就是 l(这个写分裂合并数据结构的大概都知道)
s.erase(itl,itr);//这个是前闭后开的,删除了 itl...itr-1 的区间、迭代器
s.insert(node{l,r,x});//插入一个全新的区间
}
普通操作:直接从左往右遍历珂朵莉树。
void count(I l,I r){
auto itr=split(r+1),itl=split(l);
for(;itl!=itr;++itl)//这个也是前闭后开的
//do something...
}
习题
P4979 矿洞:坍塌
题意:区间推平,单点查询,判断区间颜色是否统一。
做法
做这道题先来练手。
首先区间推平只需要 assign 即可。
单点查询?只需在 set 之中二分位置即可。
判断区间颜色是否单一?只需按照上文 count
操作即可。(还有点小优化:如果合法就可以直接统一为一个区间)
CF1638E Colorful Operations
题意:每个数有两种属性,一种颜色,一种权值。
区间颜色推平;全局某种颜色所有数的加上一个数;单点查询权值。
做法
首先观察因为颜色是不连续的区间,所以不可能一个个去查找并给它加上权值。
于是就给颜色 t t t 打上标记 b z [ t ] bz[t] bz[t] 表示当前 t t t 颜色在操作之中累加到了多少,因为不断更新很费事。
而 a a a 数组就缓慢更新,只有改变了颜色才更新,这样一个节点的真实值就是 a [ i ] + b z [ c o l [ i ] ] a[i]+bz[col[i]] a[i]+bz[col[i]] 。
如何求一个 p p p 从 c c c 颜色修改至 c ′ c' c′ 颜色对其答案的贡献呢?
因为它的真实值是 a [ i ] + b z [ c ] a[i]+bz[c] a[i]+bz[c] ,所以先算了 b z [ c ] bz[c] bz[c] 的帐,即加上 b z [ c ] bz[c] bz[c] ;
但是它不能直接存储真实值,所以要减去 b z [ c ′ ] bz[c'] bz[c′] 。
在 ODT 上每个节点都是一段区间,所以考虑使用树状数组维护。
总而言之,操作1就珂朵莉树上面的 assign 操作和区间加;操作2直接 O(1) 改变标记;操作3就直接树状数组查询权值+珂朵莉树查询颜色。
CF343D Water Tree
题意:树上推平子树;树上推平路径;单点查询。
做法
观察到本题只有区间推平和单点查询操作,所以珂朵莉树维护。
树上的操作通过树剖即可解决。
6362.【NOIP2019模拟2019.9.18】数星星(star)
题意:给 n n n 个点有点权,有 m m m 条路径,有 q q q 次操作查询 [ l , r ] [l,r] [l,r] 条路径的并的点权之和是多少。
(编者强烈建议搞定 CF343D 和 CF1638E 再来处理本题)
做法
看这么一个东西:HH的项链,就是区间统计个数,相同的只统计一次。
因为可以离线,所以按照 r r r 递增排序,不断加入 r r r ,然后更新一下每个数最后出现在哪里(其实就是权值等于 a [ r ] a[r] a[r] 的数)。对于 [ l , r ] [l,r] [l,r] 询问,查询最晚出现位置大于 l − 1 l-1 l−1 的数的个数。
然后考虑迁移到这题之中:也是离线下来,按照 r r r 递增排序,不断加入 r r r ,然后更新一下每个点最后出现在第几个询问。对于 [ l , r ] [l,r] [l,r] 依旧是查询最晚出现位置大于 l − 1 l-1 l−1 的点。对于一条路径更新路径上所有点的最晚出现位置,查询所有最晚出现位置大于 l − 1 l-1 l−1 的点权之和。
再写得显然一些,树链剖分之后(即在 dfs 序上做区间操作),区间推平;查询大于等于 l l l 的数的权值之和(可以转化成小于等于 l − 1 l-1 l−1 数的个数,初始出现位置 0 0 0 就好)。
这个大于等于 l l l 的数的点权之和就可以直接丢进树状数组里面处理;对于区间推平,直接使用 ODT 维护即可。因为 ODT 把每个区间压缩成为了一个点,所以加减的话也要直接加减这个区间的点权之和。因为是被推平,所以要减去之前的贡献,增加现在的贡献(和 CF1638E 一样)。
实现就显而易见了。
课后练习题
CF896C Willem, Chtholly and Seniorious 万 恶 之 源
CF915E Physical Education Lessons 比模板题还简单 都是暴力有什么难的
CF817F MEX Queries 和上题差不多,但是值域超大,珂朵莉树能做
P5350 序列 平衡树的题和珂朵莉树有什么关系?
CF1423G Growing flowers *3500?*800!