珂朵莉
我永遠喜歡珂朵莉。
如果幸福有顏色,那一定是終末之紅染盡的藍色!
萌娘百科:
珂朵莉樹
珂朵莉樹是基於 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));
}
感覺好像之前寫的比較好看點……
定期重構
說實話,這種東西我就見過一次。
熱知識,每個字上都可以放一個鏈接。
但感覺好像有點像塊狀鏈表?
等我學會了就來補文章。