【学习笔记】高级数据结构
平衡树
平衡树一般操作是插入、删除、查排名、查排名对应权值以及查前驱后继。
Treap
算法思想
Treap 是二叉搜索树与堆的结合,给每个节点一个随机附加权值 \(pri\),根据原权值维护二叉搜索树,根据随机附加权值维护堆。
也就是保证,对于每个节点,其左子树内原权值都小于该节点原权值,右子树内原权值都大于该节点原权值,而其子树内所有节点的附加权值都大于该节点附加权值。
也就是当随机的附加权值不满足堆的性质时,我们需要调整树的形态以保证 \(\log\) 的期望树高。
具体操作
-
左旋右旋:Treap 的核心,对于节点 \(x\),其左右儿子 \(y,z\),右旋即,将 \(y\) 作为根,而 \(x\) 成为 \(y\) 的右儿子,对原权值而言,\(y\) 原来的右儿子 \(c\) 满足:\(y<c<x<z\),因此 \(c\) 就应当放在 \(x\) 的左儿子处。左旋同理。
-
插入:按照原权值在树上找到对应位置,如果没有相等位置则新开一个节点,根据附加权值调整旋转。
-
删除:找到对应位置,讨论节点是否会删去,若会删去则讨论儿子个数,单个儿子直接提上来,两个儿子将当前节点转下去。
-
其余查询操作:平衡树上二分。
模板
点击查看代码
struct Treap{
int rt,tot;
int ch[maxn][2],val[maxn],pri[maxn],cnt[maxn],siz[maxn];
inline void push_up(int x){
siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];
}
inline int new_node(int k){
++tot;
val[tot]=k,pri[tot]=rand();
cnt[tot]=1,siz[tot]=1;
return tot;
}
inline void build(){
new_node(-inf),new_node(inf);
rt=1,ch[1][1]=2;
push_up(rt);
}
inline void left_rotate(int &x){
int y=ch[x][1];
ch[x][1]=ch[y][0],ch[y][0]=x;
push_up(x),push_up(y);
x=y;
}
inline void right_rotate(int &x){
int y=ch[x][0];
ch[x][0]=ch[y][1],ch[y][1]=x;
push_up(x),push_up(y);
x=y;
}
void insert(int &x,int k){
if(!x) return x=new_node(k),void();
if(k==val[x]) ++cnt[x];
else if(k<val[x]){
insert(ch[x][0],k);
if(pri[ch[x][0]]<pri[x]) right_rotate(x);
}
else{
insert(ch[x][1],k);
if(pri[ch[x][1]]<pri[x]) left_rotate(x);
}
push_up(x);
}
void erase(int &x,int k){
if(!x) return;
if(k==val[x]){
if(cnt[x]>1) --cnt[x];
else if(!ch[x][0]||!ch[x][1]) x=ch[x][0]+ch[x][1];
else if(pri[ch[x][0]]<pri[ch[x][1]]){
right_rotate(x);
erase(ch[x][1],k);
}
else{
left_rotate(x);
erase(ch[x][0],k);
}
}
else if(k<val[x]) erase(ch[x][0],k);
else erase(ch[x][1],k);
push_up(x);
}
int rk(int x,int k){
if(!x) return 0;
if(k==val[x]) return siz[ch[x][0]]+1;
else if(k<val[x]) return rk(ch[x][0],k);
else return siz[ch[x][0]]+cnt[x]+rk(ch[x][1],k);
}
int kth(int x,int k){
if(!x) return 0;
if(siz[ch[x][0]]>=k) return kth(ch[x][0],k);
else if(siz[ch[x][0]]+cnt[x]>=k) return val[x];
else return kth(ch[x][1],k-(siz[ch[x][0]]+cnt[x]));
}
inline int get_pre(int k){
int x=rt,res=-inf;
while(x){
if(k==val[x]){
if(ch[x][0]){
x=ch[x][0];
while(ch[x][1]) x=ch[x][1];
res=val[x];
}
break;
}
if(k<val[x]) x=ch[x][0];
else{
res=max(res,val[x]);
x=ch[x][1];
}
}
return res;
}
inline int get_nxt(int k){
int x=rt,res=inf;
while(x){
if(k==val[x]){
if(ch[x][1]){
x=ch[x][1];
while(ch[x][0]) x=ch[x][0];
res=val[x];
}
break;
}
if(k<val[x]){
res=min(res,val[x]);
x=ch[x][0];
}
else x=ch[x][1];
}
return res;
}
}T;
fhq-Treap
算法思想
无旋 Treap,通过分裂合并来维持平衡。
合并就是使其满足堆的性质,分裂一般按照一个阈值 \(k\),分成原权值小于等于和大于两部分。
具体操作
-
合并:当前根当中选择一个附加权值小的作为根,剩下的向下合并。
-
分裂:根据阈值向下二分,将整个子树划入某个分裂树中。
-
插入:按照权值分裂,先与新节点合并再整体合并。
-
删除:按照权值分裂,再按照权值 \(-1\) 分裂,这样得到的中间子树的根就是要删除的节点。
-
查询排名:按照权值分裂,左子树大小 \(+1\) 即为答案。
-
查询排名对应权值:平衡树上二分。
-
查询前驱后继:按照权值分裂,再查询子树中的最小或最大值。
模板
点击查看代码
struct fhq_Treap{
int rt,tot;
int ch[maxn][2],val[maxn],pri[maxn],siz[maxn];
inline void push_up(int x){
siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1;
}
inline int new_node(int k){
++tot;
val[tot]=k,pri[tot]=rand(),siz[tot]=1;
return tot;
}
inline void build(){
new_node(-inf),new_node(inf);
rt=1,ch[1][1]=2;
push_up(rt);
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(pri[x]<pri[y]){
ch[x][1]=merge(ch[x][1],y);
push_up(x);
return x;
}
else{
ch[y][0]=merge(x,ch[y][0]);
push_up(y);
return y;
}
}
void split(int p,int k,int &x,int &y){
if(!p) x=0,y=0;
else{
if(val[p]<=k) x=p,split(ch[p][1],k,ch[x][1],y);
else y=p,split(ch[p][0],k,x,ch[y][0]);
push_up(p);
}
}
inline void insert(int k){
int x,y;
split(rt,k,x,y);
rt=merge(merge(x,new_node(k)),y);
}
inline void erase(int k){
int x,y,z;
split(rt,k,x,z),split(x,k-1,x,y);
y=merge(ch[y][0],ch[y][1]);
rt=merge(merge(x,y),z);
}
inline int rk(int k){
int x,y;
split(rt,k-1,x,y);
int res=siz[x]+1;
rt=merge(x,y);
return res;
}
int kth(int x,int k){
if(!x) return 0;
if(siz[ch[x][0]]>=k) return kth(ch[x][0],k);
else if(siz[ch[x][0]]+1>=k) return val[x];
else return kth(ch[x][1],k-(siz[ch[x][0]]+1));
}
inline int get_pre(int k){
int x,y;
split(rt,k-1,x,y);
int res=kth(x,siz[x]);
rt=merge(x,y);
return res;
}
inline int get_nxt(int k){
int x,y;
split(rt,k,x,y);
int res=kth(y,1);
rt=merge(x,y);
return res;
}
}T;
Splay
算法思想
使用一种高级操作维护平衡,每次伸展使得一个节点旋转到根从而维持平衡。
由于这种旋转到指定位置的特性,Splay 对序列操作非常友好。
具体操作
-
单次旋转:与 Treap 的左旋右旋相同,只不过写在了一起。
-
多次旋转:要考虑当前节点与父节点的儿子身份是否相同,即是否是同向儿子,如果是先旋转父亲再旋转儿子即可,反之在旋转父亲时,儿子会被转到另一个节点的子节点,因此要连续两次旋转当前节点。
-
插入:找到对应位置直接插入。
-
删除操作,找到前驱并旋转到根,找到后继并旋转到前驱的儿子,这样前驱右子树都是大于这个权值的,而右子树的根的后继,也就是说右子树的左儿子就是这个数。
-
找到一个权值所在位置并旋转到根:平衡树上二分。
-
查询排名:找到前驱后继
-
查询排名对应权值:平衡树上二分。
-
查询前驱后继:找到所在位置并旋转到根,再向下找到对应答案。
模板
点击查看代码
struct Splay{
int rt,tot;
int fa[maxn],ch[maxn][2];
int val[maxn],cnt[maxn],siz[maxn];
inline void push_up(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];}
inline bool chkson(int x){return x==ch[fa[x]][1];}
inline void rotate(int x){
int y=fa[x],z=fa[y];
bool chk=chkson(x);
if(z) ch[z][chkson(y)]=x;
fa[x]=z;
ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
ch[x][chk^1]=y,fa[y]=x;
push_up(y),push_up(x);
}
inline void splay(int x,int goal=0){
while(fa[x]!=goal){
int y=fa[x],z=fa[y];
if(z!=goal) rotate(chkson(x)==chkson(y)?y:x);
rotate(x);
}
if(!goal) rt=x;
}
inline void insert(int k){
int x=rt,f=0;
while(x&&val[x]!=k) f=x,x=ch[x][val[x]<k];
if(x) ++cnt[x];
else{
x=++tot;
fa[x]=f,ch[x][0]=ch[x][1]=0,val[x]=k,cnt[x]=1,siz[x]=1;
if(f) ch[f][val[f]<k]=x;
}
splay(x);
}
inline void find(int k){
int x=rt;
while(ch[x][val[x]<k]&&val[x]!=k) x=ch[x][val[x]<k];
splay(x);
}
inline int pre_nxt(int k,bool type){
find(k);
int x=rt;
if(val[x]<k&&!type) return x;
if(val[x]>k&&type) return x;
x=ch[x][type];
while(ch[x][type^1]) x=ch[x][type^1];
return x;
}
inline int rank(int k){
int x=pre_nxt(k,0);
splay(x);
return siz[ch[x][0]]+cnt[x]+1;
}
inline int kth(int k){
int x=rt;
while(1){
if(k<=siz[ch[x][0]]) x=ch[x][0];
else if(k<=siz[ch[x][0]]+cnt[x]) return val[x];
else k-=siz[ch[x][0]]+cnt[x],x=ch[x][1];
}
}
inline void erase(int k){
int pre=pre_nxt(k,0),nxt=pre_nxt(k,1);
splay(pre),splay(nxt,pre);
int x=ch[nxt][0];
if(cnt[x]>1){
--cnt[x];
splay(x);
}
else fa[x]=0,val[x]=0,siz[x]=0,cnt[x]=0,ch[nxt][0]=0;
}
}S;
维护序列
平衡树不止可以维护单调序列,也可以维护一个正常的序列。实际上在正常 Splay 中只有插入时才会考虑到单调的问题,而 Splay 的伸展会保证中序遍历不变。也就是说如果我们将插入位置改变,不会影响到 Splay 的维护。于是在维护一个序列时,可以直接将 \(k\) 的位置检索与 \(siz\) 相关,就能做到动态插入删除元素,甚至是区间翻转等等。
找到一个元素或区间的前驱后继,再将这个元素或区间孤立,是处理区间的关键操作。
点击查看代码
struct Splay{
int rt,tot;
int fa[maxn],ch[maxn][2];
int val[maxn],siz[maxn];
bool tag[maxn];
inline void push_up(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1;}
inline void push_down(int x){
if(tag[x]){
if(ch[x][0]) swap(ch[ch[x][0]][0],ch[ch[x][0]][1]),tag[ch[x][0]]^=1;
if(ch[x][1]) swap(ch[ch[x][1]][0],ch[ch[x][1]][1]),tag[ch[x][1]]^=1;
tag[x]=0;
}
}
inline bool chkson(int x){return x==ch[fa[x]][1];}
inline void build(){
rt=2,tot=2;
fa[1]=2,val[1]=0,siz[1]=1;
ch[2][0]=1,val[2]=0,siz[2]=2;
}
inline void rotate(int x){
int y=fa[x],z=fa[y];
push_down(z),push_down(y),push_down(x);
bool chk=chkson(x);
if(z) ch[z][chkson(y)]=x;
fa[x]=z;
ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
ch[x][chk^1]=y,fa[y]=x;
push_up(y),push_up(x);
}
inline void splay(int x,int goal=0){
while(fa[x]!=goal){
int y=fa[x],z=fa[y];
if(z!=goal) rotate(chkson(x)==chkson(y)?y:x);
rotate(x);
}
if(!goal) rt=x;
}
inline int kth(int k){
int x=rt;
while(1){
push_down(x);
if(k<=siz[ch[x][0]]) x=ch[x][0];
else if(k<=siz[ch[x][0]]+1) return x;
else k-=siz[ch[x][0]]+1,x=ch[x][1];
}
}
inline void insert(int x,int k){
int pre=kth(x-1),nxt=kth(x);
splay(pre),splay(nxt,pre);
++tot;
fa[tot]=nxt,val[tot]=k,siz[tot]=1;
ch[nxt][0]=tot;
splay(tot);
}
inline void reverse(int l,int r){
int pre=kth(l-1),nxt=kth(r+1);
splay(pre),splay(nxt,pre);
int x=ch[nxt][0];
swap(ch[x][0],ch[x][1]),tag[x]^=1;
splay(x);
}
inline void output(int x){
push_down(x);
if(ch[x][0]) output(ch[x][0]);
if(val[x]) printf("%d ",val[x]);
if(ch[x][1]) output(ch[x][1]);
}
}S;
可持久化
支持对历史版本访问修改的一类数据结构。
可持久化线段树
算法思想
分可持久化线段树与可持久化权值线段树(主席树)两种,主要思想差不多。
考虑对每个历史版本单独建树,空间复杂度 \(O(n^2)\),而实际上每次单点修改只会修改 \(\log\) 个点,区间修改只会修改 \(\log^2\) 的个点,可以考虑动态开点,没有被修改的点直接共用,就起到了优化空间复杂度的效果。
对于单点的复杂度是 \(O(n\log n)\),区间的复杂度是 \(O(n\log^2 n)\)。
这样操作的唯一问题是:多个历史版本的节点共用同一儿子会使得无法进行区间懒标记下传,于是使用标记永久化,即每次不下传标记并维护子树内信息,每次查询只查询经过的节点以及恰好覆盖节点子树内的贡献。
模板
可持久化线段树
点击查看代码
struct SegmentTree{
#define mid ((l+r)>>1)
int tot;
int ch[maxm][2],val[maxm];
inline void build(int &pos,int l,int r){
pos=++tot;
if(l==r) return val[pos]=a[l],void();
build(ch[pos][0],l,mid);
build(ch[pos][1],mid+1,r);
}
inline void new_node(int &pos){
++tot;
ch[tot][0]=ch[pos][0],ch[tot][1]=ch[pos][1],val[tot]=val[pos];
pos=tot;
}
void insert(int &pos,int l,int r,int p,int k){
new_node(pos);
if(l==r) return val[pos]=k,void();
if(p<=mid) insert(ch[pos][0],l,mid,p,k);
else insert(ch[pos][1],mid+1,r,p,k);
}
int query(int pos,int l,int r,int p){
if(l==r) return val[pos];
if(p<=mid) return query(ch[pos][0],l,mid,p);
else return query(ch[pos][1],mid+1,r,p);
}
#undef mid
}S;
可持久化权值线段树(主席树)
点击查看代码
struct SegmentTree{
#define mid ((l+r)>>1)
int tot;
int ch[maxm][2],siz[maxm];
inline void new_node(int &pos){
++tot;
ch[tot][0]=ch[pos][0],ch[tot][1]=ch[pos][1],siz[tot]=siz[pos];
pos=tot;
}
void insert(int &pos,int l,int r,int p){
new_node(pos);
++siz[pos];
if(l==r) return;
if(p<=mid) insert(ch[pos][0],l,mid,p);
else insert(ch[pos][1],mid+1,r,p);
}
int query(int lpos,int rpos,int l,int r,int k){
if(l==r) return l;
int lsiz=siz[ch[rpos][0]]-siz[ch[lpos][0]];
if(k<=lsiz) return query(ch[lpos][0],ch[rpos][0],l,mid,k);
else return query(ch[lpos][1],ch[rpos][1],mid+1,r,k-lsiz);
}
#undef mid
}S;
可持久化平衡树
算法思想
Treap 与 Splay 旋转对可持久化非常不友好。
可持久化使用的是无旋的 fhq-Treap。
树的形态发生改变是在合并分裂时产生的,于是在这两个操作中新建节点。
模板
点击查看代码
struct fhq_Treap{
int tot;
int ch[maxm][2],val[maxm],pri[maxm],siz[maxm];
inline void push_up(int x){
siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1;
}
inline int new_node(int k){
++tot;
val[tot]=k,pri[tot]=rand(),siz[tot]=1;
return tot;
}
inline int cpy_node(int &p){
++tot;
ch[tot][0]=ch[p][0],ch[tot][1]=ch[p][1];
val[tot]=val[p],pri[tot]=pri[p],siz[tot]=siz[p];
return tot;
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(pri[x]<pri[y]){
int z=cpy_node(x);
ch[z][1]=merge(ch[z][1],y);
push_up(z);
return z;
}
else{
int z=cpy_node(y);
ch[z][0]=merge(x,ch[z][0]);
push_up(z);
return z;
}
}
void split(int p,int k,int &x,int &y){
if(!p) x=0,y=0;
else{
if(val[p]<=k){
x=cpy_node(p);
split(ch[p][1],k,ch[x][1],y);
push_up(x);
}
else{
y=cpy_node(p);
split(ch[p][0],k,x,ch[y][0]);
push_up(y);
}
}
}
inline void insert(int &p,int k){
int x,y;
split(p,k,x,y);
p=merge(merge(x,new_node(k)),y);
}
inline void erase(int &p,int k){
int x,y,z;
split(p,k,x,z),split(x,k-1,x,y);
y=merge(ch[y][0],ch[y][1]);
p=merge(merge(x,y),z);
}
inline int rk(int &p,int k){
int x,y;
split(p,k-1,x,y);
int res=siz[x]+1;
p=merge(x,y);
return res;
}
int kth(int p,int k){
if(!p) return 0;
if(siz[ch[p][0]]>=k) return kth(ch[p][0],k);
else if(siz[ch[p][0]]+1>=k) return val[p];
else return kth(ch[p][1],k-(siz[ch[p][0]]+1));
}
inline int get_pre(int &p,int k){
int x,y;
split(p,k-1,x,y);
int res=kth(x,siz[x]);
p=merge(x,y);
return res;
}
inline int get_nxt(int &p,int k){
int x,y;
split(p,k,x,y);
int res=kth(y,1);
p=merge(x,y);
return res;
}
}T;
可持久化字典树
算法思想
以 01-Trie 为主。
也是一个动态建树的做法,没有修改的节点共用,可以查询一个数在某个区间内最大异或和。
模板
点击查看代码
struct Trie{
int tot;
int ch[maxm][2],siz[maxm];
inline void new_node(int &pos){
++tot;
ch[tot][0]=ch[pos][0],ch[tot][1]=ch[pos][1],siz[tot]=siz[pos];
pos=tot;
}
void insert(int &pos,int k,int d){
new_node(pos);
++siz[pos];
if(!d) return;
bool chk=k&(1<<d-1);
insert(ch[pos][chk],k,d-1);
}
int query(int lpos,int rpos,int k,int d){
if(!d) return 0;
bool chk=k&(1<<d-1);
if(siz[ch[rpos][!chk]]-siz[ch[lpos][!chk]]) return (1<<d-1)+query(ch[lpos][!chk],ch[rpos][!chk],k,d-1);
else return query(ch[lpos][chk],ch[rpos][chk],k,d-1);
}
}T;
树套树
可以处理比原有问题限制更多的问题,例如将全局改为区间,静态改为动态。
线段树套平衡树(二逼平衡树)
在原操作的基础上加上了区间的限制。
算法思想
考虑把线段树上每个区间开一棵平衡树,维护当前区间内所有信息。
修改操作实际是删除以及插入,在对应位置所在的每个区间都进行这样操作即可。
查询排名、前驱后继都是可以合并的,查询排名即在每个区间的排名之和,前驱即每个区间的前驱中最大的,后继即每个区间的后继中最小的。
以上四种操作在单棵平衡树中都是 \(O(\log n)\) 的,于是修改一次是 \(O(\log^2 n)\) 的。
查询排名为 \(k\) 的数,这一信息无法在多个区间中合并,二分答案即可,于是单次复杂度是 \(O(\log^2 n\log V)\)。
模板
点击查看代码
int n,m;
int a[maxn];
struct Splay{
int tot;
int fa[maxm],ch[maxm][2],val[maxm],cnt[maxm],siz[maxm];
inline void maintain(int x){siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+cnt[x];}
inline bool pdson(int x){return x==ch[fa[x]][1];}
inline int new_node(int k){
++tot;
ch[tot][0]=ch[tot][1]=0,val[tot]=k,cnt[tot]=siz[tot]=1;
return tot;
}
inline void rotate(int x){
int y=fa[x],z=fa[y];
bool chk=pdson(x);
ch[z][pdson(y)]=x,fa[x]=z;
ch[y][chk]=ch[x][chk^1],fa[ch[x][chk^1]]=y;
ch[x][chk^1]=y,fa[y]=x;
maintain(y),maintain(x);
}
inline void splay(int &rt,int x,int goal=0){
while(fa[x]!=goal){
int y=fa[x],z=fa[y];
if(z!=goal) rotate(pdson(x)==pdson(y)?y:x);
rotate(x);
}
if(!goal) rt=x;
}
inline void insert(int &rt,int k){
int x=rt,y=0;
while(x&&val[x]!=k) y=x,x=ch[x][k>val[x]];
if(x) ++cnt[x];
else{
x=new_node(k);
if(y) fa[x]=y,ch[y][k>val[y]]=x;
}
splay(rt,x);
}
inline void find(int &rt,int k){
int x=rt;
while(ch[x][k>val[x]]&&val[x]!=k) x=ch[x][k>val[x]];
splay(rt,x);
}
inline int rk(int &rt,int k){
find(rt,k);
if(k>val[rt]) return siz[ch[rt][0]]+cnt[rt]-1;
else return siz[ch[rt][0]]-1;
}
inline int kth(int rt,int k){
int x=rt;
while(1){
if(siz[ch[x][0]]>=k) x=ch[x][0];
else if(siz[ch[x][0]]+cnt[x]>=k) return val[x];
else{
k-=(siz[ch[x][0]]+cnt[x]);
x=ch[x][1];
}
}
}
inline int pre_nxt(int &rt,int k,bool pd){
find(rt,k);
int x=rt;
if(val[x]<k&&!pd) return x;
if(val[x]>k&&pd) return x;
x=ch[x][pd];
while(ch[x][pd^1]) x=ch[x][pd^1];
return x;
}
inline void erase(int &rt,int k){
int pre=pre_nxt(rt,k,0),nxt=pre_nxt(rt,k,1);
splay(rt,pre),splay(rt,nxt,pre);
int x=ch[nxt][0];
if(cnt[x]>1){
--cnt[x];
splay(rt,x);
}
else ch[nxt][0]=0;
}
}T;
struct SegmentTree{
#define mid ((l+r)>>1)
#define lson pos<<1,l,mid
#define rson pos<<1|1,mid+1,r
int rt[maxn<<2];
void build(int pos,int l,int r){
T.insert(rt[pos],-inf),T.insert(rt[pos],inf);
if(l==r) return;
build(lson),build(rson);
}
inline void insert(int pos,int l,int r,int p){
T.insert(rt[pos],a[p]);
if(l==r) return;
if(p<=mid) insert(lson,p);
else insert(rson,p);
}
inline int rk(int pos,int l,int r,int pl,int pr,int k){
if(pl<=l&&r<=pr) return T.rk(rt[pos],k);
int res=0;
if(pl<=mid) res+=rk(lson,pl,pr,k);
if(pr>mid) res+=rk(rson,pl,pr,k);
return res;
}
inline void update(int pos,int l,int r,int p,int k){
T.erase(rt[pos],a[p]);
T.insert(rt[pos],k);
if(l==r) return;
if(p<=mid) update(lson,p,k);
else update(rson,p,k);
}
inline int get_pre(int pos,int l,int r,int pl,int pr,int k){
if(pl<=l&&r<=pr) return T.val[T.pre_nxt(rt[pos],k,0)];
int res=-inf;
if(pl<=mid) res=max(res,get_pre(lson,pl,pr,k));
if(pr>mid) res=max(res,get_pre(rson,pl,pr,k));
return res;
}
inline int get_nxt(int pos,int l,int r,int pl,int pr,int k){
if(pl<=l&&r<=pr) return T.val[T.pre_nxt(rt[pos],k,1)];
int res=inf;
if(pl<=mid) res=min(res,get_nxt(lson,pl,pr,k));
if(pr>mid) res=min(res,get_nxt(rson,pl,pr,k));
return res;
}
#undef mid
#undef lson
#undef rson
}S;
int main(){
n=read(),m=read();
S.build(1,1,n);
for(int i=1;i<=n;++i){
a[i]=read();
S.insert(1,1,n,i);
}
while(m--){
int opt=read();
if(opt==1){
int l=read(),r=read(),k=read();
printf("%d\n",S.rk(1,1,n,l,r,k)+1);
}
else if(opt==2){
int l=read(),r=read(),k=read();
int L=-1e8,R=1e8,ans;
while(L<=R){
int mid=(L+R)>>1;
if(S.rk(1,1,n,l,r,mid+1)+1>k) ans=mid,R=mid-1;
else L=mid+1;
}
printf("%d\n",ans);
}
else if(opt==3){
int p=read(),k=read();
S.update(1,1,n,p,k);
a[p]=k;
}
else if(opt==4){
int l=read(),r=read(),k=read();
printf("%d\n",S.get_pre(1,1,n,l,r,k));
}
else{
int l=read(),r=read(),k=read();
printf("%d\n",S.get_nxt(1,1,n,l,r,k));
}
}
return 0;
}
树状数组套主席树
求动态区间 \(k\) 小值,用到主席树的前缀和差分思想。
算法思想
如果正常用暴力修改,那么修改复杂度是 \(O(n\log n)\) 而查询是 \(O(\log n)\) 的,二者不够平衡。
如果不维护前缀的权值线段树,而是对每个位置单独维护,修改是 \(O(\log n)\) 而查询变成 \(O(n\log n)\),也就是单点修改和区间求和,可以使用树状数组来平衡这一过程。
常规树状数组 \(x\) 维护的是 \([x-\mathrm{lowbit}(x)+1,x]\) 的和,这里就改成维护这个区间内的权值线段树,于是对于 \([1,x]\) 的权值线段树,查询和修改分别变成了对 \(O(\log n)\) 个区间对应的权值线段树操作,信息可以合并起来,于是复杂度就做到 \(O(n\log^2 n)\)。
模板
点击查看代码
int n,m;
int a[maxn],rt[maxn];
struct Question{
int l,r,x,k;
}q[maxn];
vector<int> V;
int lpos[maxn],rpos[maxn];
struct SegmentTree{
#define mid ((l+r)>>1)
int tot;
int ch[maxm][2],siz[maxm];
inline void new_node(int &pos){
++tot;
ch[tot][0]=ch[tot][1]=0,siz[tot]=0;
pos=tot;
}
void insert(int &pos,int l,int r,int p,int k){
if(!pos) new_node(pos);
siz[pos]+=k;
if(l==r) return;
if(p<=mid) insert(ch[pos][0],l,mid,p,k);
else insert(ch[pos][1],mid+1,r,p,k);
}
int query(int l,int r,int k){
if(l==r) return l;
int lsiz=0;
for(int i=1;i<=lpos[0];++i) lsiz-=siz[ch[lpos[i]][0]];
for(int i=1;i<=rpos[0];++i) lsiz+=siz[ch[rpos[i]][0]];
if(lsiz>=k){
for(int i=1;i<=lpos[0];++i) lpos[i]=ch[lpos[i]][0];
for(int i=1;i<=rpos[0];++i) rpos[i]=ch[rpos[i]][0];
return query(l,mid,k);
}
else{
for(int i=1;i<=lpos[0];++i) lpos[i]=ch[lpos[i]][1];
for(int i=1;i<=rpos[0];++i) rpos[i]=ch[rpos[i]][1];
return query(mid+1,r,k-lsiz);
}
}
#undef mid
}S;
struct BinaryIndexedTree{
#define lowbit(x) (x&-x)
inline void update(int x,int k){
int id=lower_bound(V.begin(),V.end(),a[x])-V.begin()+1;
while(x<=n){
S.insert(rt[x],1,V.size(),id,k);
x+=lowbit(x);
}
}
inline int query(int l,int r,int k){
lpos[0]=rpos[0]=0;
while(r){
rpos[++rpos[0]]=rt[r];
r-=lowbit(r);
}
--l;
while(l){
lpos[++lpos[0]]=rt[l];
l-=lowbit(l);
}
return S.query(1,V.size(),k);
}
#undef lowbit
}B;
int main(){
n=read(),m=read();
for(int i=1;i<=n;++i){
a[i]=read();
V.push_back(a[i]);
}
for(int i=1;i<=m;++i){
char opt[4];
scanf("%s",opt);
if(opt[0]=='Q'){
q[i].l=read(),q[i].r=read(),q[i].x=-1,q[i].k=read();
}
else{
q[i].x=read(),q[i].k=read();
V.push_back(q[i].k);
}
}
sort(V.begin(),V.end());
V.erase(unique(V.begin(),V.end()),V.end());
for(int i=1;i<=n;++i) B.update(i,1);
for(int i=1;i<=m;++i){
if(q[i].x!=-1){
B.update(q[i].x,-1);
a[q[i].x]=q[i].k;
B.update(q[i].x,1);
}
else printf("%d\n",V[B.query(q[i].l,q[i].r,q[i].k)-1]);
}
return 0;
}
参考资料
- OI Wiki