珂朵莉树学习笔记
前言骚话
首先首先首先声明声明声明:珂朵莉珂朵莉珂朵莉是是是世界上世界上世界上最最最幸福幸福幸福的女孩的女孩的女孩
真爱鬼畜到此。(看着上面的图片感觉好心疼QwQ)
放出组织FFF滚(ノ`Д)ノ什么FFF团,我们从不烧真爱,中国珂学院地址:https://www.chtholly.ac.cn/
正片开始
珂朵莉树的介绍
珂朵莉树(ODT),老司机树,是一种基于\(c++\)中\(set\)的一种优美暴力算法,其本质和分块差不多。
珂朵莉树的应用
- 区间加法
- 区间复制
- 区间第k小
- 区间幂次和
(就问问有哪个数据结构能够像珂朵莉树这么优美的解决这一类的问题,珂朵莉最可爱了)
我们来看一波珂朵莉树的起源:万恶之源
【CF896C】Willem, Chtholly and Seniorious
【传送门】
特别说一下896这次考试的题目都是以终末为背景的
问题中提到了我们在上面讲到的几个珂朵莉树的应用,以这道题目为例。
珂朵莉树的结构体
珂朵莉树基于set
,为了实现带修操作,我们定义以下的结构体。
struct node {
int l, r;
mutable ll v;
node (int L, int R, ll V): l(L), r(R), v(V){}//构造函数,相当于附了初值
node (int L): l(L){}//一样的功能
bool operator <(const node &rhs) const {//重定义运算符
return l < rhs.l;
}
};
这个结构体表示区间\([l,r]\)之间的数都是\(v\),对于这个我们不怎么熟悉的mutable
,意思是“可变的”,那么可以再set中实现修改v
的操作了,还有结构体中的\(l,r\)值表示当前区间左右端点,对于这个重定义运算符,也是比较好理解,因为在set
中的结构体都需要重定义运算符来保证本身的顺序或者是查询的可能性,至于为什么要按照\(l\)来排序,其实和莫队的想法挺像的,我们将这个区间进行分块,然后按照块的编号来排序。
珂朵莉树的基本操作
分裂(split)操作
我们假设一下,如果我们像分块一样的算法:整块处理,不整块暴力的算法的话,那么我们之前做的事情不是都没有意义了吗?而且也完成不了操作\(4\),那么我们就需要一个split操作,就是将需要的部分和不需要修改的部分拆成两部分
代码
#define it set<node>::iterator//这个东西是为了减少代码量的宏定义
it split(int pos) {
it i = s.lower_bound(node(pos));//我们先找到第一个大于pos最的集合
if (i != s.end() && i -> l == pos) return i;//表示没有修改的区间
--i;//那么我们需要操作的区间一定在前一个内
int l = i -> l, r = i -> r;//找出前一个区间的左右端点
ll val = i -> v;//去除区间原来的集合
s.erase(i); //删除原来的区间集合
s.insert(node(l, pos - 1, val));//加入当前的区间集合
return s.insert(node(pos, r, val)).first;//放入后一半的新地址,并且返回地址指针
}
看完代码之后是不是还是有一点懵逼,其实就是将这个set
分成两个,把当前的节点分成两个区间\([l,pos-1]\)和\([pos,r]\)两个部分,这样保证了同一个区间是相同的元素。发现感觉和线段树好像啊!
推平(assign)操作
其实我更喜欢叫整合操作,这个操作就是将两个区间节点整合在一起,代码说话:
it assign(int l, int r, ll val) {
it i2 = split(r + 1), i1 = split(l);
s.erase(i1, i2);
s.insert(node(l, r, val));
}
这里先解释一下为什么要先分裂后面区间,在分裂前面区间。因为如果先分裂前区间,在分裂后区间,那么会使后区间在前区间的迭代器失效,成功RE。那么我们在回到这个问题,我们将这两个节点的地址取出来之后,我们可以直接删除掉,然后重新推平成一个新的节点。因为是直接赋值,如果范围比较大,就相当于是将\(n\)个节点变成了一个节点,疯狂降低了珂朵莉树的复杂度。由于数据随机生成,那么差不多是有\(\frac{1}{4}\)的数据是assign操作,那么就很优秀了,这个就是珂朵莉树在随机数据中能够处理像这样变态问题的原因。
但是这个仅限在随机数据,或者是推平操作较多的情况下可以使用,不然set
的个数会太多,直接爆炸,所以在想不到正解的时候,最好不要使用珂朵莉树。
据大佬实测,差不多珂朵莉树的复杂度是\(O(mlogn)\)。
n | f(n) |
---|---|
10 | 7 |
100 | 24 |
1000 | 33 |
10000 | 47 |
100000 | 67 |
1000000 | 95 |
数据来源:https://www.cnblogs.com/WAMonster/p/10181214.html
其他操作
以下暴力非常无脑,珂朵莉太可爱了。
add区间加法操作
void add(int l, int r, ll val) {
it i2 = split(r + 1), i1 = split(l);//找出区间
for (it i = i1; i != i2; i ++) i -> v += val;
}
正如你所想,每次将符合区间内的所有节点都拿出来,直接加。
查询第k大操作
ll kth(int l, int r, int k) {
it i2 = split(r + 1), i1 = split(l);
vector<pair<ll, int> > a;
a.clear();
for (it i = i1; i != i2; i ++)
a.push_back(pair<ll, int>(i->v, i->r - i->l + 1));//first是v,second是个数r-l+1
sort(a.begin(), a.end());//按照v排序,
for (int i = 0; i < (int)a.size(); i ++) {
k -= a[i].second;//这个就是减去个数
if (k <= 0) return a[i].first;//如果减去这个数之后就小于等于0了,那么就说明答案就在这个区间内
}
}
这一段对于对set
不熟悉的小伙伴们理解可能会有一点困难,首先我们定义的vector是一个有pair类型,也就是差不多是数组,如果实在sort中说,就是第一关键字和第二关键字。然后我们排序一下,注意这个时候我们是按照first来排序的,也就是v来排序的,第二关键字是second,我们可以判定不会有重复的v出现,4次中就有一次的assign推平,重复的v是几乎不会出现的,但是出现了也没有关系。排完序后,我们就查找,每次让k减去a[i].second,也就是减去个数,如果是小于等于0,那么就在当前区间,返回地址指针。
查询幂次和
ll power(ll n, int m, ll Mod) {//快速幂
ll res = 1ll;
n %= Mod;
for (; m; m >>= 1) {
if (m & 1) res = (res * n) % Mod;
n = (n * n) % Mod;
}
return res;
}
ll query(int l, int r, int x, ll y) {
it i2 = split(r + 1), i1 = split(l);
ll res = 0;
for (it i = i1; i != i2; i ++)
res = (res + (i->r - i->l + 1) * power(i->v, x, y)) % y;//当前区间次幂的和
return res;
}
有了上面的铺垫,这里应该就非常好理解了,也就是把相同的取出来,暴力取幂相加。QwQ。
关于珂朵莉树的时间复杂度
我不会证明,贴大佬的证明:
CF模板题的代码
#include <bits/stdc++.h>
#define ll long long
#define ms(a, b) memset(a, b, sizeof(a))
#define inf 0x3f3f3f3f
using namespace std;
template <typename T>
inline void read(T &x) {
x = 0; T fl = 1;
char ch = 0;
while (ch < '0' || ch > '9') {
if (ch == '-') fl = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
x *= fl;
}
struct node {
int l, r;
mutable ll v;
node (int L, int R, ll V): l(L), r(R), v(V){}
node (int L): l(L){}
bool operator <(const node &rhs) const {
return l < rhs.l;
}
};
set<node> s;
#define it set<node>::iterator
it split(int pos) {
it i = s.lower_bound(node(pos));
if (i != s.end() && i -> l == pos) return i;
--i;
int l = i -> l, r = i -> r;
ll val = i -> v;
s.erase(i);
s.insert(node(l, pos - 1, val));
return s.insert(node(pos, r, val)).first;
}
it assign(int l, int r, ll val) {
it i2 = split(r + 1), i1 = split(l);
s.erase(i1, i2);
s.insert(node(l, r, val));
}
void add(int l, int r, ll val) {
it i2 = split(r + 1), i1 = split(l);
for (it i = i1; i != i2; i ++) i -> v += val;
}
ll kth(int l, int r, int k) {
it i2 = split(r + 1), i1 = split(l);
vector<pair<ll, int> > a;
a.clear();
for (it i = i1; i != i2; i ++)
a.push_back(pair<ll, int>(i->v, i->r - i->l + 1));
sort(a.begin(), a.end());
for (int i = 0; i < (int)a.size(); i ++) {
k -= a[i].second;
if (k <= 0) return a[i].first;
}
}
ll power(ll n, int m, ll Mod) {
ll res = 1ll;
n %= Mod;
for (; m; m >>= 1) {
if (m & 1) res = (res * n) % Mod;
n = (n * n) % Mod;
}
return res;
}
ll query(int l, int r, int x, ll y) {
it i2 = split(r + 1), i1 = split(l);
ll res = 0;
for (it i = i1; i != i2; i ++)
res = (res + (i->r - i->l + 1) * power(i->v, 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() {
read(n); read(m); read(seed); read(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 opt = rnd() % 4 + 1;
l = rnd() % n + 1, r = rnd() % n + 1;
if (l > r) swap(l, r);
if (opt == 3) x = rnd() % (r - l + 1) + 1;
else x = rnd() % vmax + 1;
if (opt == 4) y = rnd() % vmax + 1;
if (opt == 1) add(l, r, (ll)x);
else if (opt == 2) assign(l, r, (ll)x);
else if (opt == 3) printf("%lld\n", kth(l, r, x));
else if (opt == 4) printf("%lld\n", query(l, r, x, (ll)y));
}
return 0;
}