Chtholly Tree 学习笔记
前言
珂朵莉树 (Chtholly Tree) 是一种简单优美的数据结构,就像 Chtholly 一样可爱。暴力即优美。 适用于一些有区间赋值操作的序列操作题。
Chtholly Tree 的本质是把一个序列分成几个连续区间,每个区间内的元素的值相同,然后用一个 std::set 维护所有区间。
然后就可以通过一些神奇的操作,做到在数据随机的情况下 \(O(logn)\) 查询区间信息。时间复杂度我不会证QAQ。
当然也可以手写 std::set ,但是那样子还不如直接用平衡树做题。
事实上,Chtholly Tree 的适用范围很小,只能在某些保证数据随机且有区间赋值操作的题目中使用,别的情况下就是“你比暴力多个 log ”。而且一般来说出题人没有不卡 Chtholly Tree 的。虽然有时可以吸氧水过去。
竞赛一般不会考 Chtholly Tree ,但是多学一种数据结构也不是坏事嘛。尤其是 Chtholly 这么可爱。
零. 前置知识
你需要关于 std::set 的基础知识:
-
set
s; 建立一个类型为 type 的set。 -
s.insert(x); 向 \(s\) 中插入一个值为 \(x\) 的元素。该函数的返回值为 pair<iterator,bool>,之后会用到这一返回值。
-
s.erase(itl, itr); 删除 s 中的一段区间\([itl,itr)\),其中 \(itl\), \(itr\) 的类型为 set
::iterator,也就是两个迭代器。 -
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));
}
值得一提的地方:
-
为了保证区间有序,std::set 中的 node 是按照 \(l\) 来排序的。也可以理解为我们以 \(l\) 作为这个区间的代表。
-
由于 std::set 自身的原因,\(val\) 前必须有 mutable,以便支持区间修改等操作。
-
在插入所有元素后需要再插入一个虚拟区间,以保证之后在查找区间时不会出错。
这样我们就建好了一棵 Chtholly Tree。
但是这样就和原序列完全一致了,一共有 \(n\) 个小区间。所以我们需要一些操作来减少区间的数量。
二. 核心操作:split 和 assign
要想维持珂朵莉树的优秀时间复杂度,这两个操作必不可少。
1.split(index)
这个操作把 std::set 维护的区间从 \(index\) 分成两段,且不改变不包含下标 \(index\) 的区间。
步骤如下:
-
首先在已有的区间中查找 \(l=index\) 的区间,如果找到了就直接返回,否则进行下一步操作。
-
经过上一步操作,我们要找的 \(index\) 一定已经被包含在一个区间中,所以我们要把包含 \(index\) 的区间分成两个更小的区间。具体来说,我们找到包含 \(index\) 的区间,然后删除该区间 \([l,r]\) ,再在 std::set 中插入区间 \([l,index-1]\) 和区间 \([index,r]\) ,\(val\) 值当然都为原区间的 \(val\)。
-
\(\operatorname{split}\) 操作会增加 std::set 维护的区间数量,但是这对时间复杂度基本不影响。
-
\(\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));
}
值得注意的地方:
- \(\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 吸氧也能过。