1. 非旋 Treap(FHQ Treap)

1.1. 算法简介

FHQ Treap 的功能非常强大。它涵盖了 Treap 几乎所有的功能 所以我非常后悔学了 Treap,浪费时间。

FHQ 的核心思想在于分裂和合并

我们所需要的存储的信息:左儿子 \(ls\),右儿子 \(rs\),权值 \(val\),子树大小 \(sz\) 以及 Treap 所特有的随机优先级 \(rd\)。一般情况下,FHQ Treap 将权值相同的节点看成多个节点,故不需要记录节点所存储的数的个数 \(cnt\)

1.1.1. 更新答案

基础中的基础。

void push(int x){sz[x]=sz[ls[x]]+sz[rs[x]]+1;}

1.1.2. 分裂

FHQ Treap:我裂开来。

有两种分裂方式,一种是按权值 \(val\) 分裂,另一种是按大小 \(sz\) 分裂。

  • 按权值分裂:具体地,给定 \(v\),我们要将原来的平衡树 \(T\) 分裂成两个平衡树 \(T_x\) 与 \(T_y\),满足对于任意 \(i\in T_x,\ j\in T_y\),都有 \(val_i\leq v<val_j\)。

    假设当前节点为 \(p\):如果 \(val_p\leq v\),那么 \(p\) 应属于 \(T_x\),而 \(p\) 的左儿子 \(ls_p\) (lsp) 显然也属于 \(T_x\),故只需要向右递归 \(rs_p\) 即可;反之则有 \(val_p>v\),那么 \(p\) 应属于 \(T_y\),而 \(p\) 的右儿子 \(rs_p\) 显然也属于 \(T_y\),故只需要向右递归 \(ls_p\) 即可。

    递归边界:\(p=0\)。

    代码实现如下:

void spl(int p,int v,int &x,int &y){
if(!p)return x=y=0,void();
if(val[p]<=v)spl(rs[p],v,rs[x=p],y);
else spl(ls[p],v,x,ls[y=p]);
push(p);
}
  • 按大小分裂:同样的,给定 \(v\),对于当前节点 \(p\),如果 \(v\leq sz_{ls_p}\),那么 \(p\) 和整个 \(rs_p\) 的子树都应被分到 \(T_y\),向 \(ls_p\) 递归即可。反之亦然。

    代码实现如下:

void spl(int p,int v,int &x,int &y){
if(!p)return x=y=0,void();
if(v<=sz[ls[p]])spl(ls[p],v,x,ls[y=p]);
else spl(rs[p],v-sz[ls[p]]-1,rs[x=p],y);
push(x);
}

1.1.3. 合并

像极了线段树分裂和合并。

首先,合并的两棵树 \(T_x\) 与 \(T_y\) 必须要满足对于任意 \(i\in T_x,\ j\in T_y\),都有 \(val_i<val_j\)。假设当前我们要合并 \(x,y\) 两个节点:

  • 如果 \(rd_x>rd_y\):那么 \(x\) 的优先级更高。因此 \(x\) 应当是 \(y\) 的父亲。而根据二叉平衡树的性质,因为 \(val_x<val_y\),所以 \(y\) 是 \(x\) 的右儿子。故直接调用 \(rs_x\gets \mathrm{merge}(rs_x,y)\) 即可。
  • 反之亦然,直接调用 \(ls_y\gets \mathrm{merge}(x,ls_y)\)。
  • 递归边界:\(x,y\) 中至少有一个为空。此时我们使用类似线段树合并的 Trick,直接返回 \(x\ \mathrm{or}\ y\) 即可。

代码实现:

int mer(int x,int y){
if(!x||!y)return x|y;
if(rd[x]<rd[y])return ls[y]=mer(x,ls[y]),push(y),y;
return rs[x]=mer(rs[x],y),push(x),x;
}

FHQ Treap 是真 tmd 好写!!!

1.1.4. 新建节点

int nd(int v){
int x=++node;
val[x]=v,rd[x]=rand(),sz[x]=1;
return x;
}

1.2. 其它功能

1.2.1. 插入

插入一个数 \(v\):只需要用 \(v-1\) 按照权值分裂成两棵树 \(T_x,T_y\),再 \(root=\mathrm{merge}(\mathrm{merge}(x,\mathrm{newnode}(v)),y)\) 即可。

void ins(int v){
int x=0,y=0;
spl(R,v-1,x,y),R=mer(mer(x,nd(v)),y);
}

1.2.2. 删除

删除一个数 \(v\):先用 \(v\) 按照权值分裂成两棵树 \(T_x,T_z\),再用 \(v-1\) 按照权值将 \(T_x\) 分裂成两棵树 \(T_x,T_y\)。此时 \(T_y\) 的所有节点的权值就都是 \(v\)。但是我们只能删一个,因此可以将 \(y\) 的左右节点合并起来,这样 \(T_y\) 的节点个数就少了一个。最后再合并回来即可。

void del(int v){
int x=0,y=0,z=0;
spl(R,v,x,z),spl(x,v-1,x,y);
R=mer(mer(x,y=mer(ls[y],rs[y])),z);
}

1.2.3. 第 k 大

查找第 \(k\) 大的数:假设当前遍历到节点 \(p\)。若 \(k\leq sz_{ls_p}\) 则向左儿子递归;若 \(k=sz_{ls_p}+1\) 则直接返回 \(val_p\);否则向右儿子递归。

int kth(int k){
int p=R;
while(1){
if(k<=sz[ls[p]])p=ls[p];
else if(k==sz[ls[p]])return val[p];
else k-=sz[ls[p]]+1,p=rs[p];
}
}

1.2.4. 前驱 / 后继

查找 \(v\) 的前驱:找一种方法是直接在 \(T\) 里面找,另一种是用 \(v-1\) 按照权值分裂成 \(T_x,T_y\),再找 \(T_x\) 的最大值即可(即 \(T_x\) 的第 \(sz_x\) 大值)。后继同理。

int pre(int v){
int p=R,ans=0;
while(1){
if(!p)return ans;
else if(v<=val[p])p=ls[p];
else ans=val[p],p=rs[p];
}
}
int suc(int v){
int p=R,ans=0;
while(1){
if(!p)return ans;
else if(v>=val[p])p=rs[p];
else ans=val[p],p=ls[p];
}
}

1.2.5. 排名

查询 \(v\) 的排名:用 \(v-1\) 按照权值分裂成 \(T_x,T_y\),那么答案即为 \(sz_x+1\)。最后合并。

int rnk(int v){
int x=0,y=0,ans;
spl(R,v-1,x,y),ans=sz[x]+1;
return R=mer(x,y),ans;
}

1.2.6. 区间翻转

平衡树维护区间,那么每个节点存储的就是对应位置的值,而它的中序遍历就是整个区间。

只需要将要翻转的区间分裂出来,打上标记再合并即可。注意标记下推交换左右儿子,具体见例题 II。

1.3. 例题

I. P3369 【模板】普通平衡树

板子题。

#include <bits/stdc++.h>
using namespace std; const int N=1e5+5;
const int inf=1e9+7; int R,node,ls[N],rs[N],val[N],rd[N],sz[N];
void push(int x){sz[x]=sz[ls[x]]+sz[rs[x]]+1;} void spl(int p,int v,int &x,int &y){
if(!p)return x=y=0,void();
if(val[p]<=v)spl(rs[p],v,rs[x=p],y);
else spl(ls[p],v,x,ls[y=p]);
push(p);
}
int mer(int x,int y){
if(!x||!y)return x|y;
if(rd[x]<rd[y])return ls[y]=mer(x,ls[y]),push(y),y;
return rs[x]=mer(rs[x],y),push(x),x;
}
int nd(int v){
int x=++node;
val[x]=v,rd[x]=rand(),sz[x]=1;
return x;
}
void ins(int v){
int x=0,y=0;
spl(R,v-1,x,y),R=mer(mer(x,nd(v)),y);
}
void del(int v){
int x=0,y=0,z=0;
spl(R,v,x,z),spl(x,v-1,x,y);
R=mer(mer(x,y=mer(ls[y],rs[y])),z);
}
int kth(int k){
int p=R;
while(1){
if(k<=sz[ls[p]])p=ls[p];
else if(k==sz[ls[p]]+1)return val[p];
else k-=sz[ls[p]]+1,p=rs[p];
}
}
int pre(int v){
int p=R,ans=0;
while(1){
if(!p)return ans;
else if(v<=val[p])p=ls[p];
else ans=val[p],p=rs[p];
}
}
int suc(int v){
int p=R,ans=0;
while(1){
if(!p)return ans;
else if(v>=val[p])p=rs[p];
else ans=val[p],p=ls[p];
}
}
int rnk(int v){
int x=0,y=0,ans=0;
spl(R,v-1,x,y),ans=sz[x]+1;
return R=mer(x,y),ans;
} int n,m;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
int op,x; cin>>op>>x;
if(op==1)ins(x);
if(op==2)del(x);
if(op==3)cout<<rnk(x)<<endl;
if(op==4)cout<<kth(x)<<endl;
if(op==5)cout<<pre(x)<<endl;
if(op==6)cout<<suc(x)<<endl;
}
return 0;
}

II. P3391 【模板】文艺平衡树

这里给出代码,是真的短。

#include <bits/stdc++.h>
using namespace std; const int N=1e5+5; int R,node,ls[N],rs[N],tg[N],val[N],sz[N],rd[N]; void push(int x){sz[x]=sz[ls[x]]+sz[rs[x]]+1;}
void down(int x){if(tg[x])tg[ls[x]]^=1,tg[rs[x]]^=1,swap(ls[x],rs[x]),tg[x]=0;}
int nd(int v){int x=++node; return rd[x]=rand(),val[x]=v,sz[x]=1,x;} void spl(int p,int v,int &x,int &y){
if(!p)return x=y=0,void();
down(p);
if(sz[ls[p]]>=v)spl(ls[p],v,x,ls[y=p]);
else spl(rs[p],v-sz[ls[p]]-1,rs[x=p],y);
push(p);
}
int mer(int x,int y){
if(!x||!y)return x|y;
down(x),down(y);
if(rd[x]>rd[y])return rs[x]=mer(rs[x],y),push(x),x;
return ls[y]=mer(x,ls[y]),push(y),y;
} void modify(int l,int r){int x,y,z; spl(R,r,x,z),spl(x,l-1,x,y),tg[y]^=1,R=mer(x,mer(y,z));}
void pr(int x){if(!x)return; down(x),pr(ls[x]),cout<<val[x]<<" ",pr(rs[x]);} int main(){
int n,m; cin>>n>>m;
for(int i=1;i<=n;i++)R=mer(R,nd(i));
for(int i=1,l,r;i<=m;i++)cin>>l>>r,modify(l,r);
pr(R);
return 0;
}

III. P2042 [NOI2005] 维护数列

平衡树维护序列的练手题,不过 FHQ Treap 的常数有点大,下面这份代码只有开了 O2 才能过。

#include <bits/stdc++.h>
using namespace std; #define gc getchar() inline int read(){
int x=0,sgn=0; char s=gc;
while(!isdigit(s))sgn|=s=='-',s=gc;
while(isdigit(s))x=x*10+s-'0',s=gc;
return sgn?-x:x;
} const int N=5e5+5;
const int inf=1e9+7; int R,node,sz[N],ls[N],rs[N],val[N],rd[N];
int top,stc[N],tg[N],mod[N]; struct data{
int sum,pre,suc,mx;
void modify(int x,int sz){sum=x*sz,pre=suc=mx=x<0?x:sum;}
friend data operator + (data a,data b){
data c;
c.sum=a.sum+b.sum;
c.pre=max(a.pre,a.sum+b.pre);
c.suc=max(b.suc,b.sum+a.suc);
c.mx=max(max(a.mx,b.mx),a.suc+b.pre);
return c;
}
}d[N],emp; void push(int x){
sz[x]=sz[ls[x]]+sz[rs[x]]+1;
d[x]=d[ls[x]]+(data){val[x],val[x],val[x],val[x]}+d[rs[x]];
}
void down(int x){
if(tg[x]){
tg[ls[x]]^=1,tg[rs[x]]^=1;
swap(d[ls[x]].pre,d[ls[x]].suc);
swap(d[rs[x]].pre,d[rs[x]].suc);
swap(ls[x],rs[x]),tg[x]=0;
}
if(mod[x]!=inf){
val[ls[x]]=val[rs[x]]=mod[ls[x]]=mod[rs[x]]=mod[x];
d[ls[x]].modify(mod[x],sz[ls[x]]);
d[rs[x]].modify(mod[x],sz[rs[x]]);
mod[x]=inf;
}
}
int nd(int v){
int x=top?stc[top--]:++node;
sz[x]=1,val[x]=v,rd[x]=rand();
d[x]={v,v,v,v},mod[x]=inf;
return x;
}
void dd(int x){
sz[x]=ls[x]=rs[x]=val[x]=rd[x]=tg[x]=0;
d[x]=emp,stc[++top]=x;
} void spl(int p,int v,int &x,int &y){
if(!p)return x=y=0,void();
down(p);
if(sz[ls[p]]>=v)spl(ls[p],v,x,ls[y=p]);
else spl(rs[p],v-sz[ls[p]]-1,rs[x=p],y);
push(p);
}
int mer(int x,int y){
if(!x||!y)return x|y;
down(x),down(y);
if(rd[x]>rd[y])return rs[x]=mer(rs[x],y),push(x),x;
return ls[y]=mer(x,ls[y]),push(y),y;
}
void empty(int x){
if(!x)return;
empty(ls[x]),empty(rs[x]),dd(x);
} void ins(){
int p=read(),t=read(),x=0,y=0,rt=0;
for(int i=1;i<=t;i++)rt=mer(rt,nd(read()));
spl(R,p,x,y),R=mer(mer(x,rt),y);
}
void spl(int &x,int &y,int &z){
int p=read(),t=read(); p--;
spl(R,p+t,x,z),spl(x,p,x,y);
}
void del(){int x,y,z; spl(x,y,z),empty(y),R=mer(x,z);}
void mak(){int x,y,z; spl(x,y,z),mod[y]=val[y]=read(),d[y].modify(val[y],sz[y]); R=mer(mer(x,y),z);}
void rev(){int x,y,z; spl(x,y,z),tg[y]^=1,R=mer(mer(x,y),z);}
void get(){int x,y,z; spl(x,y,z),printf("%d\n",d[y].sum),R=mer(mer(x,y),z);} int main(){
d[0]=emp={0,-inf,-inf,-inf};
int n,m; cin>>n>>m;
for(int i=1;i<=n;i++)R=mer(R,nd(read()));
for(int i=1;i<=m;i++){
string s; cin>>s;
if(s[0]=='I')ins();
else if(s[0]=='D')del();
else if(s[0]=='R')rev();
else if(s[0]=='G')get();
else if(s[2]=='K')mak();
else printf("%d\n",d[R].mx);
}
return 0;
}

IV. P4008 [NOI2003] 文本编辑器

对顶堆开 O2 可以过!!O2 永远滴神!!

#include <bits/stdc++.h>
using namespace std; #define gc getchar() inline int read(){
int x=0,sgn=0; char s=gc;
while(!isdigit(s))sgn|=s=='-',s=gc;
while(isdigit(s))x=x*10+s-'0',s=gc;
return sgn?-x:x;
} const int N=3e6+5; int t,at,bt;
char a[N],b[N],s[N];
string op; int main(){
cin>>t;
for(int i=1,x;i<=t;i++){
cin>>op;
if(op.size()<3)cin>>op;
if(op[0]=='M'){
x=read();
while(at>x)b[++bt]=a[at--];
while(at<x)a[++at]=b[bt--];
}
if(op[0]=='I'){
int len=0; cin>>x;
while(len<x){
s[++len]=gc;
if(s[len]<32||s[len]>126)len--;
}
for(int j=x;j;j--)b[++bt]=s[j];
}
if(op[0]=='D'){cin>>x; while(x--)bt--;}
if(op[0]=='G'){
int pos=bt; cin>>x;
while(x--)putchar(b[pos--]);
putchar('\n');
}
if(op[0]=='P')b[++bt]=a[at--];
if(op[0]=='N')a[++at]=b[bt--];
}
return 0;
}

接下来是 FHQ 版本:

#include <bits/stdc++.h>
using namespace std; #define gc getchar() inline int read(){
int x=0,sgn=0; char s=gc;
while(!isdigit(s))sgn|=s=='-',s=gc;
while(isdigit(s))x=x*10+s-'0',s=gc;
return sgn?-x:x;
} const int N=2e6+5; int R,K,node,ls[N],rs[N],sz[N],rd[N];
char val[N]; void push(int x){sz[x]=sz[ls[x]]+sz[rs[x]]+1;}
int nd(char v){
int x=++node;
rd[x]=rand(),sz[x]=1,val[x]=v;
return x;
} void spl(int p,int l,int &x,int &y){
if(!p)return x=y=0,void();
if(sz[ls[p]]>=l)spl(ls[p],l,x,ls[y=p]);
else spl(rs[p],l-sz[ls[p]]-1,rs[x=p],y);
push(p);
}
int mer(int x,int y){
if(!x||!y)return x|y;
if(rd[x]>rd[y])return rs[x]=mer(rs[x],y),push(x),x;
return ls[y]=mer(x,ls[y]),push(y),y;
}
void print(int x){
if(!x)return;
print(ls[x]),putchar(val[x]),print(rs[x]);
} int t;
string op;
int main(){
cin>>t;
for(int i=1,n;i<=t;i++){
cin>>op;
if(op[0]=='M')K=read();
else if(op[0]=='I'){
n=read();
int x,y; spl(R,K,x,y);
for(int i=1;i<=n;i++){
char s=gc;
while(s<32||s>126)s=gc;
x=mer(x,nd(s));
} R=mer(x,y);
} else if(op[0]=='D'){
n=read();
int x,y,z;
spl(R,K+n,x,z),spl(x,K,x,y),R=mer(x,z);
} else if(op[0]=='G'){
n=read();
int x,y,z;
spl(R,K+n,x,z),spl(x,K,x,y),print(y);
R=mer(mer(x,y),z),putchar('\n');
} else if(op[0]=='P')K--;
else K++;
}
return 0;
}

V. P4146 序列终结者

可以说是板子题了,注意初始化编号为 \(0\) 的节点。

#include <bits/stdc++.h>
using namespace std; #define ll long long const int N=1e5+5; ll R,node,val[N],ls[N],rs[N],sz[N],rd[N],tg[N],add[N],mx[N];
void push(int x){sz[x]=sz[ls[x]]+sz[rs[x]]+1,mx[x]=max(mx[ls[x]],max(mx[rs[x]],val[x]));}
void down(int x){
if(tg[x])tg[ls[x]]^=1,tg[rs[x]]^=1,swap(ls[x],rs[x]),tg[x]=0;
if(add[x]){
add[ls[x]]+=add[x],add[rs[x]]+=add[x];
mx[ls[x]]+=add[x],mx[rs[x]]+=add[x];
val[ls[x]]+=add[x],val[rs[x]]+=add[x];
add[x]=0;
}
}
int nd(){int x=++node; sz[x]=1,rd[x]=rand(); return x;} void spl(int p,int k,ll &x,ll &y){
if(!p)return x=y=0,void();
down(p);
if(sz[ls[p]]>=k)spl(ls[p],k,x,ls[y=p]);
else spl(rs[p],k-sz[ls[p]]-1,rs[x=p],y);
push(p);
}
int mer(int x,int y){
if(!x||!y)return x|y;
down(x),down(y);
if(rd[x]>rd[y])return rs[x]=mer(rs[x],y),push(x),x;
return ls[y]=mer(x,ls[y]),push(y),y;
} int n,m;
int main(){
cin>>n>>m,mx[0]=-1e18;
for(int i=1;i<=n;i++)R=mer(R,nd());
for(int i=1;i<=m;i++){
int k,l,r,v; cin>>k>>l>>r;
if(k==1){
cin>>v;
ll x,y,z; spl(R,r,x,z),spl(x,l-1,x,y);
add[y]+=v,mx[y]+=v,val[y]+=v;
R=mer(mer(x,y),z);
}
if(k==2){
ll x,y,z; spl(R,r,x,z),spl(x,l-1,x,y);
tg[y]^=1,R=mer(mer(x,y),z);
}
if(k==3){
ll x,y,z; spl(R,r,x,z),spl(x,l-1,x,y);
printf("%lld\n",mx[y]),R=mer(mer(x,y),z);
}
}
return 0;
}

VI. P3960 [NOIP2017 提高组] 列队

来点不那么套路的平衡树题。

众所周知平衡树可以用来维护序列,那么最后一列删除和插入一个数就用 FHQ Treap 维护。对于每一行,在其末尾插入的数也用 FHQ Treap 维护。如果这一行删除的数不足以让当前位置被曾经到过最后一列的数所占据,那么可以动态开点线段树标记原来哪些数被取到了(对于前 \(m-1\) 列),那么现在就要取该行从左往右数第 \(y\) 个没有被取的数,这个可以线段树上二分。因此总时间复杂度为 \(\mathcal{O}((n+q)\log n)\)。

#include <bits/stdc++.h>
using namespace std; #define int long long const int N=3e5+5; struct FHQ{
int node,R[N],ls[N<<1],rs[N<<1],rd[N<<1],sz[N<<1],val[N<<1];
void push(int x){sz[x]=sz[ls[x]]+sz[rs[x]]+1;}
int nd(int v){int x=++node; return val[x]=v,rd[x]=rand(),sz[x]=1,x;}
void spl(int p,int k,int &x,int &y){
if(!p)return x=y=0,void();
if(sz[ls[p]]>=k)spl(ls[p],k,x,ls[y=p]);
else spl(rs[p],k-sz[ls[p]]-1,rs[x=p],y);
push(p);
}
int mer(int x,int y){
if(!x||!y)return x|y;
if(rd[x]>rd[y])return rs[x]=mer(rs[x],y),push(x),x;
return ls[y]=mer(x,ls[y]),push(y),y;
}
}tr; struct SEG{
int node,R[N],val[N<<5],ls[N<<5],rs[N<<5];
void ins(int l,int r,int p,int &x){
if(!x)x=++node; val[x]++;
if(l==r)return;
int m=l+r>>1;
if(p<=m)ins(l,m,p,ls[x]);
else ins(m+1,r,p,rs[x]);
}
int query(int l,int r,int k,int x){
if(l==r)return l;
int m=l+r>>1,v=(m-l+1)-val[ls[x]];
if(k<=v)return query(l,m,k,ls[x]);
return query(m+1,r,k-v,rs[x]);
}
}sg; int n,m,q;
signed main(){
cin>>n>>m>>q;
for(int i=1,j=m;i<=n;i++,j+=m)tr.R[0]=tr.mer(tr.R[0],tr.nd(j));
for(int i=1;i<=q;i++){
int x,y,res,a,b,c; cin>>x>>y;
if(y<m){
int sz=tr.sz[tr.R[x]];
if(y<m-sz){
int cnt=sg.query(1,m-1,y,sg.R[x]);
cout<<(res=(x-1)*m+cnt)<<endl;
sg.ins(1,m-1,cnt,sg.R[x]);
}
else{
tr.spl(tr.R[x],y-(m-sz),a,b),tr.spl(b,1,b,c);
cout<<(res=tr.val[b])<<endl,tr.R[x]=tr.mer(a,c);
}
}
tr.spl(tr.R[0],x-1,a,b),tr.spl(b,1,b,c);
if(y<m)tr.R[x]=tr.mer(tr.R[x],b);
else cout<<(res=tr.val[b])<<endl;
tr.R[0]=tr.mer(a,tr.mer(c,tr.nd(res)));
}
return 0;
}

感觉可以只维护若干个队列 + 线段树做,然后线段树二分转为 BIT 上二分,常数就可以大大减小了。

*VII. P7739 [NOI2021] 密码箱

好了,现在你已经学会了 FHQ Treap 的基本操作,快来试试这道题吧!

别看这道题被喷得很惨,其中的内涵还是值得钻研的。

首先,重要的一点是分数不会被约分:\(a_i+\dfrac{1}{\frac{x}{y}}=\dfrac{a_ix+y}{x}\),显然 \(\gcd(xa_i+y,x)=\gcd(x,y)\)。因此,我们可以:

  • Trick 1:用矩阵维护线性变换

    考察一次变换过后,分数究竟如何变化。分子:\(x\to a_ix+y\);分母:\(y\to x\)。不难发现这是一个二维向量的线性变换:\(\begin{bmatrix}x&y\end{bmatrix}\to\begin{bmatrix}a_ix+y&x\end{bmatrix}\)。不难发现我们只需要左乘上矩阵 \(\begin{bmatrix}a_i&1\\1&0\end{bmatrix}\) 即可。

    • 对于一个 W 操作,考虑 \(\begin{bmatrix}1&k\\0&1\end{bmatrix}\times \begin{bmatrix}a_i&1\\1&0\end{bmatrix}=\begin{bmatrix}a_i+k&1\\1&0\end{bmatrix}\),所以相当于在序列最右边加入添加矩阵 \(\begin{bmatrix}1&1\\0&1\end{bmatrix}\)。
    • 对于一个 E 操作,因为当最后一项为 \(1\) 时,给倒数第二项加 \(1\) 与先给数列的最后一项减 \(1\),接着在数列末端加两个 \(1\) 是等价的,所以可以直接考虑后者:\(\begin{bmatrix}1&1\\0&1\end{bmatrix}\times \begin{bmatrix}1&1\\0&1\end{bmatrix}\times \begin{bmatrix}1&-1\\0&1\end{bmatrix}=\begin{bmatrix}2&-1\\1&0\end{bmatrix}\)​。

    接下来就可以用平衡树维护一整个序列了。不过对于操作 FLIPREVERSE 似乎不太好办。

  • Trick 2:对于没有交换律的元素的区间翻转,预处理出正序值和逆序值。

    一个非常巧妙且实用的技巧。后两种操作都可以这么办,这样就做完了。视 \(n,q\) 同阶,则时间复杂度为 \(\mathcal{O}(n\log n)\)。

#include <bits/stdc++.h>
using namespace std; typedef double db;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull; #define gc getchar()
#define pb push_back
#define mem(x,v,n) memset(x,v,sizeof(int)*n)
#define cpy(x,y,n) memcpy(x,y,sizeof(int)*n) const ld Pi=acos(-1);
const ll mod=998244353; inline int read(){
int x=0; char s=gc;
while(!isdigit(s))s=gc;
while(isdigit(s))x=x*10+s-'0',s=gc;
return x;
} const int N=2e5+5; struct Matrix{
ll a,b,c,d;
Matrix operator * (Matrix x){
Matrix y;
y.a=(a*x.a+b*x.c)%mod;
y.b=(a*x.b+b*x.d)%mod;
y.c=(c*x.a+d*x.c)%mod;
y.d=(c*x.b+d*x.d)%mod;
return y;
}
}E,W,B,tr[N][2],val[N][2],rev[N][2]; int type,R,node,rd[N],sz[N],ls[N],rs[N],rv[N],flp[N];
void push(int x){
// cout<<"push "<<x<<endl;
int l=ls[x],r=rs[x];
sz[x]=sz[l]+sz[r]+1;
val[x][0]=val[r][0]*tr[x][0]*val[l][0];
val[x][1]=val[r][1]*tr[x][1]*val[l][1];
rev[x][0]=rev[l][0]*tr[x][0]*rev[r][0];
rev[x][1]=rev[l][1]*tr[x][1]*rev[r][1];
}
int nd(char v){
int x=++node;
rd[x]=rand(),sz[x]=1;
if(v=='E')tr[x][0]=E,tr[x][1]=W;
else tr[x][0]=W,tr[x][1]=E;
val[x][0]=rev[x][0]=tr[x][0];
val[x][1]=rev[x][1]=tr[x][1];
return x;
} void reverse(int x){
swap(rev[x],val[x]);
swap(ls[x],rs[x]);
}
void flip(int x){
swap(val[x][0],val[x][1]);
swap(rev[x][0],rev[x][1]);
swap(tr[x][0],tr[x][1]);
} void down(int x){
if(rv[x]){
reverse(ls[x]),reverse(rs[x]);
rv[ls[x]]^=1,rv[rs[x]]^=1,rv[x]=0;
}
if(flp[x]){
flip(ls[x]),flip(rs[x]);
flp[ls[x]]^=1,flp[rs[x]]^=1,flp[x]=0;
}
} int mer(int x,int y){
if(!x||!y)return x|y;
down(x),down(y);
if(rd[x]>rd[y])return rs[x]=mer(rs[x],y),push(x),x;
return ls[y]=mer(x,ls[y]),push(y),y;
}
void spl(int p,int v,int &x,int &y){
if(!p)return x=y=0,void();
down(p);
if(v<=sz[ls[p]])spl(ls[p],v,x,ls[y=p]);
else spl(rs[p],v-sz[ls[p]]-1,rs[x=p],y);
push(p);
} void flip(int l,int r){
int x,y,z;
spl(R,r,x,z),spl(x,l-1,x,y);
flip(y),flp[y]^=1;
R=mer(mer(x,y),z);
}
void reverse(int l,int r){
int x,y,z;
spl(R,r,x,z),spl(x,l-1,x,y);
reverse(y),rv[y]^=1;
R=mer(mer(x,y),z);
}
void print(){
Matrix ans=val[R][0]*W;
cout<<ans.a<<" "<<ans.b<<endl;
} int n,q;
char s[N],v;
int main(){
scanf("%d%d%s",&n,&q,s+1),srand(time(0));
W={1,1,0,1},E={2,mod-1,1,0};
val[0][0]=val[0][1]=rev[0][0]=rev[0][1]={1,0,0,1};
for(int i=1;i<=n;i++)R=mer(R,nd(s[i])); print();
for(int i=1,l;i<=q;i++){
scanf("%s",s+1);
if(s[1]=='A')cin>>v,R=mer(R,nd(v));
if(s[1]=='F')l=read(),flip(l,read());
if(s[1]=='R')l=read(),reverse(l,read());
print();
}
return 0;
}

平衡树 & LCT的更多相关文章

  1. 目标&计划

    目标 感觉起来NOIP还是能考到一个比较好的分数的吧 550+? 现在可能还不大行,但是过3个月或许还是能考到的 所以先订下NOIP保底500争取550+吧 至于省选... 前面有一群巨佬挡着,感觉想 ...

  2. ORCHARD WOODEN GATE

    狗: 代码小盒子 爆零秘籍 备忘录 任务计划 核心算法: 搜索/枚举/贪心 dp 分治 数据结构: 并查集 ST表 堆 线段树 树状数组 分块 树套树 平衡树 LCT 莫队 字符串: 哈希 Trie ...

  3. 总结-一本通提高篇&算竞进阶记录

    当一个人看见星空,就再无法忍受黑暗 为了点亮渐渐沉寂的星空 不想就这样退役 一定不会鸽の坑 . 一本通提高篇 . 算竞进阶 . CDQ & 整体二分 . 平衡树 . LCT . 字符串 . 随 ...

  4. [HNOI2010]弹飞绵羊 (平衡树,LCT动态树)

    题面 题解 因为每个点都只能向后跳到一个唯一的点,但可能不止一个点能跳到后面的某个相同的点, 所以我们把它抽象成一个森林.(思考:为什么是森林而不是树?) 子节点可以跳到父节点,根节点再跳就跳飞了. ...

  5. 可持久化Trie & 可持久化平衡树 专题练习

    [xsy1629]可持久化序列 - 可持久化平衡树 http://www.cnblogs.com/Sdchr/p/6258827.html [bzoj4260]REBXOR - Trie 事实上只是一 ...

  6. BZOJ_3282_Tree_(LCT)

    描述 http://www.lydsy.com/JudgeOnline/problem.php?id=3282 给出n个点以及权值,四种操作: 0.求x,y路径上的点权值的异或和. 1.连接x,y. ...

  7. 动态树LCT小结

    最开始看动态树不知道找了多少资料,总感觉不能完全理解.但其实理解了就是那么一回事...动态树在某种意思上来说跟树链剖分很相似,都是为了解决序列问题,树链剖分由于树的形态是不变的,所以可以通过预处理节点 ...

  8. 学习笔记::LCT

    今天听见茹大神20分钟讲完了LCT,10分钟讲完平衡树,5分钟讲完树剖,感觉自己智商还不及他一半... 还有很多不懂:2017/1/15 的理解: access是干什么用的? 不知道,只知道他是用来把 ...

  9. [模板] 平衡树: Splay, 非旋Treap, 替罪羊树

    简介 二叉搜索树, 可以维护一个集合/序列, 同时维护节点的 \(size\), 因此可以支持 insert(v), delete(v), kth(p,k), rank(v)等操作. 另外, prev ...

随机推荐

  1. 在kivy中加图片

    from kivy.app import App from kivy.uix.scatterlayout import ScatterLayout from kivy.uix.image import ...

  2. 零基础入门c语言函数之递归函数

    今天来总结一下关于递归函数的使用方面的问题. 递归函数就是在函数使用的时候自己调用自己,层层调用,来实现你想要的功能. 有两个最常用的例子,我们来写一下. (1)计算阶乘 #include int f ...

  3. git commit--fatal: unable to auto-detect email address

    git commit的时候报错 *** Please tell me who you are. Run git config --global user.email "you@example ...

  4. hdu 5170 GTY's math problem(水,,数学,,)

    题意: 给a,b,c,d. 比较a^b和c^d的大小 思路: 比较log(a^b)和log(c^d)的大小 代码: int a,b,c,d; int main(){ while(scanf(" ...

  5. Harbor仓库搭建及使用

    目录 一.docker配置 二.安装docker-compose 三.安装harbor 四.管理harbor 五.springboot项目配置docker 六.linux服务器上打包并推送至harbo ...

  6. 【Go语言学习笔记】hello world

    书接上回,上回说到了为什么要学习Go语言,今天我们来实际写一下,感受一下Go语言的精美之处. 环境搭建 安装和设置 Windows: Go安装包下载网址:https://golang.org/dl/ ...

  7. Redis网络库源码分析(3)之ae.c

    一.aeCreateEventLoop & aeCreateFileEvent 上一篇文章中,我们已经将服务器启动,只是其中有些细节我们跳过了,比如aeCreateEventLoop函数到底做 ...

  8. 文件与文件系统的压缩与打包 tar gzip bzip2

    1:linux下常见的压缩文件后缀: .gz .zip .bz2 打包后的: .tar.gz .tar.zip .tar.bz2 2:gzip: 压缩:gzip file 解压:gunzip file ...

  9. Spring一套全通—工厂

    百知教育 - Spring系列课程 - 工厂 第一章 引言 1. EJB存在的问题 2. 什么是Spring Spring是一个轻量级的JavaEE解决方案,整合众多优秀的设计模式 轻量级 1. 对于 ...

  10. vue脚手架项目如何在控制台打印组件实例

    需要在浏览器上安装拓展程序vue开发工具,安装好后在控制台上输入$vm即可打印vue组件实例对象. Vue2.3开发工具都有,可自行下载 百度网盘链接提取码:si5l