貌似珂朵莉樹是目前為止(我學過的)唯一一個可以維護區間x次方和查詢的高效數據結構。
但是這玩意有個很大的毛病,就是它的高效建立在數據隨機的前提下。
在數據隨機的時候assign操作比較多,所以它的復雜度會趨近於mlogn(m為詢問次數)。假如出題人想要卡珂朵莉樹的話,那肯定是會T得沒邊。
因此不要指望什么題目都套珂朵莉樹(雖然它能水過很多數據結構題),特別是在數據非隨機的情況下不要使用。
當然,如果題目讓你求區間x次方和而在題目條件下你想不出巧算,那寫一顆珂朵莉樹還是很OK的。
不得不說珂朵莉樹的博客我也看了很多篇了,大家卻一筆帶過(可能是我太弱了)細節只講大致框架,而某大佬在B站上的視頻講解被某不知名的毒瘤lxl噴了所以沒敢去看,只好一個人頹代碼。
我講的也不一定多標准,有什么錯誤麻煩各位指正。
另:不太熟悉set的可以參考這篇博客
珂朵莉樹的核心操作在於推平一個區間。
(貌似每篇博客都說這句話)
當然事實上珂朵莉樹是將所有要操作的區間整合到一起去做的,實現也可以不依賴set,比如自己寫一顆fhq Treap之類的。
先講一下大體思路:
可以發現,這里面有一個操作是推平一整段區間。
因此我們讓每一個節點維護一個區間,然后對於2號操作清空區間[l,r]里的所有區間,用一個大區間[l,r]取代他們。
對於1,3,4號操作,我們暴力地找到每一個[l,r]里面的區間,然后對它們各個進行操作。
時間復雜度的證明可以參考發明者的原話:傳送門(注意是第五條)
珂朵莉樹的節點是這樣定義的:
1 struct node{ 2 int l,r;mutable ll v; 3 node(int L,int R=-1,ll V=0):l(L),r(R),v(V){} 4 bool operator < (const node &o)const{ 5 return l<o.l; 6 } 7 };
這個節點維護的是區間[l,r],里面的每個數都初始化為v。
有了基本的節點之后,通過set建立一棵樹。
(set是C++自帶的平衡樹,但是慢到一種境界。只有在刷時限給力的題目時推薦)
1 set<node> s;
然后是很核心的split操作,這個操作如同它的名字,將一個區間拆分開來。
1 #define IT set<node>::iterator 2 IT split(int pos){ 3 IT it=s.lower_bound(node(pos)); 4 if(it!=s.end()&&it->l==pos)return it; 5 it--; 6 int L=it->l,R=it->r;ll V=it->v;s.erase(it),s.insert(node(L,pos-1,V)); 7 return s.insert(node(pos,R,V)).first; 8 }
IT代表的玩意建議用宏,手打可以讓你懷疑人生。
第一行使用了lower_bound,這個函數的作用是求前驅。
然后我們先看是否需要split這個區間,如果不需要就直接返回it。
假如現在程序還在運行,那說明我們需要split。
因此pos肯定在上一個區間里(顯然),那我們把前一個徹底抹掉,然后再插入兩段區間。
現在看來我們什么事情都沒有做,刪掉了區間又把它放回來了。
注意,其實我們並不是什么事情都沒有做,因為我們在這個過程中已經拿到了需要的東西:后半段區間的迭代器(什么用后面說)
最后的返回語句可能比較玄學,事實上,set的insert操作返回一個<iterator,bool>的pair,我們只拿走第一個。
split操作就這樣結束了,它的復雜度應該是log級的(set通過紅黑樹實現,而那玩意(我沒寫過)的操作據說是近似logn的)。
同樣核心的assign操作,是將一個區間的每一個值都設為一個值。
1 void assign(int l,int r,int val=0){ 2 IT itl=split(l),itr=split(r+1); 3 s.erase(itl,itr),s.insert(node(l,r,val)); 4 }
val默認為0(因為很多時候我們直接推平)。
首先我們拿到itl和itr,這兩個東西分別是split(l)與split(r+1)的返回值,看起來可能不太好理解,但是畫個圖似乎挺明了的。
然后我們把中間的都刪除(這是erase的另一種用法,刪除區間內的所有元素),用一個大區間代替所有小區間。
沒了,時間復雜度很能接受。
add操作,給區間每個數加上val。
1 void add(int l,int r,ll val=1){ 2 IT itl=split(l),itr=split(r+1); 3 for(;itl!=itr;++itl)itl->v+=val; 4 }
像我們之前說的那樣,對於[l,r]內的所有小區間,暴力遍歷一遍,給他們每一個都加上val。
可能有人會問:不會有加重復或者漏加的情況嗎?
事實上不會。
漏加這個很明顯是沒有的,而重復加之所以沒有是因為
1. 最開始沒有重復。
2. 每一次推平不會產生重復。
接下來是求區間k小值。
1 ll rank(int l,int r,int k){ 2 vector<pair<ll,int> >vp;vp.clear(); 3 IT itl=split(l),itr=split(r+1); 4 for(;itl!=itr;++itl)vp.push_back(pair<ll,int>(itl->v,itl->r-itl->l+1)); 5 sort(vp.begin(),vp.end()); 6 for(vector<pair<ll,int> >::iterator it=vp.begin();it!=vp.end();++it){ 7 k-=it->second; 8 if(k<=0)return it->first; 9 } 10 return -1ll; 11 }
我們采取類似的思路:把[l,r]里面的所有元素取出來,扔到一個vector里面去。
然后給這個vector排個序。
便利一遍就可以找到最小值了。
最后的return -1ll;是特判找不到的情況,當然本題保證找得到。
最后一個操作是區間x次方和,這個也十分暴力:
1 ll sum(int l,int r,int ex,int mod){ 2 IT itl=split(l),itr=split(r+1);ll res=0; 3 for(;itl!=itr;++itl)res=(res+(ll)(itl->r-itl->l+1)*power(itl->v,(ll)ex,(ll)mod))%mod; 4 return res; 5 }
對於[l,r]每一個元素都暴力x次方,這個過程通過快速冪實現。
然后珂朵莉樹的操作基本就完了。
有人問我為什么代碼都這么一樣。
我也很無奈啊,只能說我學習的那篇博客和大家重復了qwq。