0.前情提要

在学长的课上听到了颜色段覆盖问题,顺手讲了个ODT,当时的我以为就是一个可爱的数据结构,但是学下来发现确实很可爱(。・ω・。)

CF896C——ODT的起源

你需要知道——STL的set怎么用(cppreference

对就这些。

1.什么是珂朵莉树

珂朵莉树,又称ODT(老司机树),是一种利用set的暴力数据结构,用于解决区间问题的数据结构。

2.珂朵莉树可以解决什么问题

区间推平问题出现的时候我们就可以用珂朵莉树了。她高效的基础是当数据随机生成的时候,复杂度是正确的(见下文),但是若没说,你就要开赌,这个很容易被构造数据卡掉。

3.珂朵莉树的构造

借用一下大佬的图

对于珂朵莉树的节点,我们维护的是一个数值相同的区间,如上图。我们考虑维护一个三元组(l,r,val)(l,r,val),其中l和r是数值相同区间的左右端点,val就是这个区间对应的数值,那么每个相同数列的区间就可以浓缩为一个节点,如下图

当然在维护区间的时候我们要满足区间左端点和右端点都样单调递增,这里只需要比较左端点就可以了。

代码如下

当然对于CF896C我们一开始维护n个长度为1的单点

1
2
3
4
5
6
7
8
9
10
11
12
13
struct kdltree
{
int l,r;
mutable ll val;//set里面的元素一般不可修改,用mutable修饰就珂以修改
kdltree(int L,int R=-1,ll V=0){
//构造函数,左端点是必须有,若后面不添加R和V(就是val)默认就是单独的一个端点[pos,pos]
l=L,r=R,val=V;
}
bool operator<(const kdltree&a)const{
return l<a.l;
}
};
set<kdltree> odt;

4.split分裂区间

如果我们想要的修改范围对于一个大区间来说小的话,我们就需要把区间拆开成我们想要的

split就是干这个的,他的唯一一个需要的参数就是pos,代表意义就是把一个[L,R][L,R]的区间劈成[L,pos1][L,pos-1],[pos,R][pos,R]

很显然利用set的平衡树特性,我们可以用lower_bound函数找到其对应的区间节点

接下来分类讨论,其实也挺简单的

  1. 如果pos刚好是区间节点的左端点(就是找到pos的具体数值了),那么分裂都不用分,直接返回即可
  2. 如果不是,那么因为lower_bound是\ge的操作,那么这里返回的是pos右边对应的区间节点,那么我们只需要将获取到的迭代器it–就可以获得其对应的节点,让后一刀劈成[L,pos1][L,pos-1],[pos,R][pos,R]

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
//分割区间,把区间分成[l,pos-1],[pos,r]
auto split(int pos){
auto it=odt.lower_bound(kdltree(pos));//找第一个左端点不小于pos的的区间
if(it!=odt.end()&&it->l==pos){
return it;//pos是左端点是无需分裂
}
it--;//pos一定在前一个区间;
int l=it->l,r=it->r;
ll val=it->val;
odt.erase(it);//删除原来的区间
odt.insert(kdltree(l,pos-1,val));
return odt.insert(kdltree(pos,r,val)).first;//这里返回的是指向[pos,r]这个区间的迭代器
}

5.区间推平

其实在构造区间节点的时候我们发现,这个数据结构其实就是天生适合区间推平这个操作的,因为她刚好存储的就是一个区间相同的值的。

例如我们将[2,8][2,8]这个区间推平改为666,set里面有4个区间节点。首先我们发现[8,10][8,10]是一个区间,那么要先split(8+1),拆成[8,8][8,8][9,10][9,10]两个区间。同理,[1,2][1,2]这个区间我们要拆成[1,1][1,1][2,2][2,2]

1
2
3
4
5
6
7
8
9
10
//合并区间并赋值x
void merge(int l,int r,ll val){
//注意顺序!先右端点后左端点不然可能会RE!
auto itr=split(r+1);
auto itl=split(l);
odt.erase(itl,itr);//删除[l,r]区间内所有元素
//其实这里是[l,r+1)(由于set删除特性经典左闭右开)
//但是r+1取不到也就只能取到r了
odt.insert(kdltree(l,r,val));//插入大区间
}

注意这里不能先分左再分右!

先分左再分右,你会发现这个itr和itl指向的东西出事了,如果你erase就会出问题,直接RE

但是问题在于这个顺序反了也有可能会AC,这个RE的概率是大约1%,随机RE。十分的“珂”学。最好还是先右再左

6.更新查询操作

对于任意的更新查询操作我们可以套如下的模板

  1. 先split右,在split左把区间[l,r][l,r]搞出来
  2. 暴力,启动!

这里我们贴上CF896C的几个操作,体会一下模板的套用(゚∀。)

  1. 区间加
1
2
3
4
5
6
7
void add(int l,int r,ll val){
auto itr=split(r+1);
auto itl=split(l);
for(auto it=itl;it!=itr;it++){
it->val+=val;//这就是为啥用mutable的原因
}
}

2.区间第k小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef pair<ll,int> kdl;//前人种树后人看动漫
//区间第k小
ll topk(int l,int r,int k){
vector<kdl> a;
auto itr=split(r+1);
auto itl=split(l);
for(auto it=itl;it!=itr;it++){
a.push_back(kdl(it->val,it->r-it->l+1));
//sort会先比较val,如果val相同则比较区间长度
//升序保证第k小,第k大倒序即可
}
sort(a.begin(),a.end());
for(auto it=a.begin();it!=a.end();it++){
k-=it->second;
if(k<=0){
return it->first;//找到了
}
}
return -1;//不好没找到
}
  1. 区间幂次和(这里使用的是快速幂)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ll fastpow(ll x,ll y,ll mod){
ll res=1;
x%=mod;
while (y>0)
{
if(y&1){
res=res*x%mod;
}
x=x*x%mod;
y>>=1;
}
return res;
}

ll sum(int l,int r,int x,int y){
ll ans=0;
auto itr=split(r+1);
auto itl=split(l);
for(auto it=itl;it!=itr;it++){
ans=(ans+fastpow(it->val,x,y)*(it->r-it->l+1))%y;
}

7.时间复杂度

这里前提条件是数据随机情况下复杂度正确,如果数据不随机会出现O(nm)O(nm)的错误时间复杂度

一般来说仅区间推平这一个区间操作的时间复杂度是均摊O(log2log2n)O(log_2log_2n),如果有暴力操作就是O(log2n)O(log_2n),我们发现这个数据结构能够高效的关键在于区间推平,区间推平能把小区间合并成一个大区间,这是珂朵莉树高效的关键,也是她适用范围小的原因。

很容易被构造数据卡掉,导致变成一个一个散点。

8.代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#include<iostream>
#include<set>
#include<algorithm>
#include<vector>
#define ll long long
using namespace std;
const int MOD=1000000007,MN=1e5+15;
ll n,m,seed,vmax;
ll a[MN];
ll rnd(){
ll ret=seed;
seed=(seed*7+13)%MOD;
return ret;
}

struct kdltree
{
int l,r;
mutable ll val;//set里面的元素一般不可修改,用mutable修饰就珂以修改
kdltree(int L,int R=-1,ll V=0){
//构造函数,左端点是必须有,若后面不添加R和V(就是val)默认就是单独的一个端点[pos,pos]
l=L,r=R,val=V;
}
bool operator<(const kdltree&a)const{
return l<a.l;
}
};
set<kdltree> odt;

//分割区间,把区间分成[l,pos-1],[pos,r]
auto split(int pos){
auto it=odt.lower_bound(kdltree(pos));//找第一个左端点不小于pos的的区间
if(it!=odt.end()&&it->l==pos){
return it;//pos是左端点是无需分裂
}
it--;//pos一定在前一个区间;
int l=it->l,r=it->r;
ll val=it->val;
odt.erase(it);//删除原来的区间
odt.insert(kdltree(l,pos-1,val));
return odt.insert(kdltree(pos,r,val)).first;//这里返回的是指向[pos,r]这个区间的迭代器
}

//合并区间并赋值x
void merge(int l,int r,ll val){
//注意顺序!先右端点后左端点不然可能会RE!
auto itr=split(r+1);
auto itl=split(l);
odt.erase(itl,itr);//删除[l,r]区间内所有元素
//其实这里是[l,r+1)(由于set删除特性经典左闭右开)
//但是r+1取不到也就只能取到r了
odt.insert(kdltree(l,r,val));//插入大区间
}
//所有的区间操作可以套一个模板
//先split右,在split左把区间[l,r]搞出来
//让后直接暴力!

//区间加
void add(int l,int r,ll val){
auto itr=split(r+1);
auto itl=split(l);
for(auto it=itl;it!=itr;it++){
it->val+=val;//这就是为啥用mutable的原因
}
}

typedef pair<ll,int> kdl;//前人种树后人看动漫
//区间第k小
ll topk(int l,int r,int k){
vector<kdl> a;
auto itr=split(r+1);
auto itl=split(l);
for(auto it=itl;it!=itr;it++){
a.push_back(kdl(it->val,it->r-it->l+1));
//sort会先比较val,如果val相同则比较区间长度
//升序保证第k小,第k大倒序即可
}
sort(a.begin(),a.end());
for(auto it=a.begin();it!=a.end();it++){
k-=it->second;
if(k<=0){
return it->first;//找到了
}
}
return -1;//不好没找到
}

ll fastpow(ll x,ll y,ll mod){
ll res=1;
x%=mod;
while (y>0)
{
if(y&1){
res=res*x%mod;
}
x=x*x%mod;
y>>=1;
}
return res;
}

ll sum(int l,int r,int x,int y){
ll ans=0;
auto itr=split(r+1);
auto itl=split(l);
for(auto it=itl;it!=itr;it++){
ans=(ans+fastpow(it->val,x,y)*(it->r-it->l+1))%y;
}
return ans;
}
int main(){
cin>>n>>m>>seed>>vmax;
for(int i=1;i<=n;i++){
a[i]=rnd()%vmax+1;
odt.insert(kdltree(i,i,a[i]));
}
for(int i=1;i<=m;i++){
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){
merge(l,r,x);
}else if(op==3){
cout<<topk(l,r,x)<<endl;
}else cout<<sum(l,r,x,y)<<endl;
}
return 0;
}

10.拓展

ODT映射思想的推广