題解 CF896C 【Willem, Chtholly and Seniorious】


貌似珂朵莉樹是目前為止(我學過的)唯一一個可以維護區間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。


免責聲明!

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



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