【模板】珂朵莉樹

caijianhong發表於2024-08-02

這是即將推送到 OI-wiki 的,對於原 OI-wiki 珂朵莉樹頁面的重構。作者是我。感謝上一位維護這玩意的人。

簡介

珂朵莉樹(Chtholly Tree),又名老司機樹 ODT(Old Driver Tree)。起源自 CF896C

這個名稱指代的是一種“使用平衡樹(std::setstd::map 等)或連結串列(std::list、手寫連結串列等)維護顏色段均攤”的技巧,而不是一種特定的資料結構。其核心思想是將值相同的一段區間合併成一個結點處理。相較於傳統的線段樹等資料結構,對於含有區間覆蓋的操作的問題,珂朵莉樹可以更加方便地維護每個被覆蓋區間的值。

實現(std::set)

結點型別

struct Node_t {
  int l, r;
  mutable int v;

  Node_t(const int &il, const int &ir, const int &iv) : l(il), r(ir), v(iv) {}

  bool operator<(const Node_t &o) const { return l < o.l; }
};

其中,int v 是你自己指定的附加資料。

???+ note "mutable 關鍵字的含義是什麼?"
mutable 的意思是「可變的」,讓我們可以在後面的操作中修改 v 的值。在 C++ 中,mutable 是為了突破 const 的限制而設定的。被 mutable 修飾的變數(mutable 只能用於修飾類中的非靜態資料成員),將永遠處於可變的狀態,即使在一個 const 函式中。

這意味著,我們可以直接修改已經插入 `set` 的元素的 `v` 值,而不用將該元素取出後重新加入 `set`。

結點儲存

我們希望維護所有結點,使得這些結點所代表的區間左端點單調增加且兩兩不交,最好可以保證所有區間的並是一個極大的連續範圍。此處以 std::set 為例,用一個 set<Node_t> odt; 維護所有結點。

初始化時。向珂朵莉樹中插入一個極長區間(如題目要求維護位置 \(1\)\(n\) 的資訊,插入區間 \([1,n+1]\))。

split 操作

split 操作是珂朵莉樹的核心。它接受一個位置 \(x\),將原本包含點 \(x\) 的區間(設為 \([l, r]\))分裂為兩個區間 \([l, x)\)\([x, r]\),並返回指向後者的迭代器。

參考程式碼如下:

auto split(int x) {
  auto it = odt.lower_bound(Node_t(x, 0, 0));
  if (it != odt.end() && it->l == x) return it;
  --it;
  int l = it->l, r = it->r, v = it->v;
  odt.erase(it);
  odt.insert(Node_t(l, x - 1, v));
  return odt.insert(Node_t(x, r, v)).first;
}

assign 操作

另外一個重要的操作:assign。用於對一段區間進行賦值。設將要對區間 \([l,r]\) 賦值為 \(v\)

首先,將區間 \([l, r]\) 擷取出來。依次呼叫 \(split(r+1), split(l)\),將此兩者返回的迭代器記作 \(itr, itl\),那麼 \([itl, itr)\) 這個迭代器範圍就指向了珂朵莉樹中 \([l,r]\) 包含的所有區間。

然後,將原有的資訊刪除。std::set 有成員方法 erase,簽名如同 iterator erase( const_iterator first, const_iterator last );,可以移除範圍 [first; last) 中的元素。於是我們呼叫 odt.erase(itl, itr); 以刪除原有的資訊。

最後,插入區間 \([l,r]\) 的新值。呼叫 odt.insert(Node_t(l, r, v)) 即可。

參考程式碼如下:

void assign(int l, int r, int v) {
  auto itr = split(r + 1), itl = split(l);
  odt.erase(itl, itr);
  odt.insert(Node_t(l, r, v));
}

??? 為什麼需要先 split(r + 1)split(l)

1. `std::set::erase` 方法將使指向被擦除元素的引用和迭代器失效。而其他引用和迭代器不受影響。
2. `std::set::insert` 方法不會使任何迭代器或引用失效。
3. `split` 操作會將區間拆開。呼叫 `split(r + 1)` 之後 $r + 1$ 會成為兩個新區間中右邊區間的左端點,此時 `split` 左區間,必然不會訪問到 $r + 1$ 為左端點的那個區間,也就不會將其拆開,刪去 $r + 1$ 為左端點的區間,使迭代器失效。反之,先 `split(l)`,再 `split(r + 1)`,可能會把 $l$ 為左端點的區間刪去,使迭代器失效。

perform 操作

將珂朵莉樹上的一段區間提取出來並進行操作。與 assign 操作類似,只不過是將刪除區間改為遍歷區間。

參考程式碼如下:

void perform(int l, int r) {
  auto itr = split(r + 1), itl = split(l);
  for (; itl != itr; ++itl) {
    // Perform Operations here
  }
}

注意不應該濫用這樣的提取操作,可能使得時間複雜度錯誤。見下文“複雜度分析”一欄。

實現(std::map)

相較於 std::set 的實現,std::map 的實現的 split 操作寫法更簡單。除此之外,其餘操作與 std::set 並無二異。

結點儲存

由於珂朵莉樹儲存的區間是連續的,我們不一定要記下右端點是什麼。不妨使用一個 map<int, int> mp; 儲存所有區間,其鍵維護左端點,其值維護其對應的左端點到下一個左端點之前的值。

初始化時,如題目要求維護位置 \(1\)\(n\) 的資訊,則呼叫 mp[1] = -1, mp[n + 1] = -1 表示將 \([1,n+1)\)\([1, n]\) 都設為特殊值 \(-1\)\([n+1, +\infty)\) 這個區間當作哨兵使用,也可以對它進行初始化。

split 操作

參考程式碼:(第一份)

void split(int x) {
  auto it = prev(mp.upper_bound(x)); // 找到左端點小於等於 x 的區間。
  mp[x] = it->second; // 設立新的區間,並將上一個區間儲存的值複製給本區間。
}

參考程式碼:(第二份)

auto split(int pos) {
  auto it = prev(mp.upper_bound(pos)); // 找到左端點小於等於 x 的區間。
  return mp.insert(it, make_pair(pos, it->second)); // 設立新的區間,並將上一個區間儲存的值複製給本區間。
}

這裡使用了 std::map::insert 的過載 iterator insert( const_iterator pos, const value_type& value );,其插入 value 到儘可能接近正好在 pos 之前的位置。如果插入恰好發生在正好在 pos 之前的位置,那麼複雜度是均攤常數,否則複雜度與容器大小成對數。

其餘操作與 std::set 並無二異。

實現(連結串列)

可參考 題解 CF896C 【Willem, Chtholly and Seniorious】 - 洛谷專欄 (luogu.com.cn)

複雜度分析

perform 以後立即對同一區間呼叫 assign

此時觀察發現,兩次 split 操作至多增加兩個區間;一次 assign 將刪除範圍內的所有區間並增加一個區間,同時遍歷所刪除的區間。所以,我們所遍歷的區間與所刪除的區間數量成線性,而每次操作都只會增加 \(O(1)\) 個區間,所以我們操作的區間數量關於操作次數(包括初始化)成線性,時間複雜度為均攤 \(O(m\log n)\),其中記 \(m\) 為操作次數,\(n\) 為珂朵莉樹中最大區間個數(可以認為 \(n\leq m\))。

perform 以後不進行 assign

如果允許特殊構造資料,這樣一定是能被卡掉的,只需要使珂朵莉樹中有足夠多的不同區間並反覆遍歷,就能使珂朵莉樹的複雜度達到甚至高於平方級別。

如果要保證複雜度正確,必須保證資料隨機。詳見 Codeforces 上關於珂朵莉樹的複雜度的證明。更詳細的嚴格證明見 珂朵莉樹的複雜度分析。證明的結論是:用 std::set 實現的珂朵莉樹的複雜度為 \(O(n \log \log n)\),而用連結串列實現的複雜度為 \(O(n \log n)\)

習題

  • 「SCOI2010」序列操作(該題目來源已新增 Hack 資料)
  • 「SHOI2015」腦洞治療儀
  • 「Luogu 4979」礦洞:坍塌
  • 「Luogu 8146」risrqnis

擴充套件閱讀

ODT的對映思想的推廣 - 洛谷專欄 (luogu.com.cn)

相關文章