珂朵莉树
相信珂学!
珂朵莉树(Chtholly Tree),又名老司机树(Old Driver Tree, ODT),是一种非常暴力的维护序列信息的
数据结构技巧。
前置知识
数据结构
可供珂朵莉树使用的数据结构有很多,主要分为两种:
- 基于「平衡树」:如
std::set
、std::map
等,不会还有人不知道这些是基于平衡树的吧? - 基于「链表」:如
std::list
、手写链表等。
但是这篇文章我们只介绍基于 map
的平衡树,这里就不介绍map的使用方法了,在哪里都能很轻松的找到。
指针及迭代器
没错,珂朵莉树由于使用了 STL,这导致它会出现许多迭代器和很多 STL 内嵌函数,我们需要来详细的讲解一下这部分内容。
其实,博主也不是很精通这方面内容,并且我也尝试能否不用这方面知识写出珂朵莉树,最后均已失败告终(绝大多数失败原因是超时)。
我将我尝试失败的一些代码粘贴出来,可以尝试能否在博主的思路进行优化:
点击查看代码
typedef pair<pair<ll,ll> , ll> ODT;
#define makep(l,r,val) (ODT)(make_pair(make_pair(l,r) , val))
//平替结构体,重载比较运算符,构造函数,但好像不支持set。。。无疾而终
inline void split(ll x){ //通过解引用防止使用指针,时间复杂度大致不变,属于成功尝试
ll k = (*prev(ODT.upper_bound(x))).first;
ODT[x] = ODT[k];
}
inline void assign(ll l ,ll r, ll val){
r ++; //注意,和其他模板不同,这个右端点不用输入r+1
split(r);split(l);
pair<ll,ll> k = *ODT.find(l);
while(k.first != r){
k = *ODT.erase(ODT.find(k.first));//在这里多次调用find(),TLE
}
ODT[l] = val;
}
//其他的函数大部分都是这个问题,多次调用find(),目前没有想到良好的解决方法。
定义
好了,我们正式开始相关讲解,先来了解定义:
指针是一种变量,其值为另一种类型的对象在计算机内存中的地址。你可以使用指针来直接访问和操作它指向的对象。指针的使用非常强大,但也很危险,因为你有可能错误地操作内存,这可能会导致程序崩溃或其他不可预期的行为。
迭代器是一种对象,它能够遍历并操作某种数据结构(如数组、列表、集合等)中的元素。每种数据结构都可能有自己专用的迭代器。迭代器的好处是它为处理各种数据结构提供了统一的接口,并提供了一种保护机制,使你不必直接处理内存。
它可以被视为一个高级的指针,因为它提供了类似指针的功能,例如指向容器中的特定位置、访问该位置的元素等操作。
指针与迭代器本质上的区别,那就是指针是一个变量,而迭代器是一个对象。
ll a[] = {1, 2, 3, 4, 5}; // 定义一个数组
ll* p = a; //声明一个指针变量指向数组
vector<ll> b(a , a+5);// 定义一个vector
auto it = b.begin() //迭代器,指向b的开头元素
常用操作
-
解引用
*
//可以将解引用和指针理解为一种逆运算 ll x = 10; ll* p = &x; // ptr 是一个指向整数 x 的指针 std::cout << *ptr; // 解引用之后再次变为值
-
箭头符号
->
//箭头操作符(->)在 C++ 中用于访问指针指向的对象的成员 pair<ll, ll> x = make_pair(1, 2); pair<ll, ll>* p = &x; // 使用箭头操作符访问成员 cout << p->first << endl; // 输出 1 cout << p->second << endl; // 输出 2
-
前驱及后继
ll a[] = {1, 2, 3, 4, 5}; // 定义一个数组 ll* p = a; //声明一个指针变量指向数组 p++; p--; //将p指向前驱/后继元素 p = prev(p);// 返回p指向的后继元素 p = next(p);// 返回p指向的前驱元素
好了,在珂朵莉树中我们一般只需要用到这些相关的指针知识,其他不在我们的讨论范围内,可以自行搜索。
实现逻辑
我们在引言中提到,珂朵莉树不是一种数据结构,而是一位动漫少女 ,而是一种以暴力为基本思想的技巧。
何为暴力?为什么它既然暴力但是还能解决一些问题呢?这是因为它的实现核心是将值相同的一段区间合并成一个结点处理。
对,珂朵莉树是为了去解决区间问题的,那为什么不用线段树?树状数组?这些常用的数据结构呢?
这些常用数据结构确实可以用于解决很多种形式的区间问题,但我们如果给出这么一种题:
区间简单求和,区间最小值很多数据结构都能轻松处理,那要是同一道题同时含有区间修改,区间简单求和,区间加权求和,区间平方求和,区间第 \(k\) 小问题,那你的线段树还能轻松应对吗?
这是珂朵莉树就派上用场了,其基本底层逻辑决定了它可以通过暴力遍历每一个数进行单独处理,这样是不是什么类型的区间问题都迎刃而解了呢?
其实也并不是,珂朵莉树由于是要将值相同的区间合并,所以如果一道题含有一个操作叫做区间推平,就是说将区间内所有数据赋值为统一值,那我们就可以通过合并这一区间,然后后续处理时只需要关注这一区间的值和它的长度就可以进行因题而异的客制化处理了对吧?同时这也要求这道题的操作是随机的,这样合并的区间越多,处理的速度就越快,最后均摊复杂度可以达到 \(O(m \log n)\)。
所以如果满足上述条件(其实若不满足,只会导致珂朵莉树时间复杂度改变,正确性是不会变的,可以用来暴力骗分,相信珂学!),就是珂朵莉树发挥作用的时候了。
代码实现
对于珂朵莉树,我们一般有四种函数 reset
、split
、assign
和 perform
,分别对应着清空重置,区间断开,区间合并(推平)以及最重要的区间操作。
创建珂朵莉树
诶?我还以为你要讲这四种函数
这一步是非常简单的,我们要学习基于map
的珂朵莉树,第一步就是要创建一个 map
。
这个映射用于存储所有区间,其键维护左端点,其值维护其对应的左端点到下一个左端点之前的值,这样就用类似桶排序的思想保证了每段区间的顺序问题。
map<ll,ll> ODT;// 基于map实现珂朵莉树
清空重置
初始化时,如题目要求维护位置 \(1\) 到 \(n\) 的信息,则调用 mp[1] = -1, mp[n + 1] = -1
表示将 \([1,n +1)\) 即 \([1, n]\) 都设为特殊值 \(-1\)。
inline void reset(){ //清零
ODT[1] = -1;
ODT[n + 1] = -1;
}
区间断开
这一步的话,我经尝试发现可以尽量避免使用指针,根据个人习惯而言,差别不大。
我们需要将 \(x\) 所在区间断开,我们只需要在 \(x\) 处新建一个节点,并使得它继承它之前所在区间的值就可以了。
//区间断开
inline void split(ll x){ // 从x将位置将区间断开
ll k = (*prev(ODT.upper_bound(x))).first;//找到左端点小于等于x的区间,返回其key
ODT[x] = ODT[k];//设立新的区间,并将上一个区间储存的值复制给本区间
}
区间合并
我们对于一个要合并的 \([l,r]\) 区间,并将其区间内的值赋值为 \(val\) ,很好想到我们先将 \(l\) 和 \(r\) 本身所在的两个区间断开,然后将 \([l,r]\) 区间内部所有的区间都合并,也就是将区间内所有节点删除,最后将这一区间的值赋值为 \(val\)。
这里有一点必须注意,先断开 \(r\) 所在区间,再断开 \(l\) ,具体原因不再赘述,但这非常重要!
inline void assign(ll l ,ll r, ll val){
r ++; //注意,和其他模板不同,这个右端点不用输入r+1
split(r);split(l); //先割断
auto k = ODT.find(l); // 找到l
while(k -> first != r){ //合并l - r 的所有区间;
k = ODT.erase(k);
}
ODT[l] = val;
}
区间操作
对于这一操作是因题目而异的,也最能体现出珂朵莉树的暴力思想,就是说对于要处理的 \([l,r]\) 区间,将其内部每个节点都拿出来单独处理一边就好了,非常好理解,珂学就是暴力。
但是暴力也并非纯暴力,而是对于每个节点只去处理他的值和他的长度就行,因为每个区间内的值不都是一样的嘛。
inline void perform1(ll l , ll r ,ll c){ // add
r ++;
split(r);split(l);
auto k = ODT.find(l);
while(k ->first != r){
//这里是题目要求的处理方法。
ODT[k -> first] += c; //e.g. 区间加c操作
k = next(k);
}
}
好了到这里,相信你已经对珂朵莉树有了一定了解了,我们来看板题,这也是珂朵莉树的起源。
CF896C Willem, Chtholly and Seniorious
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)
//typedef pair<pair<ll,ll> , ll> ODT;
//#define makep(l,r,val) (ODT)(make_pair(make_pair(l,r) , val))
//平替结构体,重载比较运算符,构造函数,但好像不支持set。。。无疾而终
constexpr int MOD = 1000000007;
ll n , m, seed , vmax;
map<ll,ll> ODT;// 基于map实现珂朵莉树
//存储所有区间,其键维护左端点,其值维护其对应的左端点到下一个左端点之前的值
inline void reset(){ //清零
ODT[1] = -1;
ODT[n + 1] = -1;
}
//区间断开
inline void split(ll x){ // 从x将位置将区间断开
ll k = (*prev(ODT.upper_bound(x))).first;//找到左端点小于等于x的区间,返回其key
ODT[x] = ODT[k];//设立新的区间,并将上一个区间储存的值复制给本区间
}
//区间推平
inline void assign(ll l ,ll r, ll val){
r ++; //注意,和其他模板不同,这个右端点不用输入r+1
split(r);split(l); //先割断
auto k = ODT.find(l); // 找到l
while(k -> first != r){ //合并l - r 的所有区间;
k = ODT.erase(k);
}
ODT[l] = val;
}
//区间操作
inline void perform(ll l , ll r ,ll c){
r ++;
split(r);split(l);
auto k = ODT.find(l);
while(k ->first != r){
ODT[k -> first] += c; //e.g. 区间加c操作
k = next(k);
}
}
inline ll range(ll l , ll r){
r--;
return r - l + 1;
}
inline ll perform2(ll l , ll r ,ll c){ //sort
r ++;
split(r);split(l);
vector<pair<ll, ll>> seg; // 存储(值, 区间长度)
auto k = ODT.find(l);
while (k->first < r) {
seg.push_back(make_pair(k->second, range(k -> first,next(k) -> first)));
k++;
}
sort(seg.begin(), seg.end()); // 按值排序
// 累加长度找第x小
ll cnt = 0;
for (auto &p : seg) {
cnt += p.second;
if (cnt >= c) return p.first;
}
}
inline ll perform3(ll l , ll r ,ll x , ll y){ //sum
r ++;
split(r);split(l);
ll sum = 0;
auto k = ODT.find(l);
auto kspow = [](ll a, ll b ,ll p){
ll res = 1;
a %= p;
while (b) {
if (b & 1) res = res * a % p;
a = a * a % p;
b >>= 1;
}
return res;
};
while(k->first != r){
ll cnt = range(k->first,next(k) -> first);
sum = (sum + kspow(ODT[k -> first] , x , y) * cnt % y) % y;
k = next(k);
}
return sum;
}
//题目造数据要求
ll a[100086];
ll rnd() {
ll ret = seed;
seed = (seed * 7 + 13) % MOD;
return ret;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin >> n >> m >> seed >> vmax;
reset();
for (int i = 1; i <= n; i++) {
a[i] = (rnd() % vmax) + 1;
split(i);
assign(i,i,a[i]);
}
for (int i = 1; i <= m; ++i) {
// input
ll op, l, r, x, y;
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;
}
//deal
if (op == 1) {
perform1(l, r, x);
} else if (op == 2) {
assign(l, r, x);
} else if (op == 3) {
cout << perform2(l, r, x) << endl;
} else {
cout << perform3(l, r, x, y) << endl;
}
}
return ~~ (0 ^ 0);
}