珂朵莉树

珂朵莉

我永远喜欢珂朵莉。

如果幸福有颜色,那一定是终末之红染尽的蓝色!

一个 dalao

萌娘百科:

珂朵莉树

珂朵莉树是基于 set 的暴 (pian) 力 (fen) 算法。

前置知识

优点

珂朵莉全身都是优点。

码量小,思路清晰易查错。

应用范围

  • 含推平操作,即将一个区间的数全部更新为相同的数。

  • 数据随机(防止毒瘤出题人卡珂朵莉)。

基本思想

set 储存三元组 \((left,right,value)\) ,将部分连续相同的元素储存到一起。

核心

split 是整个 ODT 的核心操作。

因为三元组存的区间和每次操作维护的区间不一定完全相同,所以操作之前我们要将区间先分裂,然后再维护。

直观感受

假设现在的珂树长这样:

\[\begin{array}{|c|c|c|c|} \hline 1,3,4&4,5,7&6,14,2&15,15,2\\ \hline \end{array} \]

接下来要将区间 \(5\sim10\) 推平为 \(-3\)

  1. 将区间 <4,5,7> 和 <6,14,2> 分裂成 <4,4,7>,<5,5,7>,<6,10,2>,<11,14,2> 。

  2. 将 <5,5,7> 和 <6,10,2> 删掉。

  3. 将 <5,10,-3> 加入。

此时的珂树长这样:

\[\begin{array}{|c|c|c|c|c|} \hline 1,3,4&4,4,7&5,10,-3&11,14,2&15,15,2\\ \hline \end{array} \]

这就是下面的分裂 (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;
}

关于对适用条件的解释

  1. 含推平

    没有推平操作会让 set 的大小趋近甚至达到 \(O(n)\) 的级别,那么用珂朵莉树就会 T 到飞起。

  2. 数据随机

    这样可以有较大概率将某段区间合并,从而将 set 的大小急剧下降,使其大小稳定在 \(O(\log n)\) 的范围左右,而不像某些 毒瘤题目 (@ 序列操作 ) 将珂朵莉树卡的死死的。

    像这样:

    话说,前三个点跑的还挺快。

    所以,随机数据下的珂朵莉树的时间复杂度就是 \(O(n\log \log n)\) 的。

例题

部分含树剖

以下则是看似能用珂朵莉树实则都会被卡的题

P2572

P2787

P4344

优化

对于某些不保证随机数据的题目,如果一时想不到正解,想用珂朵莉树骗分,但是又怕毒瘤出题人将珂朵莉树卡的连渣都不剩,那么可以试着对珂朵莉树加一些小优化。

不过说实话,网上对于优化这种东西讲的真不多,可能是感觉不优化用来骗分已经足够了。

启发式推平

这名字是我胡扯的……

其实就是在每次 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));
}

感觉好像之前写的比较好看点……

定期重构

说实话,这种东西我就见过一次。

热知识,每个字上都可以放一个链接。

但感觉好像有点像块状链表?

等我学会了就来补文章。

posted @ 2022-02-09 15:05  Zvelig1205  阅读(665)  评论(0编辑  收藏  举报