【Study】ODT
复习笔记(
学习 \(ODT\) 怎么能没有珂朵莉呢qwq(大雾)
【模板题】CF896C Willem, Chtholly and Seniorious
简介
\(ODT\) , 又名珂朵莉树,一种比较暴力的数据结构,基于 \(STL\) 里的 \(set\) 实现,其核心思想就是将相同值的区间合并成一个节点,对于某些需要区间修改的题目有奇效,但是要求数据一定要随机,否则就会退化为暴力qwq
优点:码量少,操作简单,易查错,在数据随机的情况下效率高
缺点:需要题目包含区间修改操作,容易被卡,在数据不随机的情况下会退化成暴力
实现
1、初始化
用结构体来储存节点信息。
struct node{
int l,r;//这个节点所包含的区间
mutable int v;//这个节点的值
node(int _l,int _r=0,int _v=0){l=_l;r=_r;v=_v;}
bool operator < (const node &a) const {return l
如何理解每一个节点呢?
假设有这么一段数列
1 1 1 1 4 4 3
那么我们就会构建三个节点,分别如下:
id:1 -> l=1 r=4 v=1
id:2 -> l=5 r=6 v=4
id:3 -> l=7 r=7 v=3
但是在一开始输入初始数组时,我们仅需对于每一个值建立一个节点,相同值的合并可以留到后面的操作来处理。
下面要注意有一个点,这里有个 mutable
,由于在 \(set\) 中,元素是以常量方式储存的,我们无法对其直接进行修改,因为 \(set\) 里面元素的排列顺序是个根据 \(l\) 的大小,与 \(v\) 无关,所以我们可以加个 mutable
来直接修改 \(v\) ,否则就要先删除元素再插入元素,降低了效率。
接着再
set odt;
就行了
2、\(Slpit\) 操作
这是 \(ODT\) 的核心操作之一,当某处需要修改时,发现这一处包含在一个节点之内,如何处理?我们就需要将这个节点拆成两部分。
假如我们 \(Split(i)\) ,且 \(i\) 包含在区间 \([l,r]\) 中,那么执行 \(Split\) 操作之后这个区间就会分为 \([l,i-1]\) 和 \([i,r]\) 这两个区间,接着就可以愉快地进行修改或查询了。
#define iter set::iterator
iter Split(int x)
//注意我们返回的是一个迭代器方便查找修改
{
if(x>n) return odt.end();
//如果查询的位置大于n就不必分裂
iter it=--odt.upper_bound(node(x));
//找到包含x的区间,即找到包含x的区间的左端点
if(it->l==x) return it;
//为左端点不必分裂
int l=it->l,r=it->r,v=it->v;
//先将节点的信息取出来
odt.erase(it);
//删掉节点
odt.insert(node(l,x-1,v));
//插入前半段[l,x-1],值为v
return odt.insert(node(x,r,v)).first;
//插入后半段,再返回后半段的迭代器
}
3、\(assign\) 操作
这也是 \(ODT\) 的核心操作之一,是实现区间合并的主要操作
void Assign(int l,int r,int v)
//将区间[l,r]都改为v且保存到一个节点
{
iter itr=Split(r+1),itl=Split(l);
//找出修改区间的起点和终点
odt.erase(itl,itr);
//删除区间[l,r+1)
odt.insert(node(l,r,v));
//插入节点
}
这里有个细节,对于分裂区间 \([l,r+1)\) 要先执行 Split(r+1)
,再执行 Split(l)
假如 \(l\) 和 \(r\) 均包含在区间 \([L,R]\) 。
我们先 Split(l)
,那么某段区间就会分解为 \([L,l-1]\) 和 \([l,R]\) ,并且返回 \([l,R]\) 的迭代器;
接着 Split(r+1)
,这时就会将 \([l,R]\) 分解为 \([l,r-1]\) 和 \([r,R]\) ,并且删除 \([l,R]\) 的迭代器,第一次返回的迭代器被删除了,这时就会导致 \(Re\)
所以切记,先执行 Split(r+1)
,再执行 Split(l)
qwq
3、其他操作
区间加
分裂后直接暴力(
#define iter set::iterator
void add(int l,int r,int val)
{
iter itr=split(r+1),itl=split(l);
for(;itl!=itr;++itl)
itl->v+=val;
}
区间第 \(k\) 小
取出区间直接 \(sort\) (
#define iter set::iterator
struct Rank{
int num,cnt;
Rank(int _num,int _cnt){num=_num;cnt=_cnt;}
bool operator < (const Rank &a) const {return num ve;
for(;itl!=itr;++itl)
ve.push_back(Rank(itl->v,itl->r - itl->l + 1));
sort(ve.begin(),ve.end());
int i=0;
for(;i
至于说为什么用 \(vector\) ,学着别人写的((
区间 \(N\) 次方和
这个询问有点神奇,恐怕其他数据结构没法处理qwq
对于 \(ODT\) 还是暴力(
#define iter set::iterator
int askpower(int l,int r,int b,int p)
{
iter itr=split(r+1),itl=split(l);
int ans=0;
for(;itl!=itr;++itl)
ans+=(Power(itl->v,b,p)*(itl->r - itl->l + 1)%p),ans%=p;
return ans;
}
//Power(a,b,p)为快速幂,求a^b%p
反正对于什么修改询问操作,都这么搞
#define iter set::iterator
void qwq(int l,int r,...)
{
iter itr=split(r+1),itl=split(l);
for(;itl!=itr;++itl)
{
...
//一通乱搞(
}
}
总结
其实 \(ODT\) 就是一种优化的暴力,优化的思想也挺容易的。
如果保证数据随机,那么区间个数并不会太多,差不多是在 \(log\) 级别的,复杂度也就是 \(O(mlogn)\) ,起伏并不会太大。
关于详细的复杂度证明可以看这篇文章 珂朵莉树的复杂度分析
对于数据随机的题目可以试着打棵 \(ODT\) ,能和一般的数据结构匹敌,甚至出现意想不到的结果。但如果数据不随机,出题人造数据时可以将区间的大小缩小, \(ODT\) 所储存的区间个数就会增大,硬生生卡成暴力,然后出现意想不到的结果(
总的来说,平常刷题是可以将 \(ODT\) 当正解打,但是在考场上千万不要完全依靠 \(ODT\) ,你可以拿它来骗分,骗分的多少就是运气,但是如果时间充裕的话还是骗完分后想想正解吧qwq
参考资料:
OI Wiki