珂朵莉树是由某毒瘤在2017年的一场CF比赛中提出的数据结构,原名老司机树(Old Driver Tree,ODT)。因为第一个以珂朵莉树为正解的题目的背景以《末日时在作什么?有没有空?能够来拯救吗?》中的角色——珂朵莉为主角,因此这个数据结构又被称为珂朵莉树(Chtholly Tree)她主要处理区间修改、查询问题,在数据随机的前提下有着优秀的复杂度。node
说到区间维护,我就想起明年年初 咱们都会想到线段树、树状数组、splay、分块、莫队等数据结构,这些数据结构貌似是无所不能的(毕竟一个不行用两个),不管区间加减乘除、平方开方都能搞定。但若是咱们有这么一题:(CF中的起源题)数组
题意:写个数据结构维护长度为\(n\)的序列(\(n≤10^5\)),实现区间赋值、区间加上一个值、求区间第k小、求区间每一个数x次方之和模y的值的操做,数据随机生成。数据结构
咱们发现本题的第4个操做涉及每一个数字的相关操做,那么线段树、树状数组、分块甚至splay是确定行不通的,莫队虽然能够维护4操做,但复杂度一样难以承受(要对每一个入队出队的数取快速幂)。咱们须要一个更为高效的数据结构来维护这些操做,她就是珂朵莉树。less
珂朵莉树基于std::set
,咱们定义节点为一段连续的值相同的区间,代码以下:函数
struct node { int l, r; mutable ll val; int operator < (const node &a) const{ return l < a.l; } node(int L, int R, ll Val) : l(L), r(R), val(Val) {} node(int L) : l(L) {} };
其中函数node
是构造函数,能够理解为定义变量时的初始化函数,具体用法请看下文代码。学习
mutable
修饰符意为“可变的”,这样咱们就能够在本不支持直接修改的set中修改它的值辣优化
\(l\)、\(r\)分别表明当前区间的左、右端点,\(val\)表示当前区间每一个数字的值。spa
可是咱们查询的时候不能保证查询的区间端点必定与这些节点的端点重合,若是采用分块思想(边角暴力)确定行不通(会退化成暴力),因此咱们要按需把节点分裂一下:code
#define sit set<node>::iterator sit Split(int pos) { sit it = s.lower_bound(node(pos)); if (it != s.end() && it->l == pos) return it; --it; int l = it->l, r = it->r; int val = it->val; s.erase(it); s.insert(node(l, pos - 1, val)); return s.insert(node(pos, r, val)).first; }
这段代码所作的事就是把\(pos\)所在的节点(左右端点分别为\(l\)、\(r\))分红 \([l, pos)\) 和 \([pos, r]\) 两块,而后返回后者。返回值所用的是set::insert
的返回值,这是一个pair
对象(熟悉map
的同窗应该能熟练运用),它的first
是一个迭代器,即插入的东西在set
中的迭代器。对象
这段代码很简单,应该不难打(bei)出(song)
接下来的add
、kth
、sum
等操做都依赖于Split
操做,具体作法就是把区间左右端点所在的节点分裂,使得修改&查询区间能彻底对应起来,以后就是暴力地去搞啦qwq
参考代码:
void Add(int l, int r, ll val) {//暴力枚举 sit it2 = Split(r + 1), it1 = Split(l); for (sit it = it1; it != it2; ++it) it->val += val; } ll Kth(int l, int r, int k) {//暴力排序 sit it2 = Split(r + 1), it1 = Split(l); vector< pair<ll, int> > aa; aa.clear(); for (sit it = it1; it != it2; ++it) aa.push_back(pair<ll, int>(it->val, it->r - it->l + 1)); sort(aa.begin(), aa.end()); for (int i = 0; i < aa.size(); ++i) { k -= aa[i].second; if (k <= 0) return aa[i].first; } } ll qpow(ll a, int x, ll y) { ll b = 1ll; a %= y;//不加这句话WA while (x) { if (x & 1) b = (b * a) % y; a = (a * a) % y; x >>= 1; } return b; } ll Query(int l, int r, int x, ll y) {//暴力枚举+快速幂 sit it2 = Split(r + 1), it1 = Split(l); ll res = 0; for (sit it = it1; it != it2; ++it) res = (res + (it->r - it->l + 1) * qpow(it->val, x, y)) % y; return res; }
FAQ:
\(1.Q:\)为何要Split(r + 1)
?
\(A:\)便于取出 \([l, r + 1)\) 的部分,即 \([l,r]\)
\(2.Q:\)为何要先Split(r + 1)
,再Split(l)
?
\(A:\)由于若是先Split(l)
,返回的迭代器会位于所对应的区间以\(l\)为左端点,此时若是\(r\)也在这个节点内,就会致使Split(l)
返回的迭代器被erase
掉,致使RE。
\(3.Q:\)这些操做里面全是Split
,复杂度理论上会退化成暴力(不断分裂直到没法再分),怎么让它不退化?
Assign操做也很简单,Split
以后暴力删点,而后加上一个表明当前区间的点便可。代码以下:
void Assign(int l, int r, int val) { sit it2 = Split(r + 1), it1 = Split(l); s.erase(it1, it2); s.insert(node(l, r, val)); }
不难看出,这个操做把多个节点减成一个,但这样就能使复杂度优化了吗?
因为题目数据随机,因此Assign
操做不少,约占全部操做的\(\frac{1}{4}\)。其余全部操做都有两个Split
,咱们能够用一下程序模拟一下珂朵莉树在数据随机状况下节点的个数:
#include <cstdio> #include <cstring> #include <cmath> #include <algorithm> #include <set> #include <vector> #include <cstdlib> #include <ctime> using namespace std; struct node { int l, r; mutable int val; int operator < (const node &a) const { return l < a.l; } node(int L, int R = -1, int Val = 0) : l(L), r(R), val(Val) {} }; set<node> s; #define sit set<node>::iterator sit Split(int pos) { sit it = s.lower_bound(node(pos)); if (it != s.end() && it->l == pos) return it; --it; int l = it->l, r = it->r, val = it->val; s.erase(it); s.insert(node(l, pos - 1, val)); return s.insert(node(pos, r, val)).first; } void Assign(int l, int r, int val) { sit it2 = Split(r + 1), it1 = Split(l); s.erase(it1, it2); s.insert(node(l, r, val)); } int main() { int n; scanf("%d", &n); for (int i = 1; i <= n + 1; ++i) s.insert(node(i, i, 0)); srand((unsigned)time(0)); srand(rand()); for (int t = 1; t <= n; ++t) { int a = rand() * rand() % n + 1, b = rand() * rand() % n + 1; if (a > b) swap(a, b); if (rand() % 4 == 0) { Assign(a, b, 0); } else Split(a), Split(b + 1); } printf("%d", s.size()); return 0; }
本人机子上实验数据以下(\(n\)表示序列长度,\(f(n)\)表示节点个数)
\(n\) | \(f(n)\) |
---|---|
10 | 7 |
100 | 24 |
1000 | 33 |
10000 | 47 |
100000 | 67 |
1000000 | 95 |
可见,加了Assign
的珂朵莉树在随机数据下跑得飞快,节点数达到了\(\log\)级别。也就是说,珂朵莉树的高效是由随机分配的Assign
保证的。若是一个题目没有区间赋值操做或者有数据点没有赋值操做,或者数据很不随机,请不要把珂朵莉树当正解打。
珂朵莉树目前的应用还很狭窄,各位dalao仍是用她来骗分吧qwq
关于详尽的复杂度证实,我数学太差证不出,这里贴一下@Blaze dalao的证实:
#include <cstdio> #include <cstring> #include <cmath> #include <algorithm> #include <set> #include <vector> #include <utility> using namespace std; #define ll long long struct node { int l, r; mutable ll val; int operator < (const node &a) const{ return l < a.l; } node(int L, int R, ll Val) : l(L), r(R), val(Val) {} node(int L) : l(L) {} }; set<node> s; #define sit set<node>::iterator sit Split(int pos) { sit it = s.lower_bound(node(pos)); if (it != s.end() && it->l == pos) return it; --it; int l = it->l, r = it->r; ll val = it->val; s.erase(it); s.insert(node(l, pos - 1, val)); return s.insert(node(pos, r, val)).first; } sit Assign(int l, int r, ll val) { sit it2 = Split(r + 1), it1 = Split(l); s.erase(it1, it2); s.insert(node(l, r, val)); } void Add(int l, int r, ll val) { sit it2 = Split(r + 1), it1 = Split(l); for (sit it = it1; it != it2; ++it) it->val += val; } ll Kth(int l, int r, int k) { sit it2 = Split(r + 1), it1 = Split(l); vector< pair<ll, int> > aa; aa.clear(); for (sit it = it1; it != it2; ++it) aa.push_back(pair<ll, int>(it->val, it->r - it->l + 1)); sort(aa.begin(), aa.end()); for (int i = 0; i < aa.size(); ++i) { k -= aa[i].second; if (k <= 0) return aa[i].first; } } ll qpow(ll a, int x, ll y) { ll b = 1ll; a %= y; while (x) { if (x & 1) b = (b * a) % y; a = (a * a) % y; x >>= 1; } return b; } ll Query(int l, int r, int x, ll y) { sit it2 = Split(r + 1), it1 = Split(l); ll res = 0; for (sit it = it1; it != it2; ++it) res = (res + (it->r - it->l + 1) * qpow(it->val, x, y)) % y; return res; } int n, m, vmax; ll seed; int rnd() { int ret = (int)seed; seed = (seed * 7 + 13) % 1000000007; return ret; } int main() { scanf("%d%d%lld%d", &n, &m, &seed, &vmax); for (int i = 1; i <= n; ++i) { int a = rnd() % vmax + 1; s.insert(node(i, i, (ll)a)); } s.insert(node(n + 1, n + 1, 0)); for (int i = 1; i <= m; ++i) { int l, r, x, y; int op = rnd() % 4 + 1; l = rnd() % n + 1, r = rnd() % n + 1; if (l > r) 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) Add(l, r, (ll)x); else if (op == 2) Assign(l, r, (ll)x); else if (op == 3) printf("%lld\n", Kth(l, r, x)); else if (op == 4) printf("%lld\n", Query(l, r, x, (ll)y)); } return 0; }
珂朵莉树的经典题目大都非正解(且正解大可能是线段树),不过在有的题目中吊打正解
不过有些题目作起来仍是挺有意思的qwq
标算线段树(貌似?)
不过珂朵莉树随随便便跑进最优解,这才是重点。
同上,珂朵莉树甩线段树几条街
数据不纯随机,但珂朵莉树也能无压力跑过~~
毒瘤题目,治疗脑洞操做的时候注意题面的规则,否则RE65或75。其他没什么难度。
树剖+珂朵莉树+O2,其实和线段树同样的作法qwq