FHQ_Treap 基础
一, 原理介绍
- 免旋,保存原结构的基础上进行增删,使得持久化成为可能
- 核心操作: split,merge (有点类似于左偏树)
- 把树按照某种标准(子树大小和特定权值)分成几部分,对它(需要操作的分裂子树)进行操作
- 与traep的联系:按照随机权值合并子树,(但是可是)只有一处用到随机化
二,关键操作
1,节点创建:create
struct node
{
int l,r;
int key,siz;
ll sum,v;
#define t(x) tr[x]
#define ls(x) tr[x].l
#define rs(x) tr[x].r
void init (ll val)
{
v=sum=val;
siz= 1;
l=r=0;
}
}
int create (ll val)
{
t(++tot).init (val);
return tot;
}
2,节点复制(不仅仅是权值):copy
int copy (int tar)
{
t(++tot) = t(tar);
return tot;
}
3,上传:up
void up(int x)
{
t(x).siz = t((ls(x))).siz + t(rs(x)).siz +1;
t(x).sum = t(ls(x)).sum + t(rs(x)).sum + t(x).v;
}
4,下传:down
void down(int x)
{
if(t(x).tag==0)return;
if(ls(x)) ls(x)= copy(ls(x));
if(rs(x)) rs(x)= copy(rs(x));
swap(ls(x),rs(x));
if(ls(x)) t(ls(x)).tag ^= 1;
if(rs(x)) t(rs(x)).tag ^= 1;
t(x).tag = 0;
}
5,分裂子树:split
- 将原区间 ( l , r ) (l,r) (l,r)分裂成 ( l , q u e r y v a l ) (l,queryval) (l,queryval)和 ( q u e r y v a l + 1 , r ) (queryval+1,r) (queryval+1,r)两个区间树
- x x x 就是左区间树的根节点, y y y 就是右区间树的根节点
- 不论 x x x, y y y的初始值是什么,因为分裂操作是用的 & \& & 这一可以赋值的变量
- 最终的 x x x 不是空节点0就是左区间树的根节点, y y y 不是空节点 0 就是右区间树的根节点。
void split (int p,int k,int &x,int &y)
{
if(p==0){x=y=0; return ;}
down(p);
if(t(ls(p)).siz <k)
{
x= copy (p);
split (rs(x),k-t(ls(p)).siz-1,rs(x),y);
up(x);
}
else
{
y= copy (p);
split (ls(y),k,x,ls(y));
up(y);
}
}
void split (int p,int val,int &x,int &y)
{
if(p==0) {x=y= 0;return;}
if(t(p).v<=val) x=p ,split(rs(x),val,rs(x),y);
else y=p ,split(ls(y),val,x,ls(y));
up(p);
}
6,合并子树:merge
- 保证树上靠左的在函树的左参数,右边亦之(相对位置对应)
- 注意返回值要返回~!!!
int merge (int x,int y)
{
if(x==0 || y==0)return x+y;
down(x); down(y);
if(t(x).key<t(y).key)
{
rs(x)= merge (rs(x),y);
up(x);
return x;
}
else
{
ls(y)= merge (x,ls(y));
up(y);
return y;
}
}
四,可持久化
思想概述:
- 根数组,每一个版本开一个根,没变就复制,否则开新的根
- 子结构不变: 保证树的空间结构在不同版本是一定的,(无旋)
- 动态开点(这是一种思维):改了哪个点,就把从这个点到根的所有包涵的节点开新的备份并修改成需要的,其余的保持不变
实现技巧:
- 带根修改法:传根的参的时候带引用,修改扩入函数
- 复制待修改的节点,在赋值到特定对象,最后借助复制的带修节点更新子树信息
一,可持久数组
- 只修不删,直接借助主席树思路
- 抛弃key的堆性质,只要子树大小(位置)和值
i,建树(主席树在二叉树的拓展)
int build (int l,int r)
{
if(l>r)return 0;
int mid = (l+r)>>1;
int ret=++tot;
t(ret).init(a[mid]);
ls(ret)=build (l,mid-1);
rs(ret)=build (mid+1,r);
up(ret);
return ret;
}
ii,修改
- 思路理解版
- nt(新的节点),ot(旧的递归到的,需要修改和复制的)
void modify(int ot,int &nt,int k,int val)
{
nt=++tot;
t(nt).v= t(ot).v;
t(nt).siz= t(ot).siz ;
if(tls(ot).siz+1==k)
{
t(nt).v= val;
ls(nt)=ls(ot);
rs(nt)=rs(ot);
return ;
}
else if(tls(ot).siz>=k)
{
rs(nt)=rs(ot);
modify(ls(ot),ls(nt),k,val);
}
else
{
ls(nt)=ls(ot);
modify(rs(ot),rs(nt),k-tls(ot).siz-1,val);
}
}
- 竞赛集成版:
void modify(int ot,int &nt,int k,int val)
{
nt=copy_node (ot);
if(tls(nt).siz+1==k)
{
t(nt).v=val;
return;
}
else if(tls(nt).siz>=k) modify(ls(nt),ls(nt),k,val);
else modify(rs(nt),rs(nt),k-tls(nt).siz-1,val);
}
二,普通平衡树,文艺平衡树
- 特点: 带split和merge,注意复制节点(两个函数都要)
AC 可持久普通平衡树
AC 可持久文艺平衡树
void split (int p,int val,int &x,int &y)
{
if(p==0)
{
x=y= 0;
return;
}
p=copy_node (p);
if(t(p).v<=val)
{
x=p;
split(rs(x),val,rs(x),y);
up(x);
}
else
{
y=p;
split(ls(y),val,x,ls(y));
up(y);
}
}
int merge (int x,int y)
{
if(x==0||y==0)return x+y;
if(t(x).key<t(y).key)
{
x=copy_node (x);
rs(x)=merge(rs(x),y);
up(x);
return x;
}
else
{
y=copy_node (y);
ls(y)= merge(x,ls(y));
up(y);
return y;
}
}
小技巧
1,笛卡尔线性建树
- 维护treap的最右链,保证堆性质,我们考虑按照序列从左到右的顺序,也是中序遍历的顺序加入点,那么加入之后这个点一定会在最右链上。
- 那么我们考虑用栈存储这个最右链,那么我们需要一直弹栈,直到栈空或者找到新加点的修正值大于栈顶的修正值,即满足小根堆的性质。
- 弹出的部分最堆的时候需要在当前节点以下,在遍历的时候要在当前节点的前面,所以接到左子树
- 新进的节点必然比栈顶key大,且在后面被中序遍历,直接接到右子树
int build()
{
int last,x,top;
top=0;
for(int i=1;i<=n;i++)
{
x=new_node(a[i]);last =0;
while(top && t(s[top]).v>t(x).v)
{
up(last = s[top]);
s[top--]=0;
}
if(top) t(s[top]).r= x;
t(x).l = last ; s[++top]= x;
}
while (top) up(s[top--]);
return s[1]; //返回根节点
}