平衡树 & LCT
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}\)。
接下来就可以用平衡树维护一整个序列了。不过对于操作
FLIP
和REVERSE
似乎不太好办。- 对于一个
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的更多相关文章
- 目标&计划
目标 感觉起来NOIP还是能考到一个比较好的分数的吧 550+? 现在可能还不大行,但是过3个月或许还是能考到的 所以先订下NOIP保底500争取550+吧 至于省选... 前面有一群巨佬挡着,感觉想 ...
- ORCHARD WOODEN GATE
狗: 代码小盒子 爆零秘籍 备忘录 任务计划 核心算法: 搜索/枚举/贪心 dp 分治 数据结构: 并查集 ST表 堆 线段树 树状数组 分块 树套树 平衡树 LCT 莫队 字符串: 哈希 Trie ...
- 总结-一本通提高篇&算竞进阶记录
当一个人看见星空,就再无法忍受黑暗 为了点亮渐渐沉寂的星空 不想就这样退役 一定不会鸽の坑 . 一本通提高篇 . 算竞进阶 . CDQ & 整体二分 . 平衡树 . LCT . 字符串 . 随 ...
- [HNOI2010]弹飞绵羊 (平衡树,LCT动态树)
题面 题解 因为每个点都只能向后跳到一个唯一的点,但可能不止一个点能跳到后面的某个相同的点, 所以我们把它抽象成一个森林.(思考:为什么是森林而不是树?) 子节点可以跳到父节点,根节点再跳就跳飞了. ...
- 可持久化Trie & 可持久化平衡树 专题练习
[xsy1629]可持久化序列 - 可持久化平衡树 http://www.cnblogs.com/Sdchr/p/6258827.html [bzoj4260]REBXOR - Trie 事实上只是一 ...
- BZOJ_3282_Tree_(LCT)
描述 http://www.lydsy.com/JudgeOnline/problem.php?id=3282 给出n个点以及权值,四种操作: 0.求x,y路径上的点权值的异或和. 1.连接x,y. ...
- 动态树LCT小结
最开始看动态树不知道找了多少资料,总感觉不能完全理解.但其实理解了就是那么一回事...动态树在某种意思上来说跟树链剖分很相似,都是为了解决序列问题,树链剖分由于树的形态是不变的,所以可以通过预处理节点 ...
- 学习笔记::LCT
今天听见茹大神20分钟讲完了LCT,10分钟讲完平衡树,5分钟讲完树剖,感觉自己智商还不及他一半... 还有很多不懂:2017/1/15 的理解: access是干什么用的? 不知道,只知道他是用来把 ...
- [模板] 平衡树: Splay, 非旋Treap, 替罪羊树
简介 二叉搜索树, 可以维护一个集合/序列, 同时维护节点的 \(size\), 因此可以支持 insert(v), delete(v), kth(p,k), rank(v)等操作. 另外, prev ...
随机推荐
- 【Spring】重新认识 IoC
前言 IoC (Inversion of control) 并不是Spring特有的概念. IoC 维基百科的解释: In software engineering, inversion of con ...
- 谜语人队 Scrum Meeting 博客汇总
项目 内容 课程主页 2021春季软件工程(罗杰 任健) 作业要求地址 Alpha阶段:团队项目-每日例会报告Beta阶段:团队项目-每日例会报告 团队博客主页 谜语人队 一.Alpha阶段 第一次例 ...
- 基于jpa的specification实现动态查询
spring data jpa为我们实现简单的crud操作提供了极大的方便.但大部分情况下,系统中都存在大量的动态查询操作,这个时候就可以借助spring data jpa的 Specificatio ...
- C语言零基础入门难发愁,那就快来看看这篇基础整理资料吧
C语言程序的结构认识 用一个简单的c程序例子,介绍c语言的基本构成.格式.以及良好的书写风格,使小伙伴对c语言有个初步认识. 例1:计算两个整数之和的c程序: #include main() { in ...
- 常用Java API:Math类
求最值 最小值 Math.min(int a, int b) Math.min(float a, float b) Math.min(double a, doubleb) Math.min(long ...
- TensorFlow从入门到入坑(2)
TensorFlow学习(2) 一.jupyter notebook的安装和使用 1. 什么是jupyter notebook jupyter notebook(http://jupyter.org/ ...
- Spring---IoC(控制反转)原理学习笔记【全】
1.IoC创建对象的方式 使用无参构造创建对象 假如要使用有参构造创建: 下标赋值constructor-arg <!--有参--> <bean id="User" ...
- 编译安装与gcc编译器
先说一下gcc编译器,只知道这是一个老款的编译器.编译的目的也比较重要,就是将c语言的代码编译成可以执行的binary文件. gcc 简单使用(后期补充) eg: gcc example.c # ...
- linux初中级命令语言
Linux:开源免费.大部分软件都可以自由获取,同样功能的软件选择较少.主要是字符模式,命令行界面且发行版本较多,难以集中攻击. Xshell与xftp是什么? xshell是一个客户端软件,我们本地 ...
- 【前端工具】nodejs+npm+vue 安装(windows)
预备 先看看这几个是干嘛的,相互的关系是啥. nodejs是语言,类比到php. npm是个包管理,类比到composer. vue是个框架,类比到laravel. webpack是个打包工具. 先下 ...