名稱簡介
珂朵莉樹(Chtholly Tree),又名老司機樹 ODT(Old Driver Tree)。起源自 CF896C。
注意,這種想法的本質是基於資料隨機的「顏色段均攤」,而不是一種資料結構,下文介紹的操作是這種想法的具體實現方法。
前置知識
會用 STL 的 set 就行。
核心思想
把值相同的區間合併成一個結點儲存在 set 裡面。
用處
騙分。只要是有區間賦值操作的資料結構題都可以用來騙分。在資料隨機的情況下一般效率較高,但在不保證資料隨機的場合下,會被精心構造的特殊資料卡到超時。
如果要保證複雜度正確,必須保證資料隨機。詳見 Codeforces 上關於珂朵莉樹的複雜度的證明。
更詳細的嚴格證明見 珂朵莉樹的複雜度分析。對於 add,assign 和 sum 操作,用 set 實現的珂朵莉樹的複雜度為\(O(n \log \log n)\),而用連結串列實現的複雜度為 \(O(n \log n)\)。
正文
首先,結點的儲存方式:
struct Node {
int l, r;
mutable int v;
Node(const int &il, const int &ir, const int &iv) : l(il), r(ir), v(iv) {}
bool operator<(const Node &o) const { return l < o.l; }
};
其中,int v
是你自己指定的附加資料。
mutable的關鍵字的含義是什麼?
mutable
的意思是「可變的」,讓我們可以在後面的操作中修改 v
的值。在 C++ 中,mutable 是為了突破 const 的限制而設定的。被 mutable 修飾的變數(mutable 只能用於修飾類中的非靜態資料成員),將永遠處於可變的狀態,即使在一個 const 函式中。
這意味著,我們可以直接修改已經插入 set
的元素的 v
值,而不用將該元素取出後重新加入 set
。
然後,我們定義一個 set<Node_t> odt;
來維護這些結點。為簡化程式碼,可以 typedef set<Node_t>::iterator iter
,當然在題目支援 C++11 時也可以使用 auto
。
split
split
是最核心的操作之一,它用於將原本包含點 x 的區間(設為 [l, r])分裂為兩個區間 [l, x) 和[x,r] 並返回指向後者的迭代器。 參考程式碼如下:
auto split(int x) {
if (x > n) return odt.end();
auto it = --odt.upper_bound(Node{x, 0, 0});
if (it->l == x) return it;
int l = it->l, r = it->r, v = it->v;
odt.erase(it);
odt.insert(Node(l, x - 1, v));
return odt.insert(Node(x, r, v)).first;
}
這段程式碼有什麼用呢? 任何對於 [l,r]的區間操作,都可以轉換成 set 上 [\(split(l),split(r + 1)\) ) 的操作。
assign
另外一個重要的操作 assign
用於對一段區間進行賦值。 對於 ODT 來說,區間操作只有這個比較特殊,也是保證複雜度的關鍵。 如果 ODT 裡全是長度為 1 的區間,就成了暴力,但是有了 assign
,可以使 ODT 的大小下降。 參考程式碼如下:
void assign(int l, int r, int v) {
auto itr = split(r + 1), itl = split(l);
odt.erase(itl, itr);
odt.insert(Node(l, r, v));
}
其他操作
套模板就好了,參考程式碼如下:
void performance(int l, int r) {
auto itr = split(r + 1), itl = split(l);
for (; itl != itr; ++itl) {
// Perform Operations here
}
}
注:珂朵莉樹在進行求取區間左右端點操作時,必須先 split 右端點,再 split 左端點。若先 split 左端點,返回的迭代器可能在 split 右端點的時候失效,可能會導致 RE。