颜色均摊段(ODT)学习笔记
其实我更喜欢叫她珂朵莉树,尽管这个东西和树没什么关系。
故事的一切起源于 CF896C Willem, Chtholly and Seniorious,本题基于随机数据,提出了一种理论复杂度错误,实际运行情况优异的算法。因为本题的题目背景,因此得名珂朵莉树,另外一个名字 Old Driver Tree (缩写 ODT)则是因为本题的出题人昵称为 ODT。当然,根据这种算法的特征,这个东西还有一个正式名字:颜色均摊段。
算法思想
对于数据结构题,我们有这样一种思路去维护:对于一个数列,我们把不同的数字看成不同的颜色段,然后对每个颜色段进行暴力操作,可以有效降低时间复杂度。但这种暴力是很好卡掉的,只需让颜色段尽可能多,算法就可以直接退化的 \(O(n^2)\),甚至在随机数据下,这种算法的表现依然不是很好。
但在特定的情况下,当题目中的颜色段很少(例如:存在区间赋值操作且数据随机),这种算法就可以达到 \(O(n\log n)\) 等极好的时间复杂度,于是就诞生了使用 set 维护连续颜色段的算法。下文以 CF896C 作为模板题讲解这种算法。
写一个数据结构,支持以下操作:
1 l r x:将\([l,r]\) 区间所有数加上\(x\)2 l r x:将\([l,r]\) 区间所有数改成\(x\)3 l r x:输出将\([l,r]\) 区间从小到大排序后的第\(x\) 个数是的多少(即区间第\(x\) 小,数字大小相同算多次,保证 \(1\leq\) \(x\) \(\leq\) \(r-l+1\))4 l r x y:输出\([l,r]\) 区间每个数字的\(x\) 次方的和模\(y\) 的值,即\(\left(\sum^r_{i=l}a_i^x\right)\)\(\bmod y\)。
其中 \(n,m\le 10^5\),所有操作随机生成。
算法实现
珂朵莉树实际上是一个 set,set 中的每个节点都是一个颜色段,颜色段用结构体表示其内部信息。
struct cht{
int l,r;
mutable ll v;//mutable 用于快速修改颜色
cht(int L,int R,ll V){l=L,r=R,v=V;}//变量类型有差异,所以要单独写初始化函数
bool operator < (const cht &a)const {return l<a.l;}//默认按照颜色段左端点排序
};
typedef set<cht>::iterator iter;//迭代器,就是指针,接下来的各种操作都会常用
set<cht>s;
复杂度正确的部分
珂朵莉树的核心部分是 split(分裂) 和 assign(覆盖)这两个函数函数,其时间复杂度均为单次 \(O(\log n)\)。
split
给定一个参数 \(x\),split 函数负责返回 set 中以 \(x\) 为左边界的颜色段的位置(指针)。特别的,如果 \(x\) 位于颜色段 \(l,r\) 内,并非是端点,则将 \([l,r]\) 分裂为 \([l,x-1],[x,r]\) 并返回后者的指针;如果不存在 \(x\) 这个位置,则返回 set 的尾指针,一般我们习惯于在最后插入一个空节点减少特判。步骤其实十分简单:
找到 \(x\) 所在颜色段。
如果 \(x\) 是该颜色段的端点,直接返回指针。
否则分裂,返回分裂后的结果。
iter split(int pos){
iter it=s.lower_bound(cht{pos,0,0});//查找颜色段
if(it->l==pos)return it;//判断是否是端点,前面的特判是为了避免不存在 pos 导致 RE
it--;//此时的 it 指向的颜色段包含 pos
int L=it->l,R=it->r;ll V=it->v;
s.erase(it);//分裂
s.insert(cht{L,pos-1,V});
return s.insert(cht{pos,R,V}).first; //insert 返回的 pair 的第一个参数是新插入的位置的迭代器
}
assign
区间推平操作,负责将一段区间赋为同一颜色。
我们要做的就是先分裂出以 \(l,r\) 为端点的颜色段,记为 \(s,t\),然后将编号 \([s,t)\) 以内的所有颜色段删除,然后再插入一个以 \([l,r]\) 为端点的大颜色段。
set 支持删掉某个范围内的所有元素,调用 erase 即可删除 set 中所有位于 \([s,t)\) 的元素,且操作复杂度 \(O(\log n)\)。
void assign(int l,int r,ll k){
iter itr=split(r+1),itl=split(l);//r+1 是因为 erase 函数遵循左闭右开原则
s.erase(itl,itr);
s.insert(cht{l,r,k});
}
注意到我们先调用了对 \(r+1\) 分裂后再分裂了 \(l\)。这个顺序是不可改变的。因为具体解释比较麻烦,所以我盗了一张大佬的图(uid:43206)

注意到调用先分裂 \(l\) 后再分裂 \(r+1\) 可能会导致 \(itl\) 原本指向的颜色段被删除分裂成两个,所以我们必须按照正确的顺序分裂。
复杂度不正确的部分
其实非常简单,就是分裂出左右颜色段,然后暴力遍历其中的所有颜色段.....
是的,就是这么简单,给出一份代码。
change(int l,int r,...){
iter itr=split(r+1),itl=split(l);
for(iter it=itl;it!=itr;it++){
/*
do something
*/
}
/*
do something
*/
}
理论复杂度单次为 \(O(n)\),随机数据下期望复杂度单次 \(O(\log n)\),接下来会给出若干操作的事例。
区间加:
void add(int l,int r,ll k){
iter itr=split(r+1),itl=split(l);
for(iter it=itl;it!=itr;it++){
it->v+=k;//可以直接改变结构体的成员是因为使用了 mutable
}
return;
}
区间第 \(k\) 小:
#define pli pair<ll,int>
#define mp make_pair
typedef long long ll;
pli bk[N];
int tot;
ll kth(int l,int r,int rk){
tot=0;
iter itr=split(r+1),itl=split(l);
for(iter it=itl;it!=itr;it++){
bk[++tot]=mp(it->v,it->r-it->l+1);
}
sort(bk+1,bk+1+tot);
for(int i=1;i<=tot;i++){
if(rk<=bk[i].second)return bk[i].first;
rk-=bk[i].second;
}
return -1;
}
区间幂的和:
这个操作实际上复杂度是 \(O(\log^2 n)\) 的。
ll ksm(ll a,ll b,ll p){
ll ans=1;
while(b){
if(b&1)ans=(ans*a)%p;
a=(a*a)%p;
b>>=1;
}
return ans;
}
ll pow_sum(int l,int r,ll x,ll y){
iter itr=split(r+1),itl=split(l);
ll res=0;
for(iter it=itl;it!=itr;it++){
res+=(it->r-it->l+1)*ksm(it->v%y,x,y)%y;
res%=y;
}
return res;
}
然后这题就做完了,提交记录。
时间复杂度
理论复杂度单次为 \(O(n^2)\),随机数据下期望复杂度 \(O(n\log n)\),具体不会证明。
例题讲解
P5350 序列
这个题正解好像是较为诡异的可持久化平衡树,但因为数据太水被发现 ODT 加 O2 可以草过去于是变成了 ODT 的一大模板题。
前三个操作可以用 ODT 随便做。
复制操作可以记录两个区间的下标差,删掉区间 \([l_2,r_2]\) 一个一个取出 \([l_1,r_1]\) 内的颜色段,修改下标后插入 set 即可。
交换操作最为复杂,我们必须先将 \([l_1,r_1]\) 内的所有颜色段拷贝下来后,删掉这个 \([l_1,r_1]\),然后将 \([l_2,r_2]\) 内的颜色段复制过去,在将拷贝的部分复制到 \([l_2,r_2]\) 的位置上,这里需要注意,操作前必须满足 \(l_1<l_2\),不满足就要调整;原因在于 set 所操作的区间均为左闭右开,若先操作后面的区间,会导致前面的区间的右边界向前移动,而右边界是开的,所以会少插入数。但如果始终先操作前面的区间,会导致后面的区间的左边界向后移动,而左边界是闭的,所以不会少插入,详细可以看这里,由此,对于两个区间的操作,我们需要慎重考虑操作的顺序。
翻转操作比较简单,记一个区间 \([l,r]\) 在区间 \([L,R]\) 翻转后翻到了区间 \([l',r']\),注意到翻转前后区间关于 \(\dfrac{L+R}{2}\) 对称,所以可以得到 \(l'=L+R-r,r'=L+R-l\),拷贝下来后再插回去就做完了。
时间复杂度是 \(O(跑的过)\)。record。
[ABC371F] Takahashi in Narrow Road
省选 2025 D2T1 严格弱化版。
记录每个人的位置 \(x_i\),注意到每次操作完后,会有一段区间的 \(x_i\) 呈单调上升趋势,且相邻两项差为 \(1\)。不好做,我们令 \(a_i\gets a_i-i\),则就变成了每次操作会让一段区间的 \(x_i\) 变成同一个值,所以找到对应块后直接暴力推就可以,因为只有 split 每次多分裂一个块,而没暴力推一次,都会减少一个块,所以所有操作中颜色段变化数为 \(n\) 级别的,所以珂朵莉树复杂度是正确的,时间复杂度 \(O(n\log n)\)。
P8512 [Ynoi Easy Round 2021] TEST_152
有意思的题,先不思考询问,单纯从前往后把 \(n\) 个操作推一遍,发现可以用上一题的方法,且所有操作中颜色段变化数为 \(n\) 级别的,一个颜色段对答案的贡献即为颜色乘长度。考虑加上区间限制,套路进行扫描线,当扫到区间 \(r\) 时,考虑减去 \([1,l-1]\) 操作的影响。一个颜色只会被它后面染上的颜色所覆盖,所以只需要计算每个 \([1,l-1]\) 的颜色段在扫到 \(r\) 时还对答案有多少贡献。
考虑维护时间轴,时间轴上的第 \(i\) 个位置表示,当前扫到 \(r\),操作 \(i\) 对答案剩下的贡献,若用第 \(i\) 个颜色去覆盖序列,则在时间轴位置 \(i\) 加上 \((r_i-l_i+1)\times v_i\),若颜色 \(i\) 被其他颜色覆盖,则在时间轴位置 \(i\) 加上 \(-len\times v_i\),\(len\) 表示被覆盖的区间长度。
扫描线时要对时间轴单调加,处理询问时要对时间轴区间和,所以用树状数组维护时间轴,珂朵莉树复杂度同上一题,总时间复杂度 \(O(n\log n)\)。record。
The End
一些闲话。
最近集训时发现很多同学都在学珂朵莉树,恍惚间想起自己去年(2024.10.4)貌似写过一次 ODT 的学习笔记,但并没有特别认真。到了暑假,又尝试给原版的文章优化了一下,增添了几道例题。
当我再次再晚上听末日三问的片头曲,思绪又不由得带回到了去年的夏天,第一句歌词所带来的穿透力仍然没有减弱。最近也打算补完这个系列的轻小说部分,虽然对剧情的一言难尽略有耳闻,不过还是打算鼓足勇气看完。
时至今日,我还会像过去一样说出“我永远喜欢珂朵莉”这样的话吗?我自己也不知道答案,其实无所谓了,毕竟觉得自己对这个系列的热情还没有消散,最后的最后还是放一张朋友圈的背景图。

还是趁早睡吧,最近可能会整理一些远古的博客,大部分都因为我去年摆烂到现在都没整理。
upd by 2025.7.14
颜色均摊段(ODT)学习笔记的更多相关文章
- compass color 颜色 对比色[Sass和compass学习笔记]
最基本的api 是对比色,对与我这种菜鸟来说,没有什么比在一个背景色下 用什么颜色的文字坑蛋疼的事情了,这个工具可以帮助大家很好解决这个问题 api 地址 http://compass-style.o ...
- 【学习笔记】珂朵莉树(ODT)
珂朵莉树 \(\tt 0x00\) 起源 起源于 CodeForces 的一题 CF896C,当时出题人提供了这种做法,在随机数据下均摊复杂度比较优秀. 正统名字好像叫颜色段均摊,由于题目也得名于 \ ...
- BZOJ 2120 数颜色&2453 维护队列 [带修改的莫队算法]【学习笔记】
2120: 数颜色 Time Limit: 6 Sec Memory Limit: 259 MBSubmit: 3665 Solved: 1422[Submit][Status][Discuss] ...
- Swagger学习笔记
狂神声明 : 文章均为自己的学习笔记 , 转载一定注明出处 ; 编辑不易 , 防君子不防小人~共勉 ! Swagger学习笔记 课程目标 了解Swagger的概念及作用 掌握在项目中集成Swagger ...
- Dynamic CRM 2013学习笔记(二十六)报表设计:Reporting Service报表 动态参数、参数多选全选、动态列、动态显示行字体颜色
上次介绍过CRM里开始报表的一些注意事项:Dynamic CRM 2013学习笔记(十五)报表入门.开发工具及注意事项,本文继续介绍报表里的一些动态效果:动态显示参数,参数是从数据库里查询出来的:参数 ...
- shell编程学习笔记(一):编写我的第一段代码
目前在学习Shell编程,我会把我的学习笔记记录在这里.大神可以直接略过~ 嗯,第一段代码,肯定是要输出Hello World了~ 以下蓝色字体的内容为linux命令,红色字体的内容为输出的内容: # ...
- canvas学习笔记(中篇) -- canvas入门教程-- 颜色/透明度/渐变色/线宽/线条样式/虚线/文本/阴影/图片/像素处理
[中篇] -- 建议学习时间4小时 课程共(上中下)三篇 此笔记是我初次接触canvas的时候的学习笔记,这次特意整理为博客供大家入门学习,几乎涵盖了canvas所有的基础知识,并且有众多练习案例, ...
- matlab学习笔记9 高级绘图命令_2 图形的高级控制_视点控制和图形旋转_色图和颜色映像_光照和着色
一起来学matlab-matlab学习笔记9 高级绘图命令_2 图形的高级控制_视点控制和图形旋转_色图和颜色映像_光照和着色 觉得有用的话,欢迎一起讨论相互学习~Follow Me 参考书籍 < ...
- Linux Shell输出颜色字符学习笔记(附Python脚本实现自动化定制生成)
齿轮发出咔嚓一声,向前进了一格.而一旦向前迈进,齿轮就不能倒退了.这就是世界的规则. 0x01背景 造了个轮子:御剑师傅的ipintervalmerge的Python版本.觉得打印的提示信息如果是普通 ...
- 我的Android进阶之旅------>Android中编解码学习笔记
编解码学习笔记(一):基本概念 媒体业务是网络的主要业务之间.尤其移动互联网业务的兴起,在运营商和应用开发商中,媒体业务份量极重,其中媒体的编解码服务涉及需求分析.应用开发.释放license收费等等 ...
随机推荐
- 为什么 Java 8 移除了永久代(PermGen)并引入了元空间(Metaspace)?
为什么 Java 8 移除了永久代(PermGen)并引入了元空间(Metaspace)? 在 Java 8 中,JVM 移除了 永久代(PermGen)并引入了 元空间(Metaspace),这一改 ...
- 关于:js怎么获取元素的自定义属性的问题(原生JavaScript)
最近项目需要把后端传过来的数据隐藏的保存在页面中,方便后边做事件处理时使用.鉴于之前总是在后端处理后的页面中看到元素里除了常见的id.name属性外的data-xxx,就想到:元素的属性必然是可以自定 ...
- wpf 控件绑定鼠标命令、键盘命令
1 <Window x:Class="CommandDemo.MainWindow" 2 xmlns="http://schemas.microsoft.com/w ...
- XXL-CACHE v1.2.0 | 多级缓存框架
Release Notes 1.[增强]多序列化协议支持:针对L2缓存,组件化抽象Serializer,可灵活扩展更多序列化协议: 2.[优化]移除冗余依赖,精简Core体积: XXL- CACHE ...
- 【记录】ChatGPT|图片预览魔法咒语魔改,使用 ChatGPT 返回大量可以跳转的链接
很早的时候,我已经留意到 ChatGPT 会以返回图片的 markdown 格式来显示图片,很可能拥有一定的图片上传功能,但是它往往会显示得有些问题.一些代码图片之类的或者风景图什么的都不是很会. 但 ...
- 参考用例之“本地Excel导入系统测试方案”
本地Excel导入系统测试方案 Excel 代码 @Test public void importperson() throws FileNotFoundException { FileInputSt ...
- vue3 基础-传送门 teleport
之前介绍了一波混入 mixin 和 自定义指令 directive 其基本作用就是为了在 vue 中实现代码的复用. 而本篇介绍的是 vue3 的一个新特性叫做传送门. 一听这个名字是不是就感觉特别科 ...
- 在CentOS 7虚拟机上正确安装Redis
在CentOS 7虚拟机上正确安装Redis,可以按照以下步骤进行操作: 更新系统软件包:sudo yum update 安装Redis依赖库:sudo yum install epel-releas ...
- C语言:高级语言怎样抽象执行逻辑
平时我们做编程的时候,底层 CPU 如何执行指令已经被封装好了,因此你很少会想到把底层和语言编译联系在一起.但从我自己学习各种编程语言的经历看,从这样一个全新视角重新剖析 C 语言,有助于加深你对它的 ...
- AI模型的回调能力的理解和实现
前言 BigTall最近把RAG和Agent的原理想通了,对于"一切都是提示词"的理解又更多了一些.本文把我的理解大致整理了一下,给出BigTall自己的一个实验.希望能够对大家有 ...