前言

珂朵莉树 (Chtholly Tree) 是一种简单优美的数据结构,就像 Chtholly 一样可爱。暴力即优美。 适用于一些有区间赋值操作的序列操作题。

Chtholly Tree 的本质是把一个序列分成几个连续区间,每个区间内的元素的值相同,然后用一个 std::set 维护所有区间。

然后就可以通过一些神奇的操作,做到在数据随机的情况下 \(O(logn)\) 查询区间信息。时间复杂度我不会证QAQ。

当然也可以手写 std::set ,但是那样子还不如直接用平衡树做题。

事实上,Chtholly Tree 的适用范围很小,只能在某些保证数据随机且有区间赋值操作的题目中使用,别的情况下就是“你比暴力多个 log ”。而且一般来说出题人没有不卡 Chtholly Tree 的。虽然有时可以吸氧水过去。

竞赛一般不会考 Chtholly Tree ,但是多学一种数据结构也不是坏事嘛。尤其是 Chtholly 这么可爱。

零. 前置知识

你需要关于 std::set 的基础知识:

  1. set s; 建立一个类型为 type 的set。

  2. s.insert(x); 向 \(s\) 中插入一个值为 \(x\) 的元素。该函数的返回值为 pair<iterator,bool>,之后会用到这一返回值。

  3. s.erase(itl, itr); 删除 s 中的一段区间\([itl,itr)\),其中 \(itl\), \(itr\) 的类型为 set::iterator,也就是两个迭代器。

  4. s.lower_bound(x); 在 s 中二分查找大于等于 \(x\) 的元素,返回指向第一个大于等于 \(x\) 的元素所在的位置的迭代器。

一. 建树

用一个结构体表示每一个小区间。在结构体中记录三个值 \(l\) , \(r\), \(val\) ,分别表示这段区间的左端点、右端点和区间中每个元素的数值。

struct node{
int l, r;
mutable ll val;
node(int L, int R=-1, ll V=0):l(L), r(R), val(V){}
bool operator<(const node &oth)const{return this->l<oth.l;}
};
set<node> s; inline void build()
{
for (int i=1; i<=n; ++i)
{
a[i] = read();
s.insert(node(i, i, a[i]));
}
s.insert(node(n+1, n+1, 0));
}

值得一提的地方:

  1. 为了保证区间有序,std::set 中的 node 是按照 \(l\) 来排序的。也可以理解为我们以 \(l\) 作为这个区间的代表。

  2. 由于 std::set 自身的原因,\(val\) 前必须有 mutable,以便支持区间修改等操作。

  3. 在插入所有元素后需要再插入一个虚拟区间,以保证之后在查找区间时不会出错。

这样我们就建好了一棵 Chtholly Tree。

但是这样就和原序列完全一致了,一共有 \(n\) 个小区间。所以我们需要一些操作来减少区间的数量。

二. 核心操作:split 和 assign

要想维持珂朵莉树的优秀时间复杂度,这两个操作必不可少。

1.split(index)

这个操作把 std::set 维护的区间从 \(index\) 分成两段,且不改变不包含下标 \(index\) 的区间。

步骤如下:

  1. 首先在已有的区间中查找 \(l=index\) 的区间,如果找到了就直接返回,否则进行下一步操作。

  2. 经过上一步操作,我们要找的 \(index\) 一定已经被包含在一个区间中,所以我们要把包含 \(index\) 的区间分成两个更小的区间。具体来说,我们找到包含 \(index\) 的区间,然后删除该区间 \([l,r]\) ,再在 std::set 中插入区间 \([l,index-1]\) 和区间 \([index,r]\) ,\(val\) 值当然都为原区间的 \(val\)。

  3. \(\operatorname{split}\) 操作会增加 std::set 维护的区间数量,但是这对时间复杂度基本不影响。

  4. \(\operatorname{split}(index)\) 操作的返回值是一个指向以 \(index\) 为 \(l\) 的区间的迭代器,理解为指针即可。利用了 std::set 的 insert 操作的返回值。

#define IT set<node>::iterator

IT split(int ind)
{
IT it = s.lower_bound(node(ind));
if(it != s.end()&&it->l == ind)return it;
--it;
int xl = it->l, xr = it->r;
ll v = it->val;
s.erase(it);
s.insert(node(xl, ind-1, v));
return s.insert(node(ind, xr, v)).first;
}

以上就是 Chtholly Tree 的核心操作。

之后如果要对一段区间 \([l,r]\) 进行操作,只需要分离出区间 \([l,r]\),然后用最朴素的方法乱搞即可。

2.assign(x, y, z)

\(\operatorname{assign}(x, y, z)\):把一段区间 \([x,y]\) 的值全部赋成一个数 \(z\) 。

能使用 Chtholly Tree 的题目都会有这个操作。足够的 \(\operatorname{assign}\) 操作是 Chtholly Tree 时间复杂度的保障。

事实上 \(\operatorname{assign}(x,y,z)\) 操作很好实现,我们只需要分离出左端点为 \(x\) 的区间,再分离出左端点为 \(y+1\) 的区间,用一个元素值都为 \(z\) 值区间 \([x,y]\) 替换掉这两个区间中的所有区间即可。

void assign(int l ,int r, ll v)
{
IT itr = split(r+1), itl = split(l);
s.erase(itl, itr);
s.insert(node(l, r, v));
}

值得注意的地方:

  1. \(\operatorname{split}\) 的顺序最好按照代码中的顺序,否则可能会有玄学错误。

事实上,Chtholly Tree 的基础操作只有以上的 \(\operatorname{split}\) 和 \(\operatorname{assign}\)。只要掌握了这两个操作,任何能用 Chtholly Tree 求解的题目就都不难做了。

另外,以上的操作不会使 Chtholly Tree 维护的区间产生重复或遗漏的情况。

三. 一道最经典的题目:CF896C

CF896C Willem, Chtholly and Seniorious

题意:给定一个长度为 \(n\) 的序列 \(a\) ,一共有 \(m\) 个操作,包含以下四种:

  • \((1,l,r,x)\) : 给定一段区间 \([l, r]\) ,把这段区间内的每一个元素都加上 \(x\) 。

  • \((2,l,r,x)\) : 给定一段区间 \([l, r]\) ,把这段区间内的每一个元素都变成 \(x\) 。

  • \((3,l,r,x)\) : 给定一段区间 \([l, r]\) ,求这段区间内排名为 \(x\) 的元素。

  • \((1,l,r,x,y)\) : 给定一段区间 \([l, r]\) ,求这段区间内的所有元素的 \(x\) 次方和 \(\bmod\ y\) 的值,即求\(\sum_{i=l}^r({a_i}^x)\ \bmod\ y\)。

  • $n \leq 10^5 $, $m \leq 10^5 $

保证数据随机生成。

以上标黑的字体就是本题可以使用 Chtholly Tree 的关键:区间赋值操作和数据随机生成。

接下来我们考虑如何用 Chtholly Tree 实现本题的四个操作。

首先可以发现操作2就是 \(\operatorname{assign}\) 操作,直接套用即可。

void assign(int l ,int r, ll v)
{
IT itr = split(r+1), itl = split(l);
s.erase(itl, itr);
s.insert(node(l, r, v));
}

接下来考虑操作1。我们可以分离出区间 \([l,r]\) ,然后暴力把这段区间内的每一个结点的 \(val\) 都加上 \(x\) 。

这样就行了。

void add(int l, int r, ll v)
{
IT itr = split(r+1), itl = split(l);
for(;itl!=itr;++itl)
itl->val += v;
}

就是这么暴力,所以 Chtholly Tree 才优美。

关于时间复杂度的问题,之后会再做讨论。

然后考虑操作3。我们先把区间 \([l,r]\) 中的所有结点暴力取出来,放进一个 std::vector 里,按照 \(val\) 排序,然后暴力枚举 vector 中的元素,每次记录当前已经枚举过的元素的数量,直到找到第 \(x\) 大的元素即可返回。

注意 Chtholly Tree 的结点中存的是一段区间,记录当前枚举过元素的数量时要加上这段区间的长度。

ll krank(int l ,int r, ll k)
{
vp.clear();
IT itr = split(r+1), itl = split(l);
for(;itl!=itr;++itl)
vp.push_back(make_pair(itl->val, itl->r-itl->l+1));
sort(vp.begin(), vp.end());
for(vector<pair<ll, int> >::iterator it = vp.begin();it!=vp.end();++it)
{
k -= it->second;
if(k<=0) return it->first;
}
return -1ll;
}

最后是操作4。还是暴力枚举区间中的结点,然后快速幂计算每个元素 \(x\) 次方和即可。

还是要注意Chtholly Tree 的结点中存的是一段区间,所以每一个结点对答案的贡献要乘上区间长度,另外注意取模。

ll power(ll x, ll p, ll mod)
{
ll res = 1, base = x%mod;
while(p)
{
if(p&1)res = res*base%mod;
base = base*base%mod;
p>>=1;
}
return res%mod;
}
ll sum(int l, int r, ll p, ll mod)
{
IT itr = split(r+1), itl = split(l);
ll res = 0;
for(;itl!=itr;++itl)
res = 1ll*(1ll*res+1ll*(itl->r-itl->l+1)*power(itl->val, (ll)p, (ll)mod))%mod;
return res;
}

就这样,我们以近乎纯暴力的解法完成了这道题的所有操作。

时间复杂度可以感性理解一下:足够随机的 \(\operatorname{assign}\) 操作保证了 std::set 中的结点数量不会太多,所以每次区间操作都是跑不满 \(n\) 的。单次操作(不算快速幂)的期望时间复杂度应该是在 \(O(logn)\) 左右,足以通过本题。

完整代码如下:

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<set>
#include<vector>
#include<cstring>
#define IT set<node>::iterator
using namespace std;
typedef long long ll;
const int MAXN = 100100;
const int MOD7 = 1e9 + 7;
const int MOD9 = 1e9 + 9;
struct node{
int l, r;
mutable ll val;
node(int L, int R=-1, ll V=0):l(L), r(R), val(V){}
bool operator<(const node &oth)const{return this->l<oth.l;}
};
set<node> s;
vector<pair<ll, int> > vp;
IT split(int ind)
{
IT it = s.lower_bound(node(ind));
if(it != s.end()&&it->l == ind)return it;
--it;
int xl = it->l, xr = it->r;
ll v = it->val;
s.erase(it);
s.insert(node(xl, ind-1, v));
return s.insert(node(ind, xr, v)).first;
}
void assign(int l ,int r, ll v)
{
IT itr = split(r+1), itl = split(l);
s.erase(itl, itr);
s.insert(node(l, r, v));
}
void add(int l, int r, ll v)
{
IT itr = split(r+1), itl = split(l);
for(;itl!=itr;++itl)
itl->val += v;
}
ll krank(int l ,int r, ll k)
{
vp.clear();
IT itr = split(r+1), itl = split(l);
for(;itl!=itr;++itl)
vp.push_back(make_pair(itl->val, itl->r-itl->l+1));
sort(vp.begin(), vp.end());
for(vector<pair<ll, int> >::iterator it = vp.begin();it!=vp.end();++it)
{
k -= it->second;
if(k<=0)return it->first;
}
return -1ll;
}
ll power(ll x, ll p, ll mod)
{
ll res = 1, base = x%mod;
while(p)
{
if(p&1)res = res*base%mod;
base = base*base%mod;
p>>=1;
}
return res%mod;
}
ll sum(int l, int r, ll p, ll mod)
{
IT itr = split(r+1), itl = split(l);
ll res = 0;
for(;itl!=itr;++itl)
res = 1ll*(res+1ll*(itl->r-itl->l+1)*power(itl->val, (ll)p, (ll)mod))%mod;
return res;
}
ll n,m,seed,vmax,a[MAXN];
ll rnd()
{
ll ret = seed;
seed = (seed * 7 + 13) % MOD7;
return ret;
} int main()
{
scanf("%d %d %lld %lld",&n,&m,&seed,&vmax);
for (int i=1; i<=n; ++i)
{
a[i] = (rnd() % vmax) + 1;
s.insert(node(i,i,a[i]));
}
s.insert(node(n+1, n+1, 0));
for (int i =1; i <= m; ++i)
{
int op = int(rnd() % 4) + 1;
int l = int(rnd() % n) + 1;
int r = int(rnd() % n) + 1;
if (l > r)
std::swap(l,r);
int x, y;
if (op == 3)
x = int(rnd() % (r-l+1)) + 1;
else
x = int(rnd() % vmax) +1;
if (op == 4)
y = int(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",krank(l, r, ll(x)));
else
printf("%lld\n",sum(l, r, ll(x), ll(y)));
}
return 0;
}

四.其他题目(随时更新)

另外还有一道可以用 Chtholly Tree 吸氧做的题:P2146 [NOI2015]软件包管理器

正解是重链剖分+线段树,但是用 Chtholly Tree 吸氧也能过。

Chtholly Tree 学习笔记的更多相关文章

  1. 珂朵莉树(Chtholly Tree)学习笔记

    珂朵莉树(Chtholly Tree)学习笔记 珂朵莉树原理 其原理在于运用一颗树(set,treap,splay......)其中要求所有元素有序,并且支持基本的操作(删除,添加,查找......) ...

  2. dsu on tree学习笔记

    前言 一次模拟赛的\(T3\):传送门 只会\(O(n^2)\)的我就\(gg\)了,并且对于题解提供的\(\text{dsu on tree}\)的做法一脸懵逼. 看网上的其他大佬写的笔记,我自己画 ...

  3. Link Cut Tree学习笔记

    从这里开始 动态树问题和Link Cut Tree 一些定义 access操作 换根操作 link和cut操作 时间复杂度证明 Link Cut Tree维护链上信息 Link Cut Tree维护子 ...

  4. 矩阵树定理(Matrix Tree)学习笔记

    如果不谈证明,稍微有点线代基础的人都可以在两分钟内学完所有相关内容.. 行列式随便找本线代书看一下基本性质就好了. 学习资源: https://www.cnblogs.com/candy99/p/64 ...

  5. k-d tree 学习笔记

    以下是一些奇怪的链接有兴趣的可以看看: https://blog.sengxian.com/algorithms/k-dimensional-tree http://zgjkt.blog.uoj.ac ...

  6. splay tree 学习笔记

    首先感谢litble的精彩讲解,原文博客: litble的小天地 在学完二叉平衡树后,发现这是只是一个不稳定的垃圾玩意,真正实用的应有Treap.AVL.Splay这样的查找树.于是最近刚学了学了点S ...

  7. LSM Tree 学习笔记——本质是将随机的写放在内存里形成有序的小memtable,然后定期合并成大的table flush到磁盘

    The Sorted String Table (SSTable) is one of the most popular outputs for storing, processing, and ex ...

  8. LSM Tree 学习笔记——MemTable通常用 SkipList 来实现

    最近发现很多数据库都使用了 LSM Tree 的存储模型,包括 LevelDB,HBase,Google BigTable,Cassandra,InfluxDB 等.之前还没有留意这么设计的原因,最近 ...

  9. Expression Tree 学习笔记(一)

    大家可能都知道Expression Tree是.NET 3.5引入的新增功能.不少朋友们已经听说过这一特性,但还没来得及了解.看看博客园里的老赵等诸多牛人,将Expression Tree玩得眼花缭乱 ...

  10. K-D Tree学习笔记

    用途 做各种二维三维四维偏序等等. 代替空间巨大的树套树. 数据较弱的时候水分. 思想 我们发现平衡树这种东西功能强大,然而只能做一维上的询问修改,显得美中不足. 于是我们尝试用平衡树的这种二叉树结构 ...

随机推荐

  1. VUE学习-mixin混入

    mixin混入 混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能. 组件式混入 // 定义一个混入对象 var myMixin = { created: functi ...

  2. UE4启动顺序

    GameMode PlayerController Actor Level gameMode , playerController控制pawn , 激活默认相机active camera , getP ...

  3. JavaSE——subString()方法

    package com.zhao.stringtest;public class Test4 { //手机号屏蔽中间四位 //subString(int beginIndex,int endIndex ...

  4. taro 学习笔记

    1.Taro 是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发 微信 / 京东 / 百度 / 支付宝 / 字节跳动 / QQ / 飞书 小程序 / H5 / RN ...

  5. antd 动态添加表格列 表格不更新

    原因:react 对比columns 数组为原数组,内存地址没变: 解决办法:用es6的扩展,生成新数组,添加操作列operateCol setTableColumns([...tableColums ...

  6. [Swift] SwiftUI布局的一些写法基础(用Swift构造UI布局)

    这个文档是在你 完全熟悉 Objective-C 上用代码构造UI的前提下写的 官方教程:https://developer.apple.com/tutorials/swiftui/creating- ...

  7. 架构的生态系 资讯环境被如何设计至今.PDF

    书本详情 架构的生态系 资讯环境被如何设计至今 作者: 濱野智史出版社: 大鴻藝術股份有限公司副标题: 資訊環境被如何設計至今?原作名: アーキテクチャの生態系――情報環境はいかに設計されてきたか译者 ...

  8. vue input输入框关键字筛选检索列表数据展示

    想必大家在项目开发中难免会用到关键字筛选的功能,正好这次项目有需求要做这一块,就整理一下vue的input输入框输入关键字检索数据列表的代码.下面直接上代码: html: <!-- 筛选demo ...

  9. vscore 中 vim 常用快捷键

    谷歌浏览器 ctrl + T 新建一个页面 ctrl + J 查看下载界面 F6 直接搜索 vscore 在 vscore 中使用 vim 建议去掉 ctrl 键的功能捆绑,不然会覆盖掉很多的 vsc ...

  10. 3、app自动化:使用appium定位元素的方式及元素的常用操作

    前提: 没有的包,要先进行对应包的安装 如:pip install Appium-Python-Client 一.定位元素,包括属性定位和xpath定位方式 a\属性定位 属性 定位方式     示例 ...