珂朵莉树/老司机树学习笔记
前言
好难受啊,我好菜啊,完全不知道要干什么,就写了这篇专栏放放松。
介绍
珂朵莉树(Chtholly Tree)也叫老司机树(ODT),是一种基于平衡树(一般用 set
或 map
实现)的暴力算法,可用于解决区间修改、区间加法、区间查询以及其他区间操作,在处理区间修改效率最高,适用于随机数据。
实现
从 P3740 [HAOI2014] 贴海报 开始
虽然这题并不是经典的珂朵莉树题,但因为这题只需实现 split
和 assign
操作,比较简单,我就把它放到最前面好了。
题意大致是:给出一个区间
可以用一些奇怪的办法做,比如线段树+离散化,浮水法之类的(我没写过不知道),但这不重要,我们开始讲珂朵莉树。
珂朵莉树本质上就是维护若干个区间,每个区间中的所有值都相等,相邻区间中的值可以相等。我们用一个结构体来表示一个区间:
struct node {
int l, r;
mutable int val; // 让 val 保持可变状态
node(int a, int b=-1, int c=0) { // 构造函数
l=a, r=b, val=c;
}
bool operator<(const node &oth)const { // 比较函数,后面会用
return l<oth.l;
}
};
然后我们有一堆 node
即区间,把他们用 set
排成一排:
set<node> cht; // 珂朵莉树 Chtholly Tree
然后我们要进行区间赋值,但如果区间赋值的边缘与当前区间的边缘不一致就比较困难,这个时候我们可以考虑把目前的区间分割一下再处理。
比如原本的区间像下面这样:
而下图中蓝色部分是我们要修改的区间:
然后我们把第
现在事情就好办了,我们要写
auto split(int pos) { // 分割成 [l, pos) [pos, r] 返回[pos, r]
auto it=cht.lower_bound(node(pos)); // 找到第一个区间 [l,r] 使得 l>=pos
if(it!=cht.end() && it->l==pos) return it; // 不用分割
it--; // 到左边去 此时 l<=pos<r
int l=it->l, r=it->r, val=it->val;
cht.erase(it); // 删掉此区间
cht.insert(node(l, pos-1, val)); // 插入左区间
return cht.insert(node(pos, r, val)).first; // 插入并返回右区间
}
inline void assign(int l, int r, int v) { // 赋值 把 [l, r] 中所有值改成 val
auto itr=split(r+1), itl=split(l); // 分割 这里分 r+1 是因为 erase 删除的是一个左闭右开区间
cht.erase(itl, itr);
cht.insert(node(l, r, v)); // 插入新节点
}
接着让我们回到上面那题,就可以发现这是一道裸的珂朵莉树题,修改上面已经讲过了,求最终答案时,只要遍历 set
,再用一个 unordered_set
对颜色去重即可:
#include <bits/stdc++.h>
using namespace std;
int n, m;
struct node {
int l, r;
mutable int val;
node(int a, int b=-1, int c=0) {
l=a, r=b, val=c;
}
bool operator<(const node &oth)const {
return l<oth.l;
}
};
set<node> cht;
auto split(int pos) {
auto it=cht.lower_bound(node(pos));
if(it!=cht.end() && it->l==pos) return it;
it--;
int l=it->l, r=it->r, val=it->val;
cht.erase(it);
cht.insert(node(l, pos-1, val));
return cht.insert(node(pos, r, val)).first;
}
inline void assign(int l, int r, int v) {
auto itr=split(r+1), itl=split(l);
cht.erase(itl, itr);
cht.insert(node(l, r, v));
}
unordered_set<int> ans;
int main() {
ios::sync_with_stdio(0);
cin.tie(nullptr);
cin>>n>>m;
cht.insert(node(1, n));
for(int i=1; i<=m; i++) {
int l, r;
cin>>l>>r;
assign(l, r, i);
}
for(auto i : cht) {
ans.insert(i.val);
}
cout<<ans.size()-ans.count(0);
return 0;
}
简单练习一下(用珂朵莉树可能过不了,但却是是练习的好题)
-
P1840 Color the Axis
-
P4344 [SHOI2015] 脑洞治疗仪
-
P1558 色板游戏
-
P4979 矿洞:坍塌
再看模板题 CF896C Willem, Chtholly and Seniorious
其实把这题的随机函数去掉,就只是要我们维护区间加法、区间赋值、区间第
对了,在处理之前,为了方便,我们先定义
auto itr=split(r+1), itl=split(l);
再来进行处理。
- 区间加法
很简单,把 itl
到 itr
中的每个区间的值都加上
for(; itl!=itr; itl++) itl->val+=x;
- 区间赋值
直接调用 assign
。
cht.erase(itl, itr);
cht.insert(node(l, r, x));
- 区间第
x 小
我认为这个操作十分良好地体现了珂朵莉树“暴力”的特点,我们把 itl
到 itr
中的每个区间 按其值排序,然后遍历找第
vector<pair<ll, ll> > arr; // 定义一个数组 first 为区间值 second 为区间长度
arr.clear(); // 清空
for(; itl!=itr; itl++) {
ll v=itl->val;
arr.push_back({v, itl->r-itl->l+1}); // 加入到末尾
}
sort(arr.begin(), arr.end()); // 按值排序
for(auto i: arr) { // 遍历
x-=i.second;
if(x<=0) { // 找到了
cout<<i.first<<"\n";
break;
}
}
- 区间
x 次幂和模y
做法和操作
ll ans=0;
for(; itl!=itr; itl++) {
ans=(ans+(itl->r-itl->l+1)*poww(itl->val%y, x, y))%y;
}
cout<<ans<<"\n";
再练习一下(纯珂朵莉树也可能过不了)
-
P2572 [SCOI2010] 序列操作
-
P8146 [JRKSJ R4] risrqnis
后记
就先写到这吧,我有点累了。
到这里大致上就把珂朵莉树的基本思想和实现讲完了,要是遇到珂朵莉树的题再做不出,就只有
- 惨遭 hack ~(我很讨厌 hack 珂朵莉树的行为,一来是珂朵莉太可爱了,二来是这样做显得珂朵莉树什么都干不了,三来是我不会线段树)~
- 模板打错
- 不够俄罗斯
话说我突然感觉珂朵莉好可爱啊 qwq。
参考文献(撒花)
珂朵莉树/颜色段均摊 - OI Wiki
珂朵莉树学习笔记 - 洛谷专栏
珂朵莉树的复杂度分析 - 知乎