「平衡树」学习笔记

BST

又称二分查找树,BST 性质指其左子树所有节点全职均小于该点,其右子树所有节点全职均大于该点;同时若对该棵树进行中序遍历,所产生的序列为从小到大排序的序列。

利用该性质,从而在 O(log(n)) 的复杂度内实现查询排名、第 k 小(大)值、前驱、后继等。

当每次插入的数据呈单调性时,其内部将形成一条链,从而使复杂度退化为 O(n)

Treap

当数据随机时,其内部时趋于平衡的,Treap 就是基于随机化与二叉堆使其实现平衡。

对于如下的几种基本操作单次复杂度均为 O(log(n))

旋转

对于内一个点赋予其一个随机值,使这棵树是该随机值的二叉堆。

具体如何旋转操作在 splay 那里会有更详细的解释。

旋转操作是使 Treap 达到平衡的核心。

旋转操作
void rotate(int &p,bool d)
{
int son=f.ch[d];
f.ch[d]=t[son].ch[d^1];
t[son].ch[d^1]=p;
pushup(p);
pushup(p=son);
}

插入

找到其应插入的位置,若书中已存在该值则 cnt++ ,否则创建新点,同时根据其随机值旋转,使达到平衡。

插入操作
void insert(int &p,int x)
{
if(p==0)
{
p=++tot;
f.cnt=f.size=1;
f.val=x;
f.dat=rand();
return ;
}
f.size++;
if(f.val==x)
{
f.cnt++;
return ;
}
bool d=f.val<x;
insert(f.ch[d],x);
if(f.dat>t[f.ch[d]].dat) rotate(p,d);
}

删除

找到所要删除点所在位置,如果其 cnt>1 ,直接 cnt 即可。

否则,如果该节点只有一个子节点,那么直接让子节点替换其位置即可;若该点为叶子节点,那么直接删掉它不会产生任何其他的影响。

因为 Treap 支持旋转操作,将其直接旋转到满足上面两种情况即可。

删除操作
void remove(int &p,int x)
{
if(p==0) return ;
if(f.val==x)
{
if(f.cnt>1)
{
f.cnt--;
f.size--;
return ;
}
bool d=ls.dat>rs.dat;
if(f.l==0||f.r==0) p=f.l+f.r;
else rotate(p,d),remove(p,x);
}
else f.size--,remove(f.ch[f.val<x],x);
}

查询排名

x 的排名定义为比 x 小的个数 +1

因为其满足 BST 性质,从根节点开始向下寻找,设当前点权值为 vallsrs 分别指左右儿子,cnt 指该节点重复插入次数,size 指对应节点子树的大小。

  • val==x ,即已经找到答案,返回 ls.size+1

  • val>x ,继续向其左儿子方向寻找。

  • val<x ,继续向右儿子方向寻找,同时答案加上 ls.size+cnt

在很多时候要查询的数并不在树里,但同时根据比他小的个数加一仍可以求出其排名,常见解决办法有两种,不会对复杂度产生较大影响。

  • 在查询前先插入该点,查询后再将该点删除。

  • 当寻找到一个不存在的点时,显然只有所查询数不在树中时才可能出现这种情况,此时令他返回 1 而不是 0 ,即比他小的个数 +1

查询排名
int ask_rk(int p,int x)
{
if(p==0) return 1;
if(f.val==x) return ls.size+1;
if(f.val>x) return ask_rk(f.l,x);
return ask_rk(f.r,x)+ls.size+f.cnt;
}

查询第 k 小(大)值

以查询第 k 小值为例,因为其满足 BST 性质,设 lsrs 分别指左右儿子,size 指对应节点子树的大小,cnt 指该节点重复插入次数。

  • ls.size>=k ,继续向其左儿子方向寻找。

  • ls.size<k&&ls.size+cnt>=k ,该节点即为答案。

  • ls.size+cnt<k ,另 k 减去 ls.size+cnt ,继续向其右儿子方向寻找。

查询第 k 小值
int ask_val(int p,int x)
{
if(p==0) return inf;
if(ls.size>=x) return ask_val(f.l,x);
if(ls.size+f.cnt>=x) return f.val;
return ask_val(f.r,x-ls.size-f.cnt);
}

查询前驱、后继

以前驱为例。

根据 BST 性质,找到第一个小于其的位置,之后不停向右儿子方向寻找。

查询前驱
int pre(int p,int x)
{
if(p==0) return -inf;
if(f.val>=x) return pre(f.l,x);
return max(pre(f.r,x),f.val);
}
查询后继
int nxt(int p,int x)
{
if(p==0) return inf;
if(f.val<=x) return nxt(f.r,x);
return min(nxt(f.l,x),f.val);
}

fhq_Treap

又名无旋 Treap ,以分裂与合并为核心操作代替旋转使其实现平衡。

其分裂与合并分为按照权值合并与按照树的大小合并两种,当维护序列时按照大小分则更加方便,此处以按照权值分为例,另一种将在文艺平衡树中讲解。

分裂

p 分为 xy 两部分,x 中节点全部 ky 中节点全部 >k

  • val>k ,说明其左子树全部在 x 内,故 y=p ,向左儿子方向继续分裂。

  • 反之,其右子树全部在 y 内,则 x=p ,继续向右儿子方向分裂。

分裂操作
void split(int p,int k,int &x,int &y)
{
if(!p) {x=y=0; return ;}
if(f.val>k) y=p,split(f.l,k,x,t[y].l);
else x=p,split(f.r,k,t[x].r,y);
pushup(p);
}

合并

通常是将被分开的两部分再合并起来,将 xy 合并成 p ,满足 x 中节点全部小于 y 中节点。

此时就可以按照其随机值合并,使其平衡。

合并操作
int merge(int x,int y)
{
if(!x||!y) return x+y;
int p;
if(t[x].dat>t[y].dat)t[x].r=merge(t[x].r,y),pushup(p=x);
else t[y].l=merge(x,t[y].l),pushup(p=y);
return p;
}

插入

k 插入。

root 按照 k1 分裂为 xy ,再按照 xnewy 合并起来。

插入操作
void insert(int &p,int k)
{
t[++tot].val=k,t[tot].size=1,t[tot].dat=rand();
int x,y;
split(p,k-1,x,y);
p=merge(merge(x,tot),y);
}

删除

k 删除。

依旧将 root 按照 k1 分裂成 xy ,再将 y 按照 k 分裂成 isy ,将其按照 xis.ls,is.rs,y 合并。

删除操作
void remove(int &p,int k)
{
int x,y,is;
split(p,k-1,x,y),split(y,k,is,y);
p=merge(merge(x,merge(t[is].l,t[is].r)),y);
}

查询排名

k 的排名。

root 按照 k1 分裂为 xyx.size+1 即为所求。

查询排名
int ask_rk(int p,int k)
{
int x,y,is;
split(p,k-1,x,y);
is=t[x].size+1;
p=merge(x,y);
return is;
}

其余操作与 Treap 基本一致。

优点

  1. 为所有平衡树中最好打,最容易理解的一种。

  2. 因为不需要旋转,所以可以可持久化。

  3. 允许有权值一样的不同节点,因为其是按照权值分裂的。

  4. 因为按照权值分裂,所以不在树中的数也可以直接查。

  5. 当其按照树的大小分裂时,可以实现维护序列,这是 Treap 做不到的,大多数 splay 能做的他都能做。

splay

splay 的核心在于其双旋操作,使其不依赖于随机值。

在每次操作后,将该点进行双旋转到根节点的位置,从而使复杂度达到均摊单次 O(log(n)) ,其复杂度分析详见 oi-wiki

哨兵节点

即添加 infinf 节点,为极小值和极大值,使其避免查找出界,会对一些操作产生需要 ±1 的操作。

单旋操作

image

xyk 方向上的儿子.

旋转后 yxk1 方向上的儿子。

xk1 方向上的儿子成为 yk 方向上的儿子。

其余不变。

单旋操作
void rotate(int x)
{
int y=t[x].ff,z=t[y].ff,k=fson(y,x);
t[z].ch[fson(z,y)]=x;
t[x].ff=z;
t[y].ch[k]=t[x].ch[k^1];
t[t[x].ch[k^1]].ff=y;
t[x].ch[k^1]=y;
t[y].ff=x;
pushup(y),pushup(x);
}

splay 操作(双旋操作)

将点 x 旋转到 goal 的子节点位置。

定义 fax=y,fay=z ,当 xyz 满足在同一条链时,先旋转 y ,再旋转 x ,使其满足平衡。

如图:

imageimageimage

x 不停进行单旋操作,左图中最大深度为 4 ,右图最大深度仍为 4 ,随意其实没有意义的。

对于上面所说的情况,如果先旋转 y ,再旋转 x ,最后为:

image

从而达到平衡。

splay 操作
void splay(int x,int goal)
{
while(t[x].ff!=goal)
{
int y=t[x].ff,z=t[y].ff;
if(z!=goal)
fson(z,y)^fson(y,x)?rotate(x):rotate(y);
rotate(x);
}
if(goal==0) root=x;
}

另外为了方便,有一个 find 操作,即找到某点位置,再将其转到根节点。

find 操作
void find(int x)
{
int p=root;
if(!p) return ;
while(f.ch[x>f.val]&&x!=f.val) p=f.ch[x>f.val];
splay(p,0);
}

插入操作

找到离 x 最近的点,若该点权值就是 x ,则 cnt++ ,否则建一个新的点,最后都要将其旋转到根节点。

插入操作
void insert(int x)
{
int p=root,ff=0;
while(p&&f.val!=x)
ff=p,
p=f.ch[x>f.val];
if(p) {f.cnt++; splay(p,0); return ;}
p=++tot;
if(ff) t[ff].ch[x>t[ff].val]=p;
f.ff=ff,f.val=x,f.size=f.cnt=1;
splay(p,0);
}

删除操作

删除操作需要先找到其前驱与后继对应的位置,将前驱转到根节点,再将后继转到前驱的右儿子位置,那么此时他们俩中间夹着的,即后继的左儿子,即为 x 的位置,删除即可。

删除操作
void remove(int x)
{
int pre=pre_nxt(x,0),nxt=pre_nxt(x,1);
splay(pre,0),splay(nxt,pre);
int p=t[nxt].l;
if(f.cnt>1) f.cnt--,splay(p,0);
else t[nxt].l=0;
}

查询前驱、后继

以前驱为例。

先找到离 x 最近的位置,如果该点权值就是 x ,先到其左儿子,然后向右不断地找,即为所求。

如果该点值不是 x ,若其恰好满足比 x 小,则该点即为所求,否则和上述一样即可。

注意此处找到的是其前驱的位置,不是数值,为了方便删除操作。

查询前驱、后继
int pre_nxt(int x,bool d)
{
find(x);
int p=root;
if((f.val>x&&d)||(f.val<x&&!d)) return p;
p=f.ch[d];
while(f.ch[d^1]) p=f.ch[d^1];
return p;
}

查询排名

此时需要满足 x 一定在树内,所以可以先插入,待查询后再删除。

先找到 x 所在位置,将其转到根节点位置,则此时其左儿子个数 +1 即为所求。

由于添加了哨兵节点,实际还需要 1 ,所以 ls.size 即为所求。

查询排名
int ask_rk(int x)
{
find(x);
int p=root;
return ls.size;
}

查询第 k 小(大)值

Treap 一致。

优点

因为其 splay 操作,所以其能够进行维护序列等各种操作,完成所有 fhq 能支持甚至不支持的各种操作,但是常熟更大,码量更长,不如 fhq 好理解。

例题

基本操作

前三道为最基本的查询前驱后继、第 k 大值等问题。

luogu P2234 营业额统计

luogu P2286 宠物收养场

luogu P1486 郁闷的出纳员

luogu P3369 普通平衡树

Treap
#include<bits/stdc++.h>
// #define int long long
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=1e5+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,tot,root;
struct treap
{
int ch[2],cnt,size,val,dat;
}t[N];
void pushup(int p)
{
f.size=ls.size+rs.size+f.cnt;
}
void rotate(int &p,bool d)
{
int son=f.ch[d];
f.ch[d]=t[son].ch[d^1];
t[son].ch[d^1]=p;
pushup(p);
pushup(p=son);
}
void insert(int &p,int x)
{
if(p==0)
{
p=++tot;
f.cnt=f.size=1;
f.val=x;
f.dat=rand();
return ;
}
f.size++;
if(f.val==x)
{
f.cnt++;
return ;
}
bool d=f.val<x;
insert(f.ch[d],x);
if(f.dat>t[f.ch[d]].dat) rotate(p,d);
}
void remove(int &p,int x)
{
if(p==0) return ;
if(f.val==x)
{
if(f.cnt>1)
{
f.cnt--;
f.size--;
return ;
}
bool d=ls.dat>rs.dat;
if(f.l==0||f.r==0) p=f.l+f.r;
else rotate(p,d),remove(p,x);
}
else f.size--,remove(f.ch[f.val<x],x);
}
int ask_rk(int p,int x)
{
if(p==0) return 1;
if(f.val==x) return ls.size+1;
if(f.val>x) return ask_rk(f.l,x);
return ask_rk(f.r,x)+ls.size+f.cnt;
}
int ask_val(int p,int x)
{
if(p==0) return inf;
if(ls.size>=x) return ask_val(f.l,x);
if(ls.size+f.cnt>=x) return f.val;
return ask_val(f.r,x-ls.size-f.cnt);
}
int pre(int p,int x)
{
if(p==0) return -inf;
if(f.val>=x) return pre(f.l,x);
return max(pre(f.r,x),f.val);
}
int nxt(int p,int x)
{
if(p==0) return inf;
if(f.val<=x) return nxt(f.r,x);
return min(nxt(f.l,x),f.val);
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
read(n);
while(n--)
{
int op,x;
read(op),read(x);
switch(op)
{
case 1: insert(root,x); break;
case 2: remove(root,x); break;
case 3: write(ask_rk(root,x)),puts(""); break;
case 4: write(ask_val(root,x)),puts(""); break;
case 5: write(pre(root,x)),puts(""); break;
case 6: write(nxt(root,x)),puts(""); break;
}
}
}
fhq_Treap
#include<bits/stdc++.h>
// #define int long long
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=1e5+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,root,tot;
struct fhq_treap
{
int ch[2],val,dat,size;
}t[N];
void pushup(int p) {f.size=ls.size+rs.size+1;}
void split(int p,int k,int &x,int &y)
{
if(!p) {x=y=0; return ;}
if(f.val>k) y=p,split(f.l,k,x,t[y].l);
else x=p,split(f.r,k,t[x].r,y);
pushup(p);
}
int merge(int x,int y)
{
if(!x||!y) return x+y;
int p;
if(t[x].dat>t[y].dat)t[x].r=merge(t[x].r,y),pushup(p=x);
else t[y].l=merge(x,t[y].l),pushup(p=y);
return p;
}
void insert(int &p,int k)
{
t[++tot].val=k,t[tot].size=1,t[tot].dat=rand();
int x,y;
split(p,k-1,x,y);
p=merge(merge(x,tot),y);
}
void remove(int &p,int k)
{
int x,y,is;
split(p,k-1,x,y),split(y,k,is,y);
p=merge(merge(x,merge(t[is].l,t[is].r)),y);
}
int ask_rk(int p,int k)
{
int x,y,is;
split(p,k-1,x,y);
is=t[x].size+1;
p=merge(x,y);
return is;
}
int ask_val(int p,int k)
{
if(!p) return inf;
if(ls.size>=k) return ask_val(f.l,k);
if(ls.size+1>=k) return f.val;
return ask_val(f.r,k-ls.size-1);
}
int pre(int p,int k)
{
if(!p) return -inf;
if(f.val>=k) return pre(f.l,k);
return max(pre(f.r,k),f.val);
}
int nxt(int p,int k)
{
if(!p) return inf;
if(f.val<=k) return nxt(f.r,k);
return min(nxt(f.l,k),f.val);
}
signed main()
{
read(n);
for(int i=1,op,x;i<=n;i++)
{
read(op),read(x);
switch(op)
{
case 1: insert(root,x); break;
case 2: remove(root,x); break;
case 3: write(ask_rk(root,x)),puts(""); break;
case 4: write(ask_val(root,x)),puts(""); break;
case 5: write(pre(root,x)),puts(""); break;
case 6: write(nxt(root,x)),puts(""); break;
}
}
}
splay
#include<bits/stdc++.h>
// #define int long long
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=1e5+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,root,tot;
struct splay
{
int ch[2],ff,size,cnt,val;
}t[N];
bool fson(int x,int y) {return (t[x].ch[1]==y);}
void pushup(int p) {f.size=ls.size+rs.size+f.cnt;}
void rotate(int x)
{
int y=t[x].ff,z=t[y].ff,k=fson(y,x);
t[z].ch[fson(z,y)]=x;
t[x].ff=z;
t[y].ch[k]=t[x].ch[k^1];
t[t[x].ch[k^1]].ff=y;
t[x].ch[k^1]=y;
t[y].ff=x;
pushup(y),pushup(x);
}
void splay(int x,int goal)
{
while(t[x].ff!=goal)
{
int y=t[x].ff,z=t[y].ff;
if(z!=goal)
fson(z,y)^fson(y,x)?rotate(x):rotate(y);
rotate(x);
}
if(goal==0) root=x;
}
void find(int x)
{
int p=root;
if(!p) return ;
while(f.ch[x>f.val]&&x!=f.val) p=f.ch[x>f.val];
splay(p,0);
}
void insert(int x)
{
int p=root,ff=0;
while(p&&f.val!=x)
ff=p,
p=f.ch[x>f.val];
if(p) {f.cnt++; splay(p,0); return ;}
p=++tot;
if(ff) t[ff].ch[x>t[ff].val]=p;
f.ff=ff,f.val=x,f.size=f.cnt=1;
splay(p,0);
}
int pre_nxt(int x,bool d)
{
find(x);
int p=root;
if((f.val>x&&d)||(f.val<x&&!d)) return p;
p=f.ch[d];
while(f.ch[d^1]) p=f.ch[d^1];
return p;
}
void remove(int x)
{
int pre=pre_nxt(x,0),nxt=pre_nxt(x,1);
splay(pre,0),splay(nxt,pre);
int p=t[nxt].l;
if(f.cnt>1) f.cnt--,splay(p,0);
else t[nxt].l=0;
}
int ask_rk(int x)
{
find(x);
int p=root;
return ls.size;
}
int ask_val(int p,int x)
{
if(!p) return inf;
if(ls.size>=x) return ask_val(f.l,x);
if(ls.size+f.cnt>=x) return f.val;
return ask_val(f.r,x-ls.size-f.cnt);
}
signed main()
{
insert(-inf),insert(inf);
read(n);
for(int i=1,op,x;i<=n;i++)
{
read(op),read(x);
switch(op)
{
case 1: insert(x); break;
case 2: remove(x); break;
case 3:
insert(x);
write(ask_rk(x)),puts("");
remove(x);
break;
case 4: write(ask_val(root,x+1)),puts(""); break;
case 5: write(t[pre_nxt(x,0)].val),puts(""); break;
case 6: write(t[pre_nxt(x,1)].val),puts(""); break;
}
}
}

luogu P3380 树套树

可以有多种实现方法,此处采用思路较易的线段树维护区间,平衡树维护权值,缺点是查询第 k 小值需要 log3 ,其余均是正常的 log2 ,但因为常熟不是很大,并没有慢太多。

  • 建树与修改

    对于线段树上每一个节点开一棵平衡树,直接暴力插入,因为线段树共有 log(n) 层,每一层元素数量均为 n ,每次插入复杂度为 O(log(n)) ,所以建树的复杂度为 O(nlog2(n))

    修改即删除旧的,插入新的即可,修改后不要忘了另 ai=new

  • 查询排名

    对于比 k 小的个数区间求和最后 +1 即可,复杂度 O(log2(n))

  • 查询第 k 小值

    考虑二分答案,比较该数的排名与 k 的大小,复杂度 O(log3(n))

  • 查询前驱、后继

    前驱则是线段树区间最大值,后继为区间最小值,当然都是针对线段树上每个节点对于平衡树上的前驱(后继)值,复杂度 O(log2(n))

树套树
#include<bits/stdc++.h>
// #define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=8e6+10,inf=2147483647;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,m,tot,a[N],maxx=-inf,minn=inf;
struct fhq_treap
{
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
struct treap
{
int ch[2],cnt,size,val,dat;
}t[N];
void pushup(int p) {f.size=ls.size+rs.size+1;}
void split(int p,int k,int &x,int &y)
{
if(!p) {x=y=0; return ;}
if(f.val>k) y=p,split(f.l,k,x,t[y].l);
else x=p,split(f.r,k,t[x].r,y);
pushup(p);
}
int merge(int x,int y)
{
if(!x||!y) return x+y;
int p;
if(t[x].dat>t[y].dat)t[x].r=merge(t[x].r,y),pushup(p=x);
else t[y].l=merge(x,t[y].l),pushup(p=y);
return p;
}
void insert(int &p,int k)
{
t[++tot].val=k,t[tot].size=1,t[tot].dat=rand();
int x,y;
split(p,k-1,x,y);
p=merge(merge(x,tot),y);
}
void remove(int &p,int k)
{
int x,y,is;
split(p,k-1,x,y),split(y,k,is,y);
p=merge(merge(x,merge(t[is].l,t[is].r)),y);
}
int leq(int &p,int k)
{
int x,y,is;
split(p,k-1,x,y);
is=t[x].size;
p=merge(x,y);
return is;
}
int pre(int p,int k)
{
if(!p) return -inf;
if(f.val>=k) return pre(f.l,k);
return max(pre(f.r,k),f.val);
}
int nxt(int p,int k)
{
if(!p) return inf;
if(f.val<=k) return nxt(f.r,k);
return min(nxt(f.l,k),f.val);
}
#undef f
#undef ls
#undef rs
#undef l
#undef r
}fhq;
#define f t[p]
#define ls p<<1
#define rs p<<1|1
struct aa {int l,r,root;}t[N];
void build(int p,int l,int r)
{
f.l=l,f.r=r;
for(int i=l;i<=r;i++)
fhq.insert(f.root,a[i]);
if(l==r) return ;
int mid=(l+r)>>1;
build(ls,l,mid),build(rs,mid+1,r);
}
void change(int p,int x,int d)
{
fhq.remove(f.root,a[x]);
fhq.insert(f.root,d);
if(f.l==f.r) return ;
int mid=(f.l+f.r)>>1;
if(x<=mid) change(ls,x,d);
else change(rs,x,d);
}
int leq(int p,int l,int r,int x)
{
if(l<=f.l&&r>=f.r) return fhq.leq(f.root,x);
int mid=(f.l+f.r)>>1,ans=0;
if(l<=mid) ans+=leq(ls,l,r,x);
if(r>mid) ans+=leq(rs,l,r,x);
return ans;
}
int ask_rk(int l,int r,int x) {return leq(1,l,r,x)+1;}
int ask_val(int ll,int rr,int x)
{
int l=minn,r=maxx,mid,ans=inf;
while(l<=r)
{
mid=(l+r)>>1;
if(ask_rk(ll,rr,mid)<=x) ans=mid,l=mid+1;
else r=mid-1;
}
return ans;
}
int pre(int p,int l,int r,int x)
{
if(l<=f.l&&r>=f.r) return fhq.pre(f.root,x);
int mid=(f.l+f.r)>>1,ans=-inf;
if(l<=mid) ans=max(ans,pre(ls,l,r,x));
if(r>mid) ans=max(ans,pre(rs,l,r,x));
return ans;
}
int nxt(int p,int l,int r,int x)
{
if(l<=f.l&&r>=f.r) return fhq.nxt(f.root,x);
int mid=(f.l+f.r)>>1,ans=inf;
if(l<=mid) ans=min(ans,nxt(ls,l,r,x));
if(r>mid) ans=min(ans,nxt(rs,l,r,x));
return ans;
}
#undef f
#undef ls
#undef rs
signed main()
{
read(n),read(m);
for(int i=1;i<=n;i++)
read(a[i]),
maxx=max(maxx,a[i]),minn=min(minn,a[i]);
build(1,1,n);
for(int i=1,op,l,r,x,k;i<=m;i++)
{
read(op);
switch(op)
{
case 1:
read(l),read(r),read(k),
write(ask_rk(l,r,k)),puts("");
break;
case 2:
read(l),read(r),read(k),
write(ask_val(l,r,k)),puts("");
break;
case 3:
read(x),read(k),
maxx=max(maxx,k),minn=min(minn,k);
change(1,x,k);
a[x]=k;
break;
case 4:
read(l),read(r),read(k),
write(pre(1,l,r,k)),puts("");
break;
case 5:
read(l),read(r),read(k),
write(nxt(1,l,r,k)),puts("");
break;
}
}
}

维护序列操作

luogu P3391 文艺平衡树

利用平衡树实现维护序列操作,可以使用 fhqsplay

  • fhq

    对于每次旋转,将其按照树的大小分裂,先按照 r 分裂, 再将左半部分按照 l1 分裂,则中间一部分即为所求区间,向下打标记即可。

    翻转即左右儿子互换。

    最后中序遍历输出。

    文艺平衡树(fhq)
    #include<bits/stdc++.h>
    // #define int long long
    #define endl '\n'
    #define sort stable_sort
    #define f t[p]
    #define ls t[t[p].ch[0]]
    #define rs t[t[p].ch[1]]
    #define l ch[0]
    #define r ch[1]
    using namespace std;
    const int N=1e5+10,inf=0x7f7f7f7f;
    template<typename Tp> inline void read(Tp&x)
    {
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
    }
    void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
    void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
    int n,m,tot,root;
    struct fhq_treap
    {
    int ch[2],size,dat,val;
    bool add;
    }t[N];
    void pushup(int p) {f.size=ls.size+rs.size+1;}
    void spread(int p)
    {
    if(!f.add||!p) return ;
    swap(f.l,f.r);
    ls.add^=1,rs.add^=1;
    f.add=0;
    }
    void split(int p,int k,int &x,int &y)
    {
    if(!p) {x=y=0; return ;}
    spread(p);
    if(ls.size>=k) y=p,split(f.l,k,x,t[y].l);
    else x=p,split(f.r,k-ls.size-1,t[x].r,y);
    pushup(p);
    }
    int merge(int x,int y)
    {
    if(!x||!y) return x+y;
    spread(x),spread(y);
    int p;
    if(t[x].dat>t[y].dat) t[x].r=merge(t[x].r,y),pushup(p=x);
    else t[y].l=merge(x,t[y].l),pushup(p=y);
    return p;
    }
    void insert(int &p,int k)
    {
    t[++tot].val=k,t[tot].size=1,t[tot].dat=rand();
    p=merge(p,tot);
    }
    void solve(int &p,int ll,int rr)
    {
    int x,y,is;
    split(p,rr,x,y),split(x,ll-1,x,is);
    t[is].add^=1;
    p=merge(merge(x,is),y);
    }
    void dfs(int p)
    {
    if(!p) return ;
    spread(p);
    dfs(f.l);
    write(f.val),putchar(' ');
    dfs(f.r);
    }
    signed main()
    {
    read(n),read(m);
    for(int i=1;i<=n;i++)
    insert(root,i);
    for(int i=1,ll,rr;i<=m;i++)
    read(ll),read(rr),
    solve(root,ll,rr);
    dfs(root);
    }
  • splay

    找到 l1 的位置,定义为 xr+1 的位置,定义为 y ,将 x 转到根节点,再将 y 转为 x 的子节点,那么根节点的右儿子的左儿子即对应所求区间,其余操作与 fhq 一致。

    文艺平衡树(splay)
    #include<bits/stdc++.h>
    // #define int long long
    #define endl '\n'
    #define sort stable_sort
    #define f t[p]
    #define ls t[t[p].ch[0]]
    #define rs t[t[p].ch[1]]
    #define l ch[0]
    #define r ch[1]
    using namespace std;
    const int N=1e5+10,inf=0x7f7f7f7f;
    template<typename Tp> inline void read(Tp&x)
    {
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
    }
    void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
    void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
    int n,m,root,tot;
    struct splay
    {
    int ch[2],ff,size,cnt,val;
    bool add;
    }t[N];
    bool fson(int x,int y) {return t[x].ch[1]==y;}
    void pushup(int p) {f.size=ls.size+rs.size+f.cnt;}
    void spread(int p)
    {
    if(!p||!f.add) return ;
    swap(f.l,f.r);
    ls.add^=1,rs.add^=1;
    f.add=0;
    }
    void rotate(int x)
    {
    int y=t[x].ff,z=t[y].ff,k=fson(y,x);
    t[z].ch[fson(z,y)]=x;
    t[x].ff=z;
    t[y].ch[k]=t[x].ch[k^1];
    t[t[x].ch[k^1]].ff=y;
    t[x].ch[k^1]=y;
    t[y].ff=x;
    pushup(y),pushup(x);
    }
    void splay(int x,int goal)
    {
    while(t[x].ff!=goal)
    {
    int y=t[x].ff,z=t[y].ff;
    if(z!=goal)
    fson(z,y)^fson(y,x)?rotate(x):rotate(y);
    rotate(x);
    }
    if(goal==0) root=x;
    }
    void insert(int x)
    {
    int p=root,ff=0;
    while(p&&f.val!=x)
    ff=p,
    p=f.ch[x>f.val];
    if(p) {f.cnt++; splay(p,0); return ;}
    p=++tot;
    if(ff) t[ff].ch[x>t[ff].val]=p;
    f.ff=ff,f.val=x,f.size=f.cnt=1;
    splay(p,0);
    }
    int find(int p,int x)
    {
    spread(p);
    if(ls.size>=x) return find(f.l,x);
    if(ls.size+f.cnt>=x) return p;
    return find(f.r,x-ls.size-f.cnt);
    }
    void solve(int x,int y)
    {
    x=find(root,x),y=find(root,y);
    splay(x,0),splay(y,x);
    t[t[t[root].r].l].add^=1;
    }
    void dfs(int p)
    {
    if(!p) return ;
    spread(p);
    dfs(f.l);
    if(f.val!=1&&f.val!=n+2) write(f.val-1),putchar(' ');
    dfs(f.r);
    }
    signed main()
    {
    read(n),read(m);
    for(int i=1;i<=n+2;i++) insert(i);
    for(int i=1,ll,rr;i<=m;i++)
    read(ll),read(rr),
    solve(ll,rr+2);
    dfs(root);
    }

luogu P3165 排序机械臂

因为其不会插入新点,所以可以通过排序得知第 k 小值的编号。

对于查询其所在位置,利用 splay 将其转到根节点,则根节点的左儿子的 size+1 即为所求。

区间翻转操作与文艺平衡树类似,同时向树中存的应为下标而不是权值。

注意审题与哨兵节点插入的顺序。

机械臂排序
#include<bits/stdc++.h>
// #define int long long
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=1e5+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,root,tot,a[N];
struct aa {int h,id;}e[N];
bool cmp(aa a,aa b) {return a.h==b.h?a.id<b.id:a.h<b.h;}
struct splay
{
int ch[2],ff,size,cnt,val;
bool add;
}t[N];
bool fson(int x,int y) {return t[x].ch[1]==y;}
void pushup(int p) {f.size=ls.size+rs.size+f.cnt;}
void spread(int p)
{
if(!p||!f.add) return ;
swap(f.l,f.r);
ls.add^=1,rs.add^=1;
f.add=0;
}
void rotate(int x)
{
int y=t[x].ff,z=t[y].ff,k=fson(y,x);
t[z].ch[fson(z,y)]=x;
t[x].ff=z;
t[y].ch[k]=t[x].ch[k^1];
t[t[x].ch[k^1]].ff=y;
t[x].ch[k^1]=y;
t[y].ff=x;
pushup(y),pushup(x);
}
void splay(int x,int goal)
{
while(t[x].ff!=goal)
{
int y=t[x].ff,z=t[y].ff;
spread(z),spread(y),spread(x);
if(z!=goal)
fson(z,y)^fson(y,x)?rotate(x):rotate(y);
rotate(x);
}
if(goal==0) root=x;
}
void insert(int x)
{
int p=root,ff=0;
while(p&&f.val!=x)
ff=p,
p=f.ch[x>f.val];
if(p) {f.cnt++; splay(p,0); return ;}
p=++tot;
if(ff) t[ff].ch[x>t[ff].val]=p;
f.ff=ff,f.val=x,f.size=f.cnt=1;
splay(p,0);
}
int find(int p,int x)
{
spread(p);
if(ls.size>=x) return find(f.l,x);
if(ls.size+f.cnt>=x) return p;
return find(f.r,x-ls.size-f.cnt);
}
void solve(int x,int y)
{
x--,y++;
x=find(root,x),y=find(root,y);
splay(x,0),splay(y,x);
t[t[t[root].r].l].add^=1;
}
int ask(int x)
{
splay(x,0);
return t[t[root].l].size+1;
}
void solve(int i)
{
int x=i,y=ask(e[i].id);
solve(x,y);
}
signed main()
{
srand(time(0));
insert(1);
read(n);
for(int i=2;i<=n+1;i++)
read(e[i].h),
e[i].id=i,
insert(i);
insert(n+2);
sort(e+2,e+2+n,cmp);
for(int i=2;i<=n+1;i++)
write(ask(e[i].id)-1),putchar(' '),
solve(i);
}

luogu P4309 最长上升子序列

回想最长上升子序列的 DP 式子,dpi=max0<j<i&&aj<ai{dpj}+1

此处由于每次插入的数一定大于之前插入的所有数,所以直接在 0<j<i 中找到最大的 dpj+1 即为 dpi

因为其需要动态插入,可以离线后线段树维护,也可以平衡树动态维护。

对于没一个节点定义 val 表示其子树中最大的 dp 值,dp 表示其自身的 dp 值,由此将子树的信息传递给根节点以便查询。

void pushup(int p)
{
f.val=max({ls.val,rs.val,f.dp});
}

插入操作可以用 fhqsplay 支持。

最长上升子序列
#include<bits/stdc++.h>
// #define int long long
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=1e5+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,root,tot;
struct fhq_treap
{
int ch[2],size,dat,ans,len;
}t[N];
void pushup(int p)
{
f.size=ls.size+rs.size+1;
f.ans=max({ls.ans,rs.ans,f.len});
}
void split(int p,int k,int &x,int &y)
{
if(!p) {x=y=0; return ;}
if(ls.size>=k) y=p,split(f.l,k,x,t[y].l);
else x=p,split(f.r,k-ls.size-1,t[x].r,y);
pushup(p);
}
int merge(int x,int y)
{
if(!x||!y) return x+y;
int p;
if(t[x].dat>t[y].dat) t[x].r=merge(t[x].r,y),pushup(p=x);
else t[y].l=merge(x,t[y].l),pushup(p=y);
return p;
}
void insert(int &p,int k)
{
t[++tot].size=1,t[tot].dat=rand();
int x,y;
split(p,k,x,y);
t[tot].len=t[tot].ans=t[x].ans+1;
p=merge(merge(x,tot),y);
}
signed main()
{
srand(time(0));
read(n);
for(int i=1,x;i<=n;i++)
read(x),
insert(root,x),
write(t[root].ans),puts("");
}

树上哈希

对于每个节点,保存的是其子树的 hash 值。

void pushup(int p)
{
f.hash=ls.hash*b[rs.size+1]+f.val*b[rs.size]+rs.hash;
}

luogu P4036 火星人

利用树上哈希,对于每次询问,二分答案即可。

查询一个区间的 hash 询问,与文艺平衡树类似的,找到这个区间,此时的 hash 值即为所求。

火星人
#include<bits/stdc++.h>
// #define int long long
#define ull unsigned long long
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=3e5+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,m,tot,root;
ull b[N];
char s[N];
struct fhq_treap
{
int ch[2],size,dat;
ull val,hash;
}t[N];
void pushup(int p)
{
f.size=ls.size+rs.size+1;
f.hash=ls.hash*b[rs.size+1]+f.val*b[rs.size]+rs.hash;
}
void split(int p,int k,int &x,int &y)
{
if(!p) {x=y=0; return ;}
if(ls.size>=k) y=p,split(f.l,k,x,t[y].l);
else x=p,split(f.r,k-ls.size-1,t[x].r,y);
pushup(p);
}
int merge(int x,int y)
{
if(!x||!y) return x+y;
int p;
if(t[x].dat>t[y].dat) t[x].r=merge(t[x].r,y),pushup(p=x);
else t[y].l=merge(x,t[y].l),pushup(p=y);
return p;
}
void insert(int &p,int k,int d)
{
t[++tot].val=d,t[tot].size=1,t[tot].dat=rand(),t[tot].hash=d;
int x,y;
split(p,k,x,y);
p=merge(merge(x,tot),y);
}
void change(int &p,int k,int d)
{
t[++tot].val=d,t[tot].size=1,t[tot].dat=rand(),t[tot].hash=d;
int x,y,is;
split(p,k,x,y),split(x,k-1,x,is);
p=merge(merge(x,tot),y);
}
int ask(int p,int ll,int rr)
{
int x,y,is;
ull ans;
split(p,rr,x,y),split(x,ll-1,x,is);
ans=t[is].hash;
p=merge(merge(x,is),y);
return ans;
}
bool check(int x,int y,int mid)
{
return ask(root,x,x+mid-1)==ask(root,y,y+mid-1);
}
int ask(int x,int y)
{
int ll=0,rr=min(n-x+1,n-y+1),mid,ans=0;
while(ll<=rr)
{
mid=(ll+rr)>>1;
if(check(x,y,mid)) ans=mid,ll=mid+1;
else rr=mid-1;
}
return ans;
}
void pre()
{
b[0]=1,b[1]=233;
for(int i=2;i<=N-1;i++) b[i]=b[i-1]*b[1];
}
signed main()
{
srand(time(0));
cin>>s+1; read(m);
n=strlen(s+1);
pre();
for(int i=1;i<=n;i++)
insert(root,i-1,s[i]-'a'+1);
while(m--)
{
char op,d; int x,y;
cin>>op;
if(op=='Q')
{
read(x),read(y);
if(x>y) swap(x,y);
write(ask(x,y)),puts("");
}
if(op=='R')
read(x),cin>>d,
change(root,x,d-'a'+1);
if(op=='I')
read(x),cin>>d,
insert(root,x,d-'a'+1),
n++;
}
}

AtCoder abc331_f Palindrome Query

类似的二分答案即可,需要存正串和反串,同时存在线段树解法,不过多展开。

Palindrome Query
#include<bits/stdc++.h>
// #define int long long
#define ull unsigned long long
#define endl '\n'
#define sort stable_sort
#define f t[p]
#define ls t[t[p].ch[0]]
#define rs t[t[p].ch[1]]
#define l ch[0]
#define r ch[1]
using namespace std;
const int N=2e6+10,inf=0x7f7f7f7f;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,m,root,tot;
ull b[N];
char s[N];
struct fhq_treap
{
int ch[2],val,size,dat;
ull hash[2];
}t[N];
void pushup(int p)
{
f.size=ls.size+rs.size+1;
f.hash[0]=ls.hash[0]*b[rs.size+1]+f.val*b[rs.size]+rs.hash[0];
f.hash[1]=rs.hash[1]*b[ls.size+1]+f.val*b[ls.size]+ls.hash[1];
}
void split(int p,int k,int &x,int &y)
{
if(!p) {x=y=0; return ;}
if(ls.size>=k) y=p,split(f.l,k,x,t[y].l);
else x=p,split(f.r,k-ls.size-1,t[x].r,y);
pushup(p);
}
int merge(int x,int y)
{
if(!x||!y) return x+y;
int p;
if(t[x].dat>t[y].dat) t[x].r=merge(t[x].r,y),pushup(p=x);
else t[y].l=merge(x,t[y].l),pushup(p=y);
return p;
}
void insert(int &p,int k,int d)
{
t[++tot].val=d,t[tot].size=1,t[tot].dat=rand(),t[tot].hash[0]=t[tot].hash[1]=d;
int x,y;
split(p,k,x,y);
p=merge(merge(x,tot),y);
}
void change(int &p,int k,int d)
{
t[++tot].val=d,t[tot].size=1,t[tot].dat=rand(),t[tot].hash[0]=t[tot].hash[1]=d;
int x,y,is;
split(p,k,x,y),split(x,k-1,x,is);
p=merge(merge(x,tot),y);
}
int ask(int p,int ll,int rr,bool d)
{
int x,y,is;
ull ans;
split(p,rr,x,y),split(x,ll-1,x,is);
ans=t[is].hash[d];
p=merge(merge(x,is),y);
return ans;
}
void ask(int ll,int rr)
{
if(ll==rr) {puts("Yes"); return ;}
int mid=(rr-ll+1)>>1;
puts(ask(root,ll,ll+mid-1,0)==ask(root,rr-mid+1,rr,1)?"Yes":"No");
}
void pre()
{
b[0]=1,b[1]=233;
for(int i=2;i<=N-1;i++) b[i]=b[i-1]*b[1];
}
signed main()
{
srand(time(0));
read(n),read(m);
pre();
cin>>s+1;
for(int i=1;i<=n;i++) insert(root,i-1,s[i]-'a'+1);
while(m--)
{
int op,x,y; char d;
read(op);
if(op==1) read(x),cin>>d,change(root,x,d-'a'+1);
else read(x),read(y),ask(x,y);
}
}
posted @   卡布叻_周深  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示