【Study】FHQ-Treap

其实是复习笔记(

【模板】普通平衡树

简介

\(FHQ-Treap\) 是平衡树的一种,主要沿袭了 \(Treap\) 的思路,与其他平衡树不同的是它并非进行旋转操作来维护树的平衡,而是进行分裂和合并的操作,因此又称非旋 \(Treap\)

优点:码量少,易理解,灵活

缺点:常数略大

实现

先说明一点东西:

int rt;//根节点
int L,R;//后面会提到
int cnt;//节点编号
struct node{
    int l,r;//左右儿子
    int val;//当前节点的权值
    int key;//随机生成的附属值
    int siz;//子树(包括自己)的节点数量
}t[MAXN];

1、新建节点,维护节点数

无需多言。

int Addn(int a)
{
    t[++cnt].siz=1;
    t[cnt].val=a;
    t[cnt].key=rand();
    return cnt;
}

#define ls t[u].l
#define rs t[u].r 
void Pushup(int u)
{
	t[u].siz=t[ls].siz+t[rs].siz+1;
}

2、分裂(\(Split\)

主要思想就是将一棵树分为两颗

\(Split\) 有两种类型,分为 按 \(val\) 分裂 和 按 \(siz\) 分裂 两种。

(对于上面的模板题需要按 \(val\) 分裂)

这里仔细将按 \(val\) 分裂

首先我们需要定义 \(L,R\) , 分别表示左右子树

规定一个关键值 \(k\) ,将 \(val\) 小于等于 \(k\) 的节点放到左子树,将 \(val\) 大于 \(k\) 的放到右子树

代码如下:

#define ls t[u].l
#define rs t[u].r 
void Split(int u,int k,int &L,int &R)
//u表示当前节点,L和R分别表示左右子树所新增的节点,但注意,此处的L、R仍不存在,为虚拟节点
//注意此处需要引用 & ,才能将虚拟节点修改为真正的节点

{
    if(!u){L=R=0;return;}
    //若当前节点不存在,便无需加上新的节点
    
    if(t[u].val<=k) L=u,split(rs,k,rs,R);    
    //如果当前节点的val小于等于k,则将u及u的左子树放入L,继续遍历u的右子树
    //但是右子树可能会有节点的val小于等于k且大于t[u].val,所以u的右儿子可能需要修改,于是有split(_,_,rs,_)
    //假如t[rs].val仍然小于等于k,则L=u,发现实际对于上一层遍历就是将t[u].rs修改为t[u].rs
    
    else R=u,split(ls,k,L,ls);    
    //如果当前节点的val大于k,则将u及u的右子树放入R,继续遍历u的左子树
    //剩下的其实同理
    
    pushup(u);
}

对于 int &L,int &R 的理解是个难点也是关键点,其实配合图片进行理解会较容易,比如这篇
洛谷日报
对分裂和合并的操作有详细的图解。

对于按 \(siz\) 分裂也是类似的,这里就不细讲了qwq

#define ls t[u].l
#define rs t[u].r 
void Split(int u,int k,int &L,int &R)
{
    if(!u){L=R=0;return;}
    if(t[ls].siz+1<=k) L=u,split(rs,k-t[ls].siz-1,rs,R);
    else R=u,split(ls,k,L,ls);
    pushup(u);
}

2、合并(\(Merge\)

理解了 \(Split\) 操作后对于 \(Merge\) 操作也就不难理解了

此时就是需要将分裂出来的 \(L,R\) 进行合并,因为两颗子树都满足 \(Treap\) 的性质,我们就仅需对比此时两颗需合并子树的 \(key\) 值,进行合并,来维护合并后的新树的平衡

可以结合代码进行理解:

int Merge(int L,int R)
{
    if(!L||!R) return L|R;
    //如果其中一颗子树为空,则直接合并

    if(t[L].key<=t[R].key)
    //如果左子树的根的key小于等于右子树的根的key

    {
        t[L].r=merge(t[L].r,R);
        //则将左子树的右子树与右子树进行合并,以保证小根堆的性质
        //其实左子树的左子树一定小于右子树,并且左子树的左子树已经满足小根堆的性质,就无需进行修改
        //这里有点绕qwq

        pushup(L);
        //因为处理的是左子树,进行更新

        return L;
        //返回根节点
    }
    else
    {
        //接下来均同理
        t[R].l=merge(L,t[R].l);
        pushup(R);
        return R;
    }
}

理解掌握了 \(Split\)\(Merge\) , 接下来的其实就很容易了

3、插入

void Insert(int a)
{
    Split(rt,a,L,R);
    rt=merge(merge(L,addn(a)),R);
}

挺好理解,将整棵树拆为 节点小于等于插入值的子树 和 节点大于插入值的子树 两部分,再依次将子树和节点、子树和子树进行合并。

4、删除

int x;
void Delete(int a)
{
    split(rt,a,L,R);
    split(L,a-1,L,x);
    rt=merge(L,R);
}

这里的 \(x\)\(L\)\(R\) 同种作用,在这里辅助处理

这个看代码就能理解的吧qwq

但是在上面的模板题,里面的删除操作要求删除一个,而我们这里对所有权值为 \(a\) 的节点都进行删除了,所以需要小修改一下

int x;
void Delete(int a)
{
    split(rt,a,L,R);
    split(L,a-1,L,x);
    x=merge(t[x].l,t[x].r);
    //抛弃这个节点,将这个节点的左右子树进行合并
    rt=merge(merge(L,x),R);
}

5、查询指定数的排名

int Findrank(int a)
{
    split(rt,a-1,l,r);
    int ans=t[l].siz+1;
    rt=merge(l,r);
    //注意查询完要合并qwq
    return ans;
}
排名定义为比当前数小的数的个数 +1 

根据题目定义直接找

6、查询指定排名的数

#define ls t[u].l
#define rs t[u].r 
int Findkth(int u,int k)
{
    while(1)
    {
        if(k<=t[ls].siz) u=ls;
        else if(k==t[ls].siz+1) return t[u].val;
        else k=k-t[ls].siz-1,u=rs;
        //注意这里k要减去左边的节点数
    }
}

qwq看代码吧

7、查询前驱/后继


int Findpre(int a)
{
    split(rt,a-1,l,r);
    int ans=findkth(l,t[l].siz);
    rt=merge(l,r);
    return ans;
}

int Findsuf(int a)
{
    split(rt,a,l,r);
    int ans=findkth(r,1);
    rt=merge(l,r);
    return ans;
}

对于前驱,分裂出值小于 \(a\) 的子树,在查询这个子树的最大值。

因为这颗子树满足Treap性质所以只要找这个子树对最后一名就行。

对于后驱,同理qwq

总结

到这里我们就真正处理完一颗 \(FHQ-Treap\) 的基本操作。

可以发现 \(FHQ-Treap\) 是可以对区间进行处理的,假如我们要处理区间 \(l-r\) ,只需把整棵树分裂为 \(1-r\)\((r+1)-n\) ,再将 \(1-r\) 分裂为 \(1-(l-1)\)\(l-r\) ,然后对分裂出来的区间进行乱搞就行了qwq

所以 \(FHQ-Treap\) 的灵活之处就在此,你可以随心所欲地对某些东西进行十分轻松的维护处理,这使 \(FHQ-Treap\) 在很多题目上都能大显神通 qwq

(但是在开心地刷题之余记得对常数进行优化qwq

参考资料:

洛谷日报第289期 [万万没想到]FHQ-Treap学习笔记

posted @ 2021-09-20 14:25  zhangjiayang  阅读(76)  评论(0编辑  收藏  举报
浏览器标题切换
浏览器标题切换end