前言

珂朵莉树 (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 吸氧也能过。

最新文章

  1. php一句话后门过狗姿势万千之后门构造与隐藏【二】
  2. angular.js学习笔记之一
  3. 如何为Kafka集群选择合适的Partitions数量
  4. 【html】【12】特效篇--轮播图
  5. 006 列表的三种删除方法 remove,pop,del
  6. [翻译]理解 ASP.NET 5
  7. Git和Github的配合使用
  8. 【深度学习系列】用PaddlePaddle和Tensorflow实现GoogLeNet InceptionV2/V3/V4
  9. ThinkPHP5.1 + tufanbarisyildirim 解析apk
  10. 痞子衡嵌入式:ARM Cortex-M内核那些事(2)- 第一款微控制器
  11. 解决Mac应用程序软件不出现在Launchpad里面的方法
  12. VC操作excel
  13. c++引用和指针的彻底理解
  14. Mac重要目录
  15. PHPMyWind5.4存储XSS后续getshell提权
  16. 用css绘制图形
  17. [原创]WebScarab工具介绍
  18. python中的re模块中的向后引用和零宽断言
  19. [3] 球(Sphere)图形的生成算法
  20. 小米2S手机 - Charles无法安装证书 因为无法读取证书

热门文章

  1. doskey: windows版 Alias
  2. 深入理解webpack的chunkId对线上缓存的思考(转载)
  3. web基础(3):CSS样式
  4. react hook入门
  5. mxArray 和 mwArray 的区别
  6. Selenium私房菜系列6 -- 深入了解Selenium RC工作原理(1)【QQ】
  7. ScrollView里面不能嵌套一个FlatList,这个需要如何通过FlatList自己单独实现
  8. exp1-Password engine-加密API实现与测试
  9. 小程序微信支付完整demo,包含退款
  10. 改变Jupyter notebook默认浏览器