珂朵莉
我永远喜欢珂朵莉。
如果幸福有颜色,那一定是终末之红染尽的蓝色!
萌娘百科:
珂朵莉树
珂朵莉树是基于 set
的暴 (pian) 力 (fen) 算法。
前置知识
优点
珂朵莉全身都是优点。
码量小,思路清晰易查错。
应用范围
-
含推平操作,即将一个区间的数全部更新为相同的数。
-
数据随机(防止毒瘤出题人卡珂朵莉)。
基本思想
用 set
储存三元组 \((left,right,value)\) ,将部分连续相同的元素储存到一起。
核心
split
是整个 ODT
的核心操作。
因为三元组存的区间和每次操作维护的区间不一定完全相同,所以操作之前我们要将区间先分裂,然后再维护。
直观感受
假设现在的珂树长这样:
接下来要将区间 \(5\sim10\) 推平为 \(-3\) 。
-
将区间 <4,5,7> 和 <6,14,2> 分裂成 <4,4,7>,<5,5,7>,<6,10,2>,<11,14,2> 。
-
将 <5,5,7> 和 <6,10,2> 删掉。
-
将 <5,10,-3> 加入。
此时的珂树长这样:
这就是下面的分裂 (split) 和推平 (assign) 操作。
实现
基本概念与储存
只需要存 set
需要的三元组,然后用重载运算符,让 set
按照区间的左端点排序。
struct C_Tree{
int le,ri;
mutable int val;
C_Tree(int le,int ri=0,int val=0):
le(le),ri(ri),val(val){}
bool operator <(const C_Tree &b)const
{
return le<b.le;
}
};
set<C_Tree>T;
分裂
将区间 \([l,r]\) 分裂成 \([l,now-1]\) 和 \([now,r]\)。
显然,让 now
为区间左端点的时候并不需要分裂。
否则需要分裂。删除原区间后将两端插入即可。
#define IT set<C_Tree>::iterator
IT split(int now)
{
IT i=T.lower_bound(C_Tree(now));
if(i!=T.end()&&i->le==now)return i;
i--;int l=i->le,r=i->ri,v=i->val;
T.erase(i);
T.insert(C_Tree(l,now-1,v));
return T.insert(C_Tree(now,r,v)).first;
}
推平
如果只分裂,那么 set
的大小就会增大直到达到 n ,此时我们再用 set
就会很慢。所以需要将一个区间推平。
先把要推平的区间分裂出来,然后一并删除,将新区间插入。
void assign(int l,int r,int k)
{
IT ir=split(r+1),il=split(l);
tre.erase(il,ir);
tre.insert(C_tree(l,r,k));
}
暴力
用 set
维护好三元组之后,剩余的操作就基于 set
进行暴力维护就好了。
比如这个非官方模板。
- 区间加
void update(int l,int r,int k)
{
IT ir=split(r+1),il=split(l);
while(il!=ir)
{
il->val+=k;
il++;
}
}
- 区间查值
typedef pair<int,int>Pr;
int ask_rank(int l,int r,int k)
{
vector<Pr>ans_;
IT ir=split(r+1),il=split(l);
for(IT i=il;i!=ir;i++)
ans_.push_back((Pr){i->val,((i->ri)-(i->le)+1)});
sort(ans_.begin(),ans_.end());
vector<Pr>::iterator i=ans_.begin();
while(i!=ans_.end())
{
k-=i->second;
if(k<=0)return i->first;
i++;
}
return -1;
}
- 区间次幂和
int ksm(int a,int b,int p)
{
int ans=1;a%=p;
while(b)
{
if(b&1)ans=(ans*a)%p;
a=(a*a)%p;
b>>=1;
}
return ans;
}
int ask_sum(int l,int r,int x,int y)
{
int ans=0;
IT ir=split(r+1),il=split(l);
for(IT i=il;i!=ir;i++)
ans=(ans+ksm(i->val,x,y)*(i->ri-i->le+1))%y;
return ans;
}
关于对适用条件的解释
-
含推平
没有推平操作会让
set
的大小趋近甚至达到 \(O(n)\) 的级别,那么用珂朵莉树就会 T 到飞起。 -
数据随机
这样可以有较大概率将某段区间合并,从而将
set
的大小急剧下降,使其大小稳定在 \(O(\log n)\) 的范围左右,而不像某些 毒瘤题目 (@ 序列操作 ) 将珂朵莉树卡的死死的。像这样:
话说,前三个点跑的还挺快。所以,随机数据下的珂朵莉树的时间复杂度就是 \(O(n\log \log n)\) 的。
例题
部分含树剖
以下则是看似能用珂朵莉树实则都会被卡的题
优化
对于某些不保证随机数据的题目,如果一时想不到正解,想用珂朵莉树骗分,但是又怕毒瘤出题人将珂朵莉树卡的连渣都不剩,那么可以试着对珂朵莉树加一些小优化。
不过说实话,网上对于优化这种东西讲的真不多,可能是感觉不优化用来骗分已经足够了。
启发式推平
这名字是我胡扯的……
其实就是在每次 assign 的时候,比较一下其与两侧的块是否相同,如果相同则一起合并。
这种优化在大多数题中应该是没有什么作用的,但在权值比较少的题中则表现的很优秀。
比如这个题,权值只有 \(0,1\)。
assign
只需要这样写:
void assign(int l,int r,int k)
{//此代码写于 2022.10.13
IT ir=split(r+1),il=split(l);
IT pre=il;pre--;
if(pre->val==k)il=pre,l=il->le;
if(ir->val==k)r=ir->ri,ir++;
T.erase(il,ir);
T.insert(C_Tree(l,r,k));
}
由于迭代器的特性,加加减减的东西写出来都特别丑……
还有这个题,权值只有 \(A,B,C\)。
我是这样写的:
void assign(int l,int r,char ch)
{//此代码写于 2022.04.29
IT ir=split(r+1),il=split(l);
il--;
if(ch!=il->val)il++;
if(ch==ir->val)ir++;
l=il->le;r=ir->ri;
T.erase(il,ir);
T.insert(C_Tree(l,r,ch));
}
感觉好像之前写的比较好看点……
定期重构
说实话,这种东西我就见过一次。
热知识,每个字上都可以放一个链接。
但感觉好像有点像块状链表?
等我学会了就来补文章。