浅谈珂朵莉树
0.前言
如有错误,欢迎指出。(什么错误都可以。)
前置芝士
1.还是 oi-wiki 上面的那句话, 会用 STL_SET 就行。(不会的话,也可以去学习一下。)
1.什么是珂朵莉树
当你在 oi-wiki 上面看到一个叫做珂朵莉树的数据结构时,你可能会很好奇,也可能会觉得 oi-wiki 非常高大尚。但是,今天我要在这里告诉你:珂朵莉树贼简单,在比赛中也很实用!
好,不多说废话,我们进入正题。
珂朵莉树起源题:CF896C,也是我们后面讲述的例题。可以发现,在这个题目中,前面三个操作我们都还可以运用主席树实现,但是当你看到第4个操作嘛,呵呵,我可以猜到你的心情。
珂朵莉树,又名 old driver tree
老司机树,(ODT树),在比赛中,一般 是用来骗分的。对于这一种数据结构,它只适用于随机数据,对于精心策划的数据将会被卡掉,但是在比赛中,卡珂朵莉树的数据毕竟少,所以可以在不知道正解的情况下骗到很多分。
为什么我们说他实用呢?因为在比赛中,通常出现这样的情况:正解是一棵长度需要想和写很久的权值线段树或者平衡树,但是,如果你采用珂朵莉树,你可以非常快速的的打完,并且还可以至少得到正解差不了太多分的分数(一般也就差10~20分,并且如果出题人忘记卡了的话,甚至可以得到满分)。更为重要的是,如果你正解代码出现了亿些错误,那么你将会死的更惨,而珂朵莉树易于调试不易写错的特点,则更能保证你在比赛中的成绩。
虽然珂朵莉树在比赛中比较实用,但是在学习 OI 的时候,最好还是不要使用它来骗分了,乖乖打正解吧!
2.珂朵莉树具体做法
珂朵莉树基于 STL_SET 实现,他之所以是一棵树也跟 SET 是有关系的,他的具体思想是:将一个权值相同的子段看作一个节点来看待。
我们先定义一个结构体来储存节点,即每个权值相同的区间:
#define ll long long//因为珂朵莉树可以存下长度很大的节点以及权值,所以开 long long
struct odt
{
ll l,r;//将该字段化为一个节点后,他的左端点和右端点
mutable ll val;//该字段的权值
bool operator <(const odt &n)const//重载小于运算符,因为要存入set就必须有小于符号的定义
{
return l<n.l;
}
//下面的在后面代码中有用。
odt(ll a,ll b,ll c)
{
l=a,r=b,val=c;
}
odt(ll a)
{
l=a;
}
};
刚刚在代码中,或许你看到了一个你并不认识的关键字 mutable
。mutable
译为可变的。因为我们在 CF896C 当中有区间加操作,需要给改变 \(val\) 的值,而 set 中是默认不能改的,所以我们需要加上 mutable
关键字。
然后,我们在申请一个 SET 来储存这棵珂朵莉树。
set<odt> tree;
接着,我们再宏定义一下 set 的迭代器,方便我们在以后访问时使用。
#define It set<odt>::iterator
注意,接下来是重点!
在珂朵莉树中,最重要的操作无疑是 split
操作,即分裂一个节点。这时,或许你会十分疑惑,珂朵莉树都存好了,为什么还有分裂节点呢?
我们可以先想这样一个问题,假设现在只有一个节点,他表示的是区间 \([1,8]\)。在 CF896C 中,我们有区间加操作。如果我们要将区间 \([5,7]\) 全部加上 \(3\) ,那该怎么办呢。由于我们在珂朵莉树中,不存在以 \(5\) 或 \(7\) 为开头的节点。所以我们需要将节点 \([1,8]\) 分裂成 \([1,4][5,7][8,8]\),我们才能完成操作。所以你现在明白 split
的重要性了吧。
假设我们现在需要一个以 \(x\) 为左端点的节点。我们可以现在 \(set\) 中直接二分查找,找到第一个左端点大于等于 \(x\) 的节点。如果这个左端点于 \(x\) 相等,则就说明 set 中存在以 \(x\) 为左端点的节点,我们就不需要分裂。如果不相等,我们将这个左端点所在的节点的上一个节点进行分裂。(因为上一个节点才包含我们要找的 \(x\)。)最后,我们再返回这个节点所在的迭代器。
代码:
It split(ll x)//分裂,返回以 x 为左端点的节点的迭代器
{
It it=tree.lower_bound(odt(x));//直接进行二分查找
if(it!=tree.end()&&it->l==x)//如果相等,且不是 set 中的尾指针
return it;//直接返回迭代器。
it--;//我们将当前迭代器减1,因为要找上一个。
ll l=it->l,r=it->r,val=it->val;//我们找到上一个节点的所有信息,储存下来,以免在删除时指针失效无法访问。
tree.erase(it);//将该节点删除
tree.insert(odt(l,x-1,val));//先将该节点分裂为区间 [l,x-1]
return tree.insert(odt(x,r,val)).first;//然后以 x 为左端点再分裂一个区间,由于 insert 后返回的是一个 pair 类型,所以我们需要返回 first 来返回迭代器。
}
时间复杂度的保证-Assign
显然,如果我们一直分裂下去,节点就会越来越多,渐渐退化成暴力,这样是对我们不利的。所以我们需要去改变。
现在有一个操作,assign,推平一个区间,将一个区间赋成同一个值。显然,我们可以直接取出在这个区间里面的所有节点,然后直接删除,最后在新建一个节点来表示这个区间。
这样,我们就能有效控制节点个数,保证时间复杂度了。由于本人能力有限,不会证明复杂度,所以我就在这里贴一个珂朵莉时间复杂度的证明。
Assign 代码:
void assign(ll l,ll r,ll val)//将 [l,r] 赋值为 val
{
It it2=split(r+1),it1=split(l);//我们无论是什么操作,我们都先取出右端点,因为先取左端点的话,左端点的指针可能会失效。
tree.erase(it1,it2);//删除区间 [l,r+1)
tree.insert(odt(l,r,val));//新建以l为左端点,r为右端点的节点
}
其他操作
一个比一个更暴力更玄学,就是把这个区间所有的节点取出来就可以了。
操作三,求区间第 \(k\) 小整数示例代码:
ll qsort(ll l,ll r,ll x)
{
It it2=split(r+1),it1=split(l);//还是像往常一样,取出以左端点开始,和 右端点加一的 迭代器。
vector<pair<ll,ll> > p;//为了方便排序,直接用pair
for(It it=it1;it!=it2;it++)
{
p.push_back(make_pair(it->val,it->r-it->l+1));//将区间中所有节点放入p数组。
}
stable_sort(p.begin(),p.end());//将p数组排序
for(ll i=0;i<p.size();++i)
{
x-=p[i].second;//每次减去这个结点的长度
if(x<=0)//如果小于等于0,那显然第 x 小的整数就在这个节点上。
return p[i].first;
}
}
操作4,求区间每个数字的 \(x\) 次方模 \(y\) 的值的和 实例代码:
ll qpow(ll a,ll b,ll p)//快速幂 a^b%p
{
ll ans=1;
a%=p;
while(b)
{
if(b&1)
(ans*=a)%=p;
(a*=a)%=p;
b>>=1;
}
return ans%p;
}
ll pow_mod(ll l,ll r,ll x,ll y)
{
It it2=split(r+1),it1=split(l);//照常取出
ll ans=0;//答案
for(It it=it1;it!=it2;it++)
{
//将每个节点暴力取出算快速幂就可以了。
ll res=qpow(it->val,x,y)*(it->r-it->l+1);
res%=y;
ans+=res;
ans%=y;
}
return ans;//返回
}
我们很早就说到,珂朵莉树只适合解决随机生成的数据,而这个题目不就是给你随机数种子自己生成数据吗,那珂朵莉树的复杂度是肯定可以承受住这个题目的了。
最后,贴上完整代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mod int(1e9+7)
#define It set<odt>::iterator
ll n,m;
ll seed,vmax;
ll a[1000005];
ll rnd()
{
ll res=seed;
seed=(seed*7+13)%mod;
return res;
}
ll qpow(ll a,ll b,ll p)
{
ll ans=1;
a%=p;
while(b)
{
if(b&1)
(ans*=a)%=p;
(a*=a)%=p;
b>>=1;
}
return ans%p;
}
struct odt
{
ll l,r;
mutable ll val;
bool operator <(const odt &n)const
{
return l<n.l;
}
odt(ll a,ll b,ll c)
{
l=a,r=b,val=c;
}
odt(ll a)
{
l=a;
}
};
set<odt> tree;
It split(ll x)
{
It it=tree.lower_bound(odt(x));
if(it!=tree.end()&&it->l==x)
return it;
it--;
ll l=it->l,r=it->r,val=it->val;
tree.erase(it);
tree.insert(odt(l,x-1,val));
return tree.insert(odt(x,r,val)).first;
}
void assign(ll l,ll r,ll val)
{
It it2=split(r+1),it1=split(l);
tree.erase(it1,it2);
tree.insert(odt(l,r,val));
}
void add(ll l,ll r,ll val)
{
It it2=split(r+1),it1=split(l);
for(It it=it1;it!=it2;it++)
{
it->val+=val;
}
}
ll qsort(ll l,ll r,ll x)
{
It it2=split(r+1),it1=split(l);
vector<pair<ll,ll> > p;
for(It it=it1;it!=it2;it++)
{
p.push_back(make_pair(it->val,it->r-it->l+1));
}
stable_sort(p.begin(),p.end());
for(ll i=0;i<p.size();++i)
{
x-=p[i].second;
if(x<=0)
return p[i].first;
}
}
ll pow_mod(ll l,ll r,ll x,ll y)
{
It it2=split(r+1),it1=split(l);
ll ans=0;
for(It it=it1;it!=it2;it++)
{
ll res=qpow(it->val,x,y)*(it->r-it->l+1);
res%=y;
ans+=res;
ans%=y;
}
return ans;
}
int main()
{
scanf("%lld%lld%lld%lld",&n,&m,&seed,&vmax);
for(int i=1;i<=n;++i)
{
a[i]=rnd()%vmax+1;
tree.insert(odt(i,i,a[i]));
}
tree.insert(odt(n+1,n+1,0));
while(m--)
{
int op=rnd()%4+1,l=rnd()%n+1,r=rnd()%n+1,x,y;
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;
if(op==1)
add(l,r,x);
else if(op==2)
assign(l,r,x);
else if(op==3)
printf("%lld\n",qsort(l,r,x));
else
printf("%lld\n",pow_mod(l,r,x,y));
}
return 0;
}
3.后记
珂朵莉树这东西好是好,简单是简单。但是我还是觉得平常练习的时候就不要用了,要是教练看到你用了,一气之下把你卡掉那可就不好玩咯。
至于有关珂朵莉树的例题嘛,还是很多的,可以参见 oi-wiki 和其他有关珂朵莉树的文章。