引入
无旋转 $treap$ ,又称分裂合并树,因为其操作由分裂合并实现,代码简单,好调,并且没有旋转操作,可能有时常数略大,但不影响其优秀。
原理
$fhqtreap$ 是以 $BST$ 二叉搜索树为基础实现的
不同于 $BST$ 的是,加入数值时我们保存一个随机 $key$ 值 ,并保证父亲的 $key$ 值大于儿子的 $key$ 值,使得树成为随机情况下的 $BST$ ,树高降至 $\log N$
以下就是一颗 $fhqtreap$

其中红色的值为 $key$ , 黑色的值为数的值 $val$
假如我们查询 $21$ 的排名,就把树分裂成 $<=val$ 的一部分和 $>val$ 的一部分
分裂后:


令第一部分的根节点为 $l$
则 $rank(21)=$ $l$ 的子树大小(包含 $l$ ) $+1=3$
实现
基础操作
结构体:
struct node{
int l,r; //左右儿子
int val,key,s; //数值,随机值,size
};
更新
inline void pushup(int x){ t[x].s=t[t[x].l].s+t[t[x].r].s+1; }
新建节点
inline int newnode(int val){
++tot;
t[tot].l=t[tot].r=0;
t[tot].val=val;
t[tot].key=rnd();
t[tot].s=1;
return tot;
}
其中在代码前加上
std::mt19937 rnd(233);
避免普通 $rand$ 全是 $0$ 导致树被卡成一条链
Split 分裂
void split(int p,int val,int &l,int &r){
if(!p){
l=r=0;
return;
}
if(t[p].val<=val){
l=p;
split(t[p].r,val,t[p].r,r);
}else{
r=p;
split(t[p].l,val,l,t[p].l);
}
pushup(p);
}
其中 $l$ 代表 $<=val$ 树的根节点 , $r$ 代表 $>val$ 树的根节点
递归时如果 $t[p].val <= val$ 就把 $p$ 加到 $l$ 的子树下 , 递归 $t[p].r$
否则就把 $p$ 加到 $r$ 的子树下,递归 $t[p].l$
merge 合并
分裂过后肯定要再合并回去
假设我们现在在合并 $x$ ,$y$
因为 $Split$ 过后 $x$ 的所有节点的值都严格小于 $y$ 的所有节点,所以我们只考虑两种情况
情况一: $t[x].key>t[y].key$

把 $y$ 合并在 $x$ 的右节点
情况二: $t[x].key<=t[y].key$

把 $x$ 合并在 $y$ 的左节点
代码:
int merge(int l,int r){
if(!l||!r) return l|r;
if(t[l].key>t[r].key){
t[l].r=merge(t[l].r,r);
pushup(l);
return l;
}else{
t[r].l=merge(l,t[r].l);
pushup(r);
return r;
}
}
insert 加入
设加入值 $val$
将树分裂成 $<=val$ 和 $>val$
把 $val$ 合并进去就行了
代码:
inline void insert(int val){
int dl,dr;
split(rt,val,dl,dr);
rt=merge(merge(dl,newnode(val)),dr);
return;
}
erase 删除
先保证删除的数一定存在
设删除值 $val$
把树分裂成三个部分 $<val$ 和等于 $=val$ 的以及 $>val$ 三棵树
取出等于 $val$ 的设根为 $temp$
由于可能有多个 $val$ ,我们可以直接做 $temp=merge(t[temp].l,t[temp].r)$
最后再把 $temp$ 合并回去
代码:
inline void erase(int val){
int dl,dr,temp;
split(rt,val,dl,dr);
split(dl,val-1,dl,temp);
temp=merge(t[temp].l,t[temp].r);
rt=merge(merge(dl,temp),dr);
return;
}
rank 排名
直接分裂出 $<val$ 的树,设其根为 $l$
则 $rank(val)=t[l].s+1$
代码:
inline int rank(int val){
int dl,dr;
split(rt,val-1,dl,dr);
int rnk=t[dl].s+1;
rt=merge(dl,dr);
return rnk;
}
rank_find 求排名为k的数
与 $BST$ 的查找相同
若搜索的节点为 $p$ ,如果 $t[t[p].l].s+1=k$ ,返回 $t[p].val$
否则如果 $t[t[p].l].s>=k$ ,递归查找 $p=t[p].l$
否则 $k-=(t[t[p].l].s+1)$ , 递归查找 $p=t[p].r$
代码:
inline int rank_find(int rnk){
int p=rt;
while(true){
if(t[t[p].l].s+1==rnk) break;
else if(t[t[p].l].s>=rnk) p=t[p].l;
else rnk-=t[t[p].l].s+1,p=t[p].r;
}
return t[p].val;
}
pre 最大的数且严格小于val(前驱)
先分裂出 $<val$ 的树,设根为 $l$
因为要求最大,可以一直查找当前右节点直到叶子结点
代码:
inline int pre(int val){
int dl,dr;
split(rt,val-1,dl,dr);
int p=dl;
while(t[p].r) p=t[p].r;
rt=merge(dl,dr);
return t[p].val;
}
suf 最小的数且严格大于val (后继)
同理,分裂出 $>val$ 的树再递归右节点即可
代码:
inline int suf(int val){
int dl,dr;
split(rt,val,dl,dr);
int p=dr;
while(t[p].l) p=t[p].l;
rt=merge(dl,dr);
return t[p].val;
}
实现普通平衡树 P3369
#include<bits/stdc++.h>
#define sf scanf
#define pf printf
#define rep(i,x,y) for(int i=x;i<=y;i++)
#define drep(i,x,y) for(int i=x;i>=y;i--)
#define ll long long
#define pb push_back
#define lb long double
using namespace std;
std::mt19937 rnd(233);
template<int T> struct fhq_treap{
struct node{
int val,key,l,r,s;
}t[T+5];
int tot,rt,size;
fhq_treap(){ tot=0; rt=0; size=0; }
int newnode(int val){
tot++;
t[tot].s=1;
t[tot].l=t[tot].r=0;
t[tot].val=val;
t[tot].key=rnd();
return tot;
}
inline void pushup(int u){ t[u].s=t[t[u].l].s+t[t[u].r].s+1; }
void split(int p,int val,int &l,int &r){
if(!p) {
l=r=0;
return;
}
if(t[p].val<=val){
l=p;
split(t[p].r,val,t[p].r,r);
}else{
r=p;
split(t[p].l,val,l,t[p].l);
}
pushup(p);
}
int merge(int l,int r){
if(!l||!r) return l|r;
if(t[l].key>t[r].key){
t[l].r=merge(t[l].r,r);
pushup(l);
return l;
}else{
t[r].l=merge(l,t[r].l);
pushup(r);
return r;
}
}
inline void insert(int val){
int dl=0,dr=0; size++;
split(rt,val,dl,dr);
rt=merge(merge(dl,newnode(val)),dr);
}
inline void erase(int val){
int dl=0,dr=0,temp=0; size--;
split(rt,val,dl,dr);
split(dl,val-1,dl,temp);
temp=merge(t[temp].l,t[temp].r);
rt=merge(merge(dl,temp),dr);
}
inline int rank(int val){
int dl=0,dr=0;
split(rt,val-1,dl,dr);
int rnk=t[dl].s+1;
rt=merge(dl,dr);
return rnk;
}
inline int rank_find(int rnk){
return get_rank(rt,rnk);
}
int get_rank(int p,int rnk){
if(t[t[p].l].s+1==rnk) return t[p].val;
if(rnk<t[t[p].l].s+1) return get_rank(t[p].l,rnk);
else return get_rank(t[p].r,rnk-t[t[p].l].s-1);
}
inline int pre(int val){
int dl,dr;
split(rt,val-1,dl,dr);
int p=dl;
while(t[p].r) p=t[p].r;
rt=merge(dl,dr);
return t[p].val;
}
inline int suf(int val){
int dl,dr;
split(rt,val,dl,dr);
int p=dr;
while(t[p].l) p=t[p].l;
rt=merge(dl,dr);
return t[p].val;
}
};
fhq_treap<100010> t;
int q;
int main(){
sf("%d",&q);
while(q--){
int opt,x;
sf("%d%d",&opt,&x);
if(opt==1) t.insert(x);
else if(opt==2) t.erase(x);
else if(opt==3) printf("%d\n",t.rank(x));
else if(opt==4) printf("%d\n",t.rank_find(x));
else if(opt==5) printf("%d\n",t.pre(x));
else printf("%d\n",t.suf(x));
}
return 0;
}
加强版代码也差不多,就不给了
实测加强版最慢的点跑了 $906ms$ ,离 $3.0s$ 还是挺远的
P1486 [NOI2004] 郁闷的出纳员
很明显的板子
实现呢我写的是每个点打一个 $tag$ (其实可以不用)
询问到他的时候像线段树一样下传
对于离开的打工人我们只需要在扣工资的时候把值小于 $min$ 的一部分 分裂出来
只留大于等于的即可
代码 :
#include<bits/stdc++.h>
#define ll long long
#define sf scanf
#define pf printf
#define pb push_back
#define cmax(x,y) x=max(x,y);
#define cmin(x,y) x=min(x,y);
#define ull unsigned long long
#define drep(i,x,y) for(int i=x;i>=y;i--)
#define rep(i,x,y) for(int i=x;i<=y;i++)
#define IOS ios::sync_with_stdio(false)
using namespace std;
inline ll in(){ ll x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9') (ch=='-'?f=-1:1),ch=getchar(); while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*f; }
std::mt19937 rnd(233);
ll mi;
template<int T> struct fhq{
struct node{
int l,r,s,key;
ll val,tag;
}t[T+5];
int tot,rt,cnt;
fhq(){
tot=rt=0;
cnt=0;
}
inline int newnode(int val){
tot++;
t[tot].s=1;
t[tot].l=t[tot].r=0;
t[tot].val=val;
t[tot].key=rnd();
t[tot].tag=0;
return tot;
}
inline int pushup(int x){
t[x].s=t[t[x].l].s+t[t[x].r].s+1;
}
inline void down(int x){
if(t[x].tag!=0){
t[t[x].l].val+=t[x].tag;
t[t[x].r].val+=t[x].tag;
t[t[x].l].tag+=t[x].tag;
t[t[x].r].tag+=t[x].tag;
t[x].tag=0;
return;
}
}
void split(int p,ll val,int &l,int &r){
if(!p) {
l=r=0;
return;
}
down(p);
if(t[p].val<=val){
l=p;
split(t[p].r,val,t[p].r,r);
}else{
r=p;
split(t[p].l,val,l,t[p].l);
}
pushup(p);
}
int merge(int l,int r){
if(!l||!r) return l|r;
if(t[l].key>t[r].key){
down(l);
t[l].r=merge(t[l].r,r);
pushup(l);
return l;
}else{
down(r);
t[r].l=merge(l,t[r].l);
pushup(r);
return r;
}
}
inline void insert(ll val){
if(val<mi) return;
int dl,dr;
split(rt,val,dl,dr);
rt=merge(merge(dl,newnode(val)),dr);
}
inline int rank_find(int rnk){
if(rnk>t[rt].s) return -1;
rnk=t[rt].s-rnk+1;
int p=rt,cnt=0;
while(1){
down(p);
if(t[t[p].l].s+1==rnk) return t[p].val;
else if(t[t[p].l].s+1<rnk) rnk-=t[t[p].l].s+1,p=t[p].r;
else p=t[p].l;
}
}
inline void add(ll val){
t[rt].tag+=val;
t[rt].val+=val;
if(val<0){
int dl,dr;
split(rt,mi-1,dl,dr);
cnt+=t[dl].s;
rt=dr;
}
}
};
fhq<300020> t;
int n;
int main(){
n=in(); mi=in();
while(n--){
char op[9];
ll v;
sf("%s",op+1);
sf("%lld",&v);
if(op[1]=='I'){
t.insert(v);
}else{
if(op[1]=='F'){
pf("%d\n",t.rank_find(v));
}else{
if(op[1]=='S') v=-v;
t.add(v);
}
}
}
pf("%d\n",t.cnt);
return 0;
}
P3224 [HNOI2012] 永无乡
简化题意:联通块合并,联通块第 $k$ 大
首先考虑由并查集维护联通块
有个坑点,就是合并两个联通块的时候注意不能直接 $merge$ ,因为 $fhqtreap$ 的 $merge$ 要有 $l$ 中所有节点都小于 $r$ 的节点
所以我们暴力遍历另一个联通块中的数并 $insert$ 到联通块的树里
查询就 $rankfind$ 正常做就行了
但是普通暴力做着是 $n ^ 2 \log n$ 的,考虑启发式合并优化,最后总复杂度
$n \log^2 n$ 常数比较小,开 $O2$ 最慢才 $180ms$
代码:
#include<bits/stdc++.h>
#define ll long long
#define rep(i,x,y) for(int i=(x);i<=(y);i++)
#define drep(i,x,y) for(int i=(x);i>=(y);i--)
#define sf scanf
#define pf printf
#define pb push_back
#define pii pair<int,int>
#define i128 __int128
#define pt putchar
using namespace std;
inline ll in(){ ll x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9') (ch=='-'?f=-1:1),ch=getchar(); while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*f; }
std::mt19937 rnd(233);
template<int T> struct fhq{
struct node{
int l,r,s,val,key;
}t[T+5];
int f[T+5],rt[T+5],id[T+5];
int tot;
fhq(){
tot=0;
rep(i,1,T) rt[i]=f[i]=i;
}
inline int newnode(int val){
t[++tot]={0,0,1,val,rnd()};
return tot;
}
inline void pushup(int x){
t[x].s=t[t[x].l].s+t[t[x].r].s+1;
}
void split(int p,int val,int &l,int &r){
if(!p){
l=r=0;
return;
}
if(t[p].val<=val){
l=p;
split(t[p].r,val,t[p].r,r);
}else{
r=p;
split(t[p].l,val,l,t[p].l);
}
pushup(p);
}
int merge(int l,int r){
if(!l||!r) return l|r;
if(t[l].key>t[r].key){
t[l].r=merge(t[l].r,r);
pushup(l);
return l;
}
t[r].l=merge(l,t[r].l);
pushup(r);
return r;
}
int get(int x){
if(f[x]==x) return x;
return f[x]=get(f[x]);
}
inline void insert(int x,int y){
int dl,dr;
split(rt[x],t[y].val-1,dl,dr);
rt[x]=merge(merge(dl,y),dr);
}
void dfs(int u,int fa){
if(!u) return;
dfs(t[u].l,fa);
int r=t[u].r;
t[u].l=t[u].r=0;
t[u].s=1;
insert(fa,u);
dfs(r,fa);
}
inline void vmerge(int x,int y){
int fx=get(x),fy=get(y);
if(fx==fy) return;
if(t[rt[fx]].s<t[rt[fy]].s) swap(fx,fy);
dfs(fy,fx);
f[fy]=fx;
}
inline int ask(int x,int k){
int fx=rt[get(x)],p=fx;
if(t[fx].s<k) return -1;
while(1){
if(t[t[p].l].s+1==k) return id[t[p].val];
if(t[t[p].l].s+1>k) p=t[p].l;
else k-=(t[t[p].l].s+1),p=t[p].r;
}
}
};
fhq<300030> t;
int n,m;
int main(){
sf("%d%d",&n,&m);
rep(i,1,n){
int x=in();
t.id[x]=i;
t.newnode(x);
}
while(m--){
int u,v;
u=in(); v=in();
t.vmerge(u,v);
}
m=in();
while(m--){
char op[5];
int x,y;
sf("%s",op+1);
x=in();y=in();
if(op[1]=='Q') pf("%d\n",t.ask(x,y));
else t.vmerge(x,y);
}
return 0;
}
代码有点丑,因为一开始题读错了,求的是第 $k$ 大的编号而不是值
实现文艺平衡树
平衡树实现区间操作时存的 $val$ 改成下标
每次翻转分裂出 $l$ ~ $r$ 区间并打上翻转标记,在 $merge$ 时下传即可
具体看实现:
#include<bits/stdc++.h>
#define sf scanf
#define pf printf
#define rep(i,x,y) for(int i=x;i<=y;i++)
#define drep(i,x,y) for(int i=x;i>=y;i--)
#define ll long long
#define pb push_back
#define lb long double
using namespace std;
inline ll in(){ ll x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9') (ch=='-'?f=-1:1),ch=getchar(); while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*f; }
std::mt19937 rnd(233);
template<int T> struct fhq{
struct node{
int l,r,s,val,key;
int rev;
}t[T*2+5];
int tot=0,rt=0;
inline int newnode(int val){
t[++tot]={0,0,1,val,rnd(),0};
return tot;
}
inline void pushup(int x){
t[x].s=t[t[x].l].s+t[t[x].r].s+1;
}
inline void down(int x){
if(t[x].rev){
swap(t[x].l,t[x].r);
t[t[x].l].rev^=1;
t[t[x].r].rev^=1;
t[x].rev=0;
return;
}
}
void split(int p,int val,int &l,int &r){
if(!p){
l=r=0;
return;
}
down(p);
if(t[t[p].l].s+1<=val){
l=p;
split(t[p].r,val-t[t[p].l].s-1,t[p].r,r);
}
else{
r=p;
split(t[p].l,val,l,t[p].l);
}
pushup(p);
}
int merge(int l,int r){
if(!l||!r) return l|r;
if(t[l].key<t[r].key){
down(l);
t[l].r=merge(t[l].r,r);
pushup(l);
return l;
}
down(r);
t[r].l=merge(l,t[r].l);
pushup(r);
return r;
}
inline void insert(int val){
int dl,dr;
split(rt,val-1,dl,dr);
rt=merge(merge(dl,newnode(val)),dr);
}
inline void reverse(int l,int r){
int dl,temp,dr;
split(rt,l-1,dl,dr);
split(dr,r-l+1,temp,dr);
t[temp].rev^=1;
rt=merge(merge(dl,temp),dr);
}
inline void dfs(int u){
if(!u) return;
down(u);
dfs(t[u].l);
pf("%d ",t[u].val);
dfs(t[u].r);
}
};
fhq<100005> t;
int n,m;
int main(){
n=in(); m=in();
rep(i,1,n) t.insert(i);
rep(i,1,m) {
int l=in(),r=in();
t.reverse(l,r);
}
t.dfs(t.rt); pf("\n");
return 0;
}
闲话
考试的时候双 $split$ 的 $fhq$ 花了 $2h$ 没调出来,等我调出来了继续更双 $split$
$$\large-- End --$$