吾日三省吾身:末日時在幹什麼?有沒有空?可以來拯救一下嗎?
演算法思想
非常簡單:就是暴力。
對於資料結構題,我們有這樣一種思路去維護:對於一個數列,我們把不同的數字看成不同的顏色段,然後對每個顏色段進行暴力操作,可以有效降低時間複雜度。但這種暴力是很好卡掉的,只需讓顏色段儘可能多,演算法就可以直接退化的 \(O(n^2)\),甚至在隨機資料下,這種演算法的表現依然不是很好。
但在特定的情況下,當題目中的顏色段很少(例如:存在區間賦值操作且資料隨機),這種演算法就可以達到 \(O(n\log n)\) 等極好的時間複雜度,珂朵莉樹,也叫老司機樹(英文:Old Drive Tree,簡寫:ODT)應運而生。可以支援許多複雜的操作。
演算法講解
模板:CF896C Willem, Chtholly and Seniorious
題意簡述:
寫一個資料結構,支援以下操作:
1 l r x
:將\([l,r]\) 區間所有數加上\(x\)2 l r x
:將\([l,r]\) 區間所有數改成\(x\)3 l r x
:輸出將\([l,r]\) 區間從小到大排序後的第\(x\) 個數是的多少(即區間第\(x\) 小,數字大小相同算多次,保證 \(1\leq\) \(x\) \(\leq\) \(r-l+1\))4 l r x y
:輸出\([l,r]\) 區間每個數字的\(x\) 次方的和模\(y\) 的值,即\(\left(\sum^r_{i=l}a_i^x\right)\)\(\bmod y\)。
操作用普通的資料結構難以維護,同時存在區間賦值操作,啟發我們使用珂朵莉樹。
珂朵莉樹實際上就是一個 set,set 中的每個節點都是一個顏色段。
struct cht{
int l,r;
mutable ll v;//用於快速修改顏色
cht(int L,int R,ll V){
l=L,r=R,v=V;
}
bool operator < (const cht &a)const {
return l<a.l;
}//預設按照顏色段左端點排序
};
typedef set<cht>::iterator iter;//迭代器,就是指標,接下來的各種操作都會常用
set<cht>s;
操作 \(1\):split
這是珂朵莉樹中最為重要的操作之一。
我們定義 split(pos)
表示返回以 \(pos\) 為左端點的顏色段的指標。特別的,如果 \(pos\) 位於顏色段 \(l,r\) 內,並非是端點,則將 \([l,r]\) 分裂為 \([l,pos-1],[pos,r]\) 並返回後者的指標;如果不存在 \(pos\) 這個位置,則返回 set 的尾指標。
步驟其實十分簡單:
-
找到 \(pos\) 所在顏色段。
-
如果 \(pos\) 是該顏色段的端點,直接返回指標
-
分裂,返回分裂後的結果
iter split(int pos){
iter it=s.lower_bound(cht{pos,0,0});//查詢顏色段
if(it!=s.end()&&it->l==pos)return it;//判斷是否是端點,前面的特判是為了避免不存在 pos 導致 RE
it--;//此時的 it 指向的顏色段包含 pos
if(it->r<pos)return s.end();//特判 pos 是否存在
int L=it->l,R=it->r;ll V=it->v;
s.erase(it);//分裂
s.insert(cht{L,pos-1,V});
return s.insert(cht{pos,R,V}).first; //insert 返回的 pair 的第一個引數是新插入的位置的迭代器
}
操作 \(2\):assign
區間推平,同樣也是極其重要的操作之一。
我們要做的就是先分裂出以 \(l,r\) 為端點的顏色段,記為 \(s,t\),然後將編號 \([s,t)\) 以內的所有顏色段刪除,然後再插入一個以 \([l,r]\) 為端點的大顏色段。
set 支援刪掉某個範圍內的所有元素,呼叫 erase(s,t)
即可刪除 set 中所有位於 \([s,t)\) 的元素。
void assign(int l,int r,ll k){
iter itr=split(r+1),itl=split(l);//r+1 是因為 erase 函式遵循左閉右開原則
s.erase(itl,itr);
s.insert(cht{l,r,k});
}
注意到我們先呼叫了 split(r+1)
後再呼叫了 split(l)
。這個順序是不可改變的。因為具體解釋比較麻煩,所以我盜了一張大佬的圖(by user43206)
注意到呼叫 split(l)
後再呼叫 split(r+1)
可能會導致 \(itl\) 原本指向的顏色段被刪除分裂成兩個,所以我們必須按照正確的順序分裂。
其他操作
其實非常簡單,就是分裂出左右顏色段,然後暴力遍歷其中的所有顏色段.....
是的,就是這麼簡單,給出一份虛擬碼
change(int l,int r,...){
iter itr=split(r+1),itl=split(l);
for(iter it=itl;it!=itr;it++){
/*
do something
*/
}
/*
do something
*/
}
區間加
void add(int l,int r,ll k){
iter itr=split(r+1),itl=split(l);
for(iter it=itl;it!=itr;it++){
it->v+=k;//可以直接改變結構體的成員是因為使用了 mutable
}
return;
}
區間第 \(k\) 小
#define pli pair<ll,int>
#define mp make_pair
typedef long long ll;
pli bk[N];
int tot;
ll kth(int l,int r,int rk){
tot=0;
iter itr=split(r+1),itl=split(l);
for(iter it=itl;it!=itr;it++){
bk[++tot]=mp(it->v,it->r-it->l+1);
}
sort(bk+1,bk+1+tot);
for(int i=1;i<=tot;i++){
if(rk<=bk[i].second)return bk[i].first;
rk-=bk[i].second;
}
return -1;
}
區間冪的和
ll ksm(ll a,ll b,ll p){
ll ans=1;
while(b){
if(b&1)ans=(ans*a)%p;
a=(a*a)%p;
b>>=1;
}
return ans;
}
ll pow_sum(int l,int r,ll x,ll y){
iter itr=split(r+1),itl=split(l);
ll res=0;
for(iter it=itl;it!=itr;it++){
res+=(it->r-it->l+1)*ksm(it->v%y,x,y)%y;
res%=y;
}
return res;
}
然後這題就做完了,提交記錄。
時間複雜度
不會證,但是是神奇的 \(O(n\log n)\)。
例題
鴿,以後補幾道。
The End
「最喜歡珂朵莉了」