珂朵莉樹


珂朵莉

我永遠喜歡珂朵莉。

如果幸福有顏色,那一定是終末之紅染盡的藍色!

一個 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));
}

感覺好像之前寫的比較好看點……

定期重構

說實話,這種東西我就見過一次。

熱知識,每個字上都可以放一個鏈接。

但感覺好像有點像塊狀鏈表?

等我學會了就來補文章。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM