珂朵莉樹

zuoqingyuan111發表於2024-10-04

吾日三省吾身:末日時在幹什麼?有沒有空?可以來拯救一下嗎?

演算法思想

非常簡單:就是暴力。

對於資料結構題,我們有這樣一種思路去維護:對於一個數列,我們把不同的數字看成不同的顏色段,然後對每個顏色段進行暴力操作,可以有效降低時間複雜度。但這種暴力是很好卡掉的,只需讓顏色段儘可能多,演算法就可以直接退化的 \(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 的尾指標。

步驟其實十分簡單:

  1. 找到 \(pos\) 所在顏色段。

  2. 如果 \(pos\) 是該顏色段的端點,直接返回指標

  3. 分裂,返回分裂後的結果

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

圖

「最喜歡珂朵莉了」

相關文章