珂朵莉樹
0x00 緒言
Update:2022/11/23 原來文章大部分都是拼湊內容(現在也是.....),質量過低,所以進行了一次小換血,主要是對代碼風格以及閱讀體驗進行了優化。
溫馨提示:如果你幻想找到非指針珂朵莉樹代碼,那你可以停下了,就看這一篇文章即可,雖然這篇文章代碼實現也是指針。但你不可能找到數組實現......
0x01 珂朵莉樹的起源
珂朵莉樹原名老司機樹 (Old Driver Tree,ODT),是一種基於 std::set
的暴力數據結構,由 2017 年一場 CF 比賽中提出的數據結構,因為題目背景主角是《末日時在做什么?有沒有空?可以來拯救嗎?》的主角珂朵莉,因此該數據結構被稱為珂朵莉樹。
0x02 應用
解決各種線段樹無法完成的操作。
注意珂朵莉樹保持復雜度主要依靠 assign
操作,所以題目中必須有區間賦值。
還有很重要的一點:數據需純隨機。
0x03 什么時候用珂朵莉樹
關鍵操作:推平一段區間,使一整段區間內的東西變得一樣。保證數據隨機。
n
個數,m
次操作。 \(n,m\leq10^5\)
操作:
-
區間加
-
區間賦值
-
區間第 \(k\) 小
-
求區間冪次和
-
數據隨機
0x04 構造
用一個帶結構體的集合 set
維護序列
集合中的每個元素有左端點,右端點,值
下面展示該結構體的構造:
struct node
{
int l, r;
mutable int val;
friend bool operator<(node a, node b)
{
return a.l < b.l;
}
node(int l, int r = 0, int val = 0) : l(l), r(r), val(val) {}
};
//mutale,意為可變的,即不論在哪里都是可修改的,用於突破C++帶const函數的限制。
0x05 Split 操作
操作過程
set::iterator split(int pos)
將原來含有 pos
的區間分為 [l,pos)
和 [pos,r]
兩段。
返回一個 std::set
的迭代器,指向 [pos,r]
段
可能有些抽象,詳細解如下:
split
函數的作用就是查找 set
中第一個左端點不小於 pos
的結點,如果找到的結點的左端點等於 pos
便直接返回指向該結點的迭代器,如果不是,說明 pos
包含在前一個結點所表示的區間之間,此時便直接刪除包含 pos
的結點,然后以 pos
為分界點,將此結點分裂成兩份,分別插入 set
中,並返回指向后一個分裂結點的迭代器。
首先我們假設 set
中有三個 node
結點,這三個結點所表示的區間長度為 14
,如下圖:
不妨以提取區間 [10,12]
為例詳細展開(說好的查詢 10
到 12呢,怎么下面扯了一堆
13` ?別急,后續將會揭曉 ):
如果我們要查詢序列第 13
個位置,首先執行 auto it = s.lower_bound(node(13))
; 此時 it
將成為一個指向第三個結點的迭代器,為什么是第三個結點,而不是第二個結點呢,因為 lower_bound
這個函數獲取的是第一個左端點 l
不小於 13
的結點,所以 it
是指向第三個結點的。
然后執行判斷語句,發現第三個結點的左端點不是 13
,不滿足條件,說明 13
必包含在前一個結點中,繼續向下執行,讓 it
指向前一個結點:
先將該結點的信息保存下來:int l = 10, r = 13, val = 2
然后直接刪除該結點
以 pos
為分界點,將被刪除的結點分裂為 [l,pos-1] ,[pos,r]
這兩塊,並返回指向 [pos,r]
這個區間的迭代器,事實上 return s.insert(node(pos,r,val)).first
; ,便做到了插入 [pos,r]
這端區間,並返回指向它的迭代器,有一個 insert
函數返回值為 pair
類型,其中 pair
的第一個元素就是元素插入位置的迭代器。
至此 13
位置已經分裂完成,然后是查詢第 10
個位置,查詢步驟同上,但是 10
號點滿足 if
語句,便直接返回了
由上述步驟,為了提取區間 [10,12]
,我們執行了兩次 split
,一次為 split(13)
,一次為 split(10)
,並獲得了兩個迭代器,一個指向第二結點,一個指向第三結點。
為什么要先分裂右端點,然后再分裂左端點呢?
因為如果先分裂左端點,返回的迭代器會位於所對應的區間以 l
為左端點,此時如果 r
也在這個節點內,就會導致分裂左端點返回的迭代器被 erase
掉,導致 RE
結合問題 1
和問題 2
,獲取區間迭代器 auto itr = split(r+1), itl = split(l)
;
代碼
auto split(int p)
{
auto it = s.lower_bound(node(p));
if (it != s.end() && it->l == p)
{
return it;
}
it--;
if (it->r < p)
{
return s.end();
}
int l = it->l;
int r = it->r;
int val = it->val;
s.erase(it);
s.insert(node(l, p - 1, val));
return s.insert(node(p, r, val)).first;
}
0x06 Assign 操作
操作過程
注意:以后在使用 split
分裂區間的時候,請先右后左,一般情況不會出事,但你沒有機會失誤。
區間賦值操作,也是珂樹維持其復雜度的關鍵函數
很暴力的思想,既然剛剛我們寫了一個 split
,那么就要把它用起來。
首先 split
出 l
並記返回值為 itl
,然后 split
出 r+1
並記返回值為 itr
,顯然我們要操作的區間為 [itl,itr)
,那么我們將 [itl,itr)
刪除 (std::set.erase(itl, itr))
,再插入一個節點 Node
,其 l
為 l
,r
為 r
,val
為賦值的 val
。
我們注意到因為這個操作, [itl,itr)
中的所有節點合並為了一個節點,大大降低了集合的元素數量,因此調整了我們的復雜度
代碼
void assign(int l, int r, int x)
{
auto itr = split(r + 1);
auto itl = split(l);
s.erase(itl, itr);
s.insert(node(l, r, x));
}
//將一個區間全部改為某個值。
0x07 其他操作
通用方法是 split
出 l
,split
出 r+1
,然后直接暴力掃描這段區間內的所有節點執行需要的操作
查詢區間和
long long querySum(int l, int r)
{
auto itr = split(r + 1);
auto itl = split(l);
long long res = 0;
for (auto i = itl; i != itr; i++)
{
res += (i->r - i->l + 1) * i->val;
}
return res;
}
區間加:
void add(int l, int r, int x)
{
auto itr = split(r + 1);
auto itl = split(l);
for (auto i = itl; i != itr; i++)
{
i->val += x;
}
}
區間第 k
小:
algorithm
庫中的 std::sort
(快速排序)
std::map
(方便起見使用其中的pair
),std::vector
(方便起見)
還是split
出l
,split
出r+1
,然后將每個節點的值和個數(即r-l+1
)組成一個pair
(注意為了排序,將值放在第一關鍵字),將pair
加入一個vector
中
將vector
排序
從vector
的begin
開始掃描,不停的使k
減去vector
當前項的第二關鍵字,若 \(k\leq0\),返回當前項的第一關鍵字。
int kth_number(int l, int r, int k)
{
auto itr = split(r + 1);
auto itl = split(l);
vector<rank> v;
for (auto i = itl; i != itr; i++)
{
v.push_back(rank(i->val, i->r - i->l + 1));
}
sort(v.begin(), v.end());
int i;
for (i = 0; i < v.size(); i++)
{
if (v[i].cnt < k)
{
k -= v[i].cnt;
}
else
{
break;
}
}
return v[i].num;
}
區間平方和
求區間所有數 x
次方的和模 y
的值
int qpow(int x, int b, int p)
{
int res = 1;
int a = x % p;
for (; b; b >>= 1)
{
if (b & 1)
{
res = res * a % p;
}
a = a * a % p;
}
return res;
}
int calc(int l, int r, int x, int y)
{
auto itr = split(r + 1);
auto itl = split(l);
int ans = 0;
for (auto i = itl; i != itr; i++)
{
ans = (ans + qpow(i->val, x, y) * (i->r - i->l + 1) % y) % y;
}
return ans;
}
0x08 模板題代碼實現CF896C
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <set>
#define rint register int
#define endl '\n'
#define int long long
using std::set;
using std::vector;
const int mod = 1e9 + 7;
const int N = 1e5 + 5;
int n, m, seed, vmax;
int a[N];
int rnd()
{
int ret = seed;
seed = (seed * 7 + 13) % mod;
return ret;
}
struct Chtholly_Tree
{
struct node
{
int l, r;
mutable int val;
friend bool operator<(node a, node b)
{
return a.l < b.l;
}
node(int l, int r = 0, int val = 0) : l(l), r(r), val(val) {}
};
set<node> s;
auto split(int p)
{
auto it = s.lower_bound(node(p));
if (it != s.end() && it->l == p)
{
return it;
}
it--;
if (it->r < p)
{
return s.end();
}
int l = it->l;
int r = it->r;
int val = it->val;
s.erase(it);
s.insert(node(l, p - 1, val));
return s.insert(node(p, r, val)).first;
}
void add(int l, int r, int x)
{
auto itr = split(r + 1);
auto itl = split(l);
for (auto i = itl; i != itr; i++)
{
i->val += x;
}
}
void assign(int l, int r, int x)
{
auto itr = split(r + 1);
auto itl = split(l);
s.erase(itl, itr);
s.insert(node(l, r, x));
}
struct rank
{
int num, cnt;
friend bool operator<(rank a, rank b)
{
return a.num < b.num;
}
rank(int num, int cnt) : num(num), cnt(cnt) {}
};
int kth_number(int l, int r, int k)
{
auto itr = split(r + 1);
auto itl = split(l);
vector<rank> v;
for (auto i = itl; i != itr; i++)
{
v.push_back(rank(i->val, i->r - i->l + 1));
}
sort(v.begin(), v.end());
int i;
for (i = 0; i < v.size(); i++)
{
if (v[i].cnt < k)
{
k -= v[i].cnt;
}
else
{
break;
}
}
return v[i].num;
}
int qpow(int x, int b, int p)
{
int res = 1;
int a = x % p;
for (; b; b >>= 1)
{
if (b & 1)
{
res = res * a % p;
}
a = a * a % p;
}
return res;
}
int calc(int l, int r, int x, int y)
{
auto itr = split(r + 1);
auto itl = split(l);
int ans = 0;
for (auto i = itl; i != itr; i++)
{
ans = (ans + qpow(i->val, x, y) * (i->r - i->l + 1) % y) % y;
}
return ans;
}
void build()
{
for (rint i = 1; i <= n; i++)
{
a[i] = (rnd() % vmax) + 1;
s.insert(node(i, i, a[i]));
}
}
} tree;
signed main()
{
scanf("%lld%lld%lld%lld", &n, &m, &seed, &vmax);
tree.build();
while (m--)
{
int op, l, r, x, y;
op = (rnd() % 4) + 1;
l = (rnd() % n) + 1;
r = (rnd() % n) + 1;
if (l > r)
{
std::swap(l, r);
}
if (op == 3)
{
x = (rnd() % (r - l + 1)) + 1;
}
else
{
x = (rnd() % vmax) + 1;
}
if (op == 4)
{
y = (rnd() % vmax) + 1;
}
if (op == 1)
{
tree.add(l, r, x);
}
if (op == 2)
{
tree.assign(l, r, x);
}
if (op == 3)
{
printf("%lld\n", tree.kth_number(l, r, x));
}
if (op == 4)
{
printf("%lld\n", tree.calc(l, r, x, y));
}
}
return 0;
}
0x09 例題
CF915E
每個操作就是區間賦值 0 或 1,順帶把總和修改一下
//把 cin 改成快讀就可以了,這個題卡常
#include <bits/stdc++.h>
#define rint register int
#define endl '\n'
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, m;
struct node
{
int l, r;
mutable int val;
friend bool operator<(node a, node b)
{
return a.l < b.l;
}
node(int l, int r = 0, int val = 0) : l(l), r(r), val(val) {}
};
struct Chtholly_Tree
{
set<node> s;
int sum = 0;
auto split(int p)
{
auto it = s.lower_bound(node(p));
if (it != s.end() && it->l == p)
{
return it;
}
it--;
if (it->r < p)
{
return s.end();
}
int l = it->l;
int r = it->r;
int val = it->val;
s.erase(it);
s.insert(node(l, p - 1, val));
return s.insert(node(p, r, val)).first;
}
void assign(int l, int r, int x)
{
auto itr = split(r + 1);
auto itl = split(l);
auto it = itl;
for( ;itl != itr; itl++) sum -= itl->val * (itl->r - itl->l + 1);
s.erase(it, itr);
s.insert(node(l, r, x));
sum += x * (r - l + 1);//這個跟模板不一樣,要順帶計算一下
}
} tree;
signed main()
{
cin >> n >> m;
tree.s.insert(node(1, n, 1));
tree.sum = n;
while(m--)
{
int l, r, op;
cin >> l >> r >> op;
if(op == 1)
{
tree.assign(l, r, 0);
}
else
{
tree.assign(l, r, 1);
}
cout << tree.sum << endl;
}
return 0;
}
0x10 后話
半年多了,終於重新更了,個人感覺質量漲了許多(最近沒時間,0x09 我會盡快更的555.....)