珂朵莉树学习笔记
一类依据区间推平,set保证复杂度的数据结构。
CF896C
思路:
发现这题有两个特点:区间赋值和数据随机。
那么每四个操作就有一个操作 2
,并且因为 \(l,r\) 随机,每次 2
操作都会把一段较长的区间改为相同的数,那么自然可以想到用三个变量 \(l,r,v\) 表示这段数值相同的区间的左端点,右端点以及值。
本题中,整个序列可以分为多个这种区间,于是使用结构体存储。
因为有区间赋值,因此每个区间应当是 可修改的 ,于是我们使用C++中的 mutable
来存储值。
为了便于对整个区间进行操作,我们把这些结构体压入一个 set
中(用来保证 \(O(n\ log\log n )\) 的时间复杂度),因此需要定义排序方式。
struct node {
int l,r;
mutable int v;
friend bool operator < (node a, node b) {
return a.l<b.l;
}
};
考虑如何维护题目中的操作。
对于操作1,我们对每个被查询区间中 被完全包围 的区间直接对 v
加上 x
。
考虑如何处理 不被完全包围 的区间。
我们考虑像线段树一样将区间分为两个区间,但是不同的是,线段树通过将区间分为长度相等的两个区间保证复杂度 ,而珂朵莉树只需要直接把区间 \([l,r]\) 分为 \([l,askr]\) 和 \([askr,r]\) 即可。
因此我们可以写出 split
函数的代码:
inline sti split(int pos) {
sti it=s.lower_bound(node{pos,0,0}});//找到左端点
if(it!=s.end() && it->l==pos) return it;//直接包含
//如果不满足1说明当前这个区间比需要的区间大
//如果不满足2说明 pos 超过了最后一个区间的右端点
it--;//往前一个
if(it->r<pos) return s.end();
//如果最后一个的右端点还是比 pos 小就说明 pos 太大,返回 s.end() 即可。
T l=it->l,r=it->r,v=it->v;//进行分割
s.erase(it),s.insert(node(l,pos-1,v));
//删掉原来的,把区间分成[l,pos-1],[pos,r]两段
return s.insert(node(pos,r,v)).first;
//s.insert()返回一个 pair ,第一个就是新插入的结构体的位置
}
对于操作2,考虑将 不被完全包围 的区间分解后直接修改 v
即可。
注意区间赋值函数同样是 保证复杂度的关键。
因为将左右端点之间的 sti
都删除了,所以大大减少了区间数量。
inline void assign(int l, int r, int x) {
sti ir=split(r+1),il=split(l);
s.erase(il,ir);
s.insert(l,r,x);
}
对于操作3,考虑将区间暴力取出丢到一个 vector
里面然后排序。因为区间个数不会太多,所以复杂度大概是 \(O({\log}^2 n )\) 的。
对于操作4,同样暴力,复杂度也是 \(O({\log}^2 n )\) 的。
需要注意在修改和查询时,需要先 split
右端点再 split
左端点。因为如果先 split
左端点,存储下来的 sti
可能会在 split
右端点时被删除。
完整代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define sti set<node>::iterator
const ll MOD=1000000007;
const ll MAXN=100005;
struct node {
ll l,r;
mutable ll v;
node(ll l,ll r=0,ll v=0):l(l),r(r),v(v){}
bool operator < (const node&a) const {
return l<a.l;
}
};
ll n,m,seed,vmax,a[MAXN];
set<node>s;
sti split(int pos) {
sti it=s.lower_bound(node(pos));
if(it!=s.end()&&it->l==pos) return it;
it--;
if(it->r<pos) return s.end();
ll l=it->l;
ll r=it->r;
ll v=it->v;
s.erase(it);
s.insert(node(l,pos-1,v));
return s.insert(node(pos,r,v)).first;
}
void add(ll l,ll r,ll x) {
sti itr=split(r+1),itl=split(l);
for(sti it=itl;it!=itr;++it)
it->v+=x;
}
void assign(ll l,ll r,ll x) {
sti itr=split(r+1),itl=split(l);
s.erase(itl,itr);
s.insert(node(l,r,x));
}
ll rnk(ll l,ll r,ll x) {
sti itr=split(r+1),itl=split(l);
vector<pair<ll,ll> >v;
for(sti i=itl;i!=itr;++i) v.push_back(make_pair(i->v,i->r-i->l+1));
sort(v.begin(),v.end());
int i;
for(i=0;i<v.size();++i)
if(v[i].second<x) x-=v[i].second;
else break;
return v[i].first;
}
ll ksm(ll x,ll y,ll p) {
ll r=1;
ll base=x%p;
while(y) {
if(y&1) r=r*base%p;
base=base*base%p;
y>>=1;
}
return r;
}
ll calP(ll l,ll r,ll x,ll y) {
sti itr=split(r+1),itl=split(l);
ll ans=0;
for(sti i=itl;i!=itr;++i) ans=(ans+ksm(i->v,x,y)*(i->r-i->l+1)%y)%y;
return ans;
}
ll rnd() {
ll ret=seed;
seed=(seed*7+13)%MOD;
return ret;
}
int main() {
cin>>n>>m>>seed>>vmax;
for(int i=1;i<=n;++i) {
a[i]=(rnd()%vmax)+1;
s.insert(node(i,i,a[i]));
}
for(int i=1;i<=m;++i) {
ll op,l,r,x,y;
op=(rnd()%4)+1;
l=(rnd()%n)+1;
r=(rnd()%n)+1;
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) cout<<rnk(l,r,x)<<endl;
else cout<<calP(l,r,x,y)<<endl;
}
return 0;
}
其实不是很难码,注意 split
就可以了。
练习
1.P3740
使用珂朵莉树每次暴力覆盖即可。
2.CF915E
很套路的珂朵莉树,注意在每次修改的时候记录工作日时间而不是每次扫一遍。
3.P3071
A
操作即为暴力遍历整个 set
,如果有就赋值为1。
L
操作就是区间赋值。
4.P2894
同3
5.CF817F
珂朵莉树维护即可。