珂朵莉樹/顏色段均攤

Ke_scholar發表於2024-05-08

名稱簡介

珂朵莉樹(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。

相關文章