浅谈 FHQ-Treap

本文同步发表在洛谷博客


什么是 FHQ-Treap?

平衡树上存放两个信息,权值 \(val\) 以及随机索引 \(key\)。值满足二叉搜索树性质,随机值索引满足堆的性质,通过结合二叉搜索树和二叉堆的性质来使树平衡。至于这里用的是大根堆还是小根堆,不重要。

当权值 \(val\) 的数值情况不可控时,如果保证索引 \(key\) 为随机,树的期望深度为 \(\log n\)

通常的平衡树维护平衡的方法是旋转,而 FHQ-Treap 则是用分裂和合并来实现的。

详解 FHQ-Treap

FHQ-Treap 存放的节点信息

首先,有必不可少的权值 \(val\) 和随即索引 \(key\)。其次为了维护树的结构,肯定要有左节点编号 \(ls\) 以及右节点编号 \(rs\)。当然,为了求排名之类的东西,我们还会维护一个子树大小 \(sz\)。由于要维护的信息较多,这里采用结构体进行维护。

struct FHQ_Trp{int ls,rs,val,key,sz;}tree[N];

当然,你还需要 \(cnt\)\(root\),分别表示当前节点个数(也就是最大的编号)以及当前这棵平衡树的根节点 \(root\)

FHQ-Treap 需要用到的操作

创建新节点

很简单。首先将 \(cnt \gets cnt+1\) 然后让这个节点的编号为 \(cnt\)。给定这个节点的 \(val\) 值,为传参进来的 \(k\),然后再用 mt19937 随机生成索引 \(key\),以及要记得初始化子树大小为 \(1\) 哦。

mt19937 num(998244353);
int New_node(int k){
    ++cnt;
    tree[cnt].val=k;
    tree[cnt].key=num();
    tree[cnt].sz=1;
    return cnt;
}

分裂操作

分裂,即 Split,是 FHQ-Treap 中必不可少的一环。

分裂方法有两种:

  1. 按值分裂:把树拆成两棵树,拆出来的一棵树的值全部小于等于给定的值,另外一棵树的值全部大于给定的值。
  2. 按大小分裂:把树拆成两棵树,拆出来的一棵树的值全部等于给定的大小,剩余部分在另外一颗树里。

通常来说,当 FHQ-Treap 作为正常平衡树使用时,采用按值分裂;维护区间信息时,用按大小分裂。

这里用按值分裂举例。其实很简单的,根据二叉搜索树的性质,如果当前节点的权值小于等于这个分裂的值,那么左子树肯定都稳了,可右子树还确定不了,因此这个时候就往右边递归;同理,如果当前节点的权值大于这个分裂的值,那么右子树也肯定都稳了,左子树不确定,这个时候就往左递归。更新迭代的时候不要忘了更新分裂出来的两棵树的情况,以及它们的根哦。当然,在更新完成之后需要对当前节点进行一次 upd,因为树的结构会随着分裂的操作而变化,子树大小需要重新维护。

void upd(int u){
    tree[u].sz=tree[tree[u].ls].sz+tree[tree[u].rs].sz+1;return;
}
void Split(int u,int k,int &x,int &y){
    if(!u){x=0,y=0;return;}
    else if(tree[u].val<=k){
        x=u;
        Split(tree[u].rs,k,tree[u].rs,y);
    }else{
        y=u;
        Split(tree[u].ls,k,x,tree[u].ls);
    }upd(u);return;
}

合并操作

合并操作,很简单的,就是把两棵分别以 \(x\)\(y\) 为根的平衡树合并在一起,并要求合并之后仍然满足平衡树的性质。在这里的合并就和之前的分裂不一样了,分裂的时候重点运用的是二叉搜索树的性质,而这里需要重点运用二叉堆的性质,去选择维护的方向。

int Merge(int x,int y){
    if(!x||!y)return x+y;
    if(tree[x].key>tree[y].key){
        tree[x].rs=Merge(tree[x].rs,y);
        upd(x);return x;
    }else{
        tree[y].ls=Merge(x,tree[y].ls);
        upd(y);return y;
    }
}

值得注意的是,当你的平衡树的分裂操作做的是按大小分裂的时候,即你维护区间情况的时候,一定要注意,\(\text{Merge}(x,y)\)\(\text{Merge}(y,x)\) 是在干两件不同的事情哦!本人被狠狠坑过。

如何维护 FHQ-Treap

插入节点

假设我们要插入的节点的权值为 \(k\)。首先我们要分裂这棵平衡树,把它分裂成两个部分,一个部分的权值 \(\le k\),还有一个部分的权值 \(>k\),即执行 \(\text{Split}(root,k,x,y)\) 操作,其中 \(x\)\(y\) 是你设定的两个变量,用于存储分裂后的树的两个根节点。接着,我们考虑新建一个节点村上这个要插入的值,然后将其和以 \(x\) 为根的树合并,最后再和以 \(y\) 为根的树合并。这样,你就完美地实现了一个插入的过程!

void Insert(int k){
    int x=0,y=0;
    Split(root,k,x,y);
    root=Merge(Merge(x,New_node(k)),y);
    return;
}

删除节点

同样假设我们要删除的节点的权值为 \(k\)。首先,仍然对树进行分裂,不过这一次我们将把树分裂成三部分:以 \(x\) 为根节点的部分,所有节点权值 \(< k\);以 \(y\) 为根节点的部分,所有节点权值 \(=k\);以 \(z\) 为根节点的部分,所有节点权值 \(>k\)。这个时候,我们只需要从以 \(y\) 为根节点的树里面删掉一个节点,最后把大家全都合并起来就可以了。如何删掉 \(y\) 中的一个节点?很简单,由于权值都是 \(k\),没多大区别,因此直接把根节点删掉,让 \(y\) 的左子树和右子树合并起来就行了。如果题目特殊要求,说如果存在多个全部删掉,那么最后就只需要合并 \(x\)\(z\) 了,\(y\) 直接扔掉不管就行。

void Delete(int k){
    int x=0,y=0,z=0;
    Split(root,k,x,z);
    Split(x,k-1,x,y);
    y=Merge(tree[y].ls,tree[y].rs);
    root=Merge(Merge(x,y),z);
    return;
}

给定数值查询排名

即根据具体值的大小情况查询排名,如有重复则存在并列情况。假设这个值为 \(k\)

很简单啦,同样对树进行分裂,不同于以往的 \(\le k\) 以及 \(> k\),这次为了方便求排名我们是按照 \(<k\)\(\ge k\) 去分裂的,然后得到 \(<k\) 的这一部分树的大小,加上一就是答案。最后记得要把树合并回来。

int Get_rank(int k){
    int x=0,y=0,rk=0;
    Split(root,k-1,x,y);
    rk=tree[x].sz+1;
    Merge(x,y);return rk;
}

给定排名查询数值

很简单,从根节点开始往下搜索,如果左子树大小 \(+1\) 恰好等于传参进来的 \(rk\)(也就是排名),那么说明当前 \(u\) 就是答案,直接返回即可;否则根据左子树的大小是否足够,判断现在 \(u\) 是移动向左子树还是右子树,同时如果往右子树移的话要注意将 \(rk\) 的值减少对应数量。如果到最后都还没找到就返回 \(-1\),但除非这个排名越界了,否则都是会找到的啦!

int Get_number(int rk){
    int u=root;
    while(u){
        if(tree[tree[u].ls].sz+1==rk)return tree[u].val;
        else if(tree[tree[u].ls].sz>=rk)u=tree[u].ls;
        else rk-=tree[tree[u].ls].sz+1,u=tree[u].rs;
    }return -1;
}

查询数值前驱

数值 \(k\) 的前驱定义为最大的 \(<k\) 的数的具体值,\(k\) 不一定要在树中出现过。

依然很简单,与上面相同的,把树拆成 \(< k\)\(\ge k\) 的两部分,然后从 \(<k\) 的那部分树的根节点找起,只要有右节点,就一直往右边跑(因为这样才能得到最大值),直到没得跑了,直接返回就行了,不过要注意把树合并回来。

int Find_pre(int k){
    int x=0,y=0;Split(root,k-1,x,y);
    int u=x;while(tree[u].rs)u=tree[u].rs;
    Merge(x,y);return u;
}

查询数值后继

数值 \(k\) 的后继定义为最小的 \(>k\) 的数的具体值,\(k\) 不一定要在树种出现过。

和查询前驱是一样的,不同的是,这里是把树拆成 \(\le k\)\(>k\) 两部分,然后从 \(>k\) 的那部分树的根节点找起。当然了,这里是一直往左边跑,因为这样才能得到最小值嘛!同样要记得把树合并回来。

int Find_nxt(int k){
    int x=0,y=0;Split(root,k,x,y);
    int u=y;while(tree[u].ls)u=tree[u].ls;
    Merge(x,y);return u;
}

【模版】普通平衡树

来看板子题,https://www.luogu.com.cn/problem/P3369

由于其要求支持插入、删除、查询排名、查询数值、查询前驱后继这些操作,正好就是上面所提到的,所以把上面的那些零零散散的代码拼在一起就过了!

为了方便我还是放个完整代码在这里吧。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
using namespace std;
const int N = 1e5+5;
mt19937 num(998244353);
int Q,cnt,root;
struct FHQ_Trp{int ls,rs,val,key,sz;}tree[N];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
int New_node(int k){
    ++cnt;
    tree[cnt].val=k;
    tree[cnt].key=num();
    tree[cnt].sz=1;
    return cnt;
}
void upd(int u){
    tree[u].sz=tree[tree[u].ls].sz+tree[tree[u].rs].sz+1;return;
}
void Split(int u,int k,int &x,int &y){
    if(!u){x=0,y=0;return;}
    else if(tree[u].val<=k){
        x=u;
        Split(tree[u].rs,k,tree[u].rs,y);
    }else{
        y=u;
        Split(tree[u].ls,k,x,tree[u].ls);
    }upd(u);return;
}
int Merge(int x,int y){
    if(!x||!y)return x+y;
    if(tree[x].key>tree[y].key){
        tree[x].rs=Merge(tree[x].rs,y);
        upd(x);return x;
    }else{
        tree[y].ls=Merge(x,tree[y].ls);
        upd(y);return y;
    }
}
void Insert(int k){
    int x=0,y=0;
    Split(root,k,x,y);
    root=Merge(Merge(x,New_node(k)),y);
    return;
}
void Delete(int k){
    int x=0,y=0,z=0;
    Split(root,k,x,z);
    Split(x,k-1,x,y);
    y=Merge(tree[y].ls,tree[y].rs);
    root=Merge(Merge(x,y),z);
    return;
}
int Get_rank(int k){
    int x=0,y=0,rk=0;
    Split(root,k-1,x,y);
    rk=tree[x].sz+1;
    Merge(x,y);return rk;
}
int Get_number(int rk){
    int u=root;
    while(u){
        if(tree[tree[u].ls].sz+1==rk)return tree[u].val;
        else if(tree[tree[u].ls].sz>=rk)u=tree[u].ls;
        else rk-=tree[tree[u].ls].sz+1,u=tree[u].rs;
    }return -1;
}
int Find_pre(int k){
    int x=0,y=0;Split(root,k-1,x,y);
    int u=x;while(tree[u].rs)u=tree[u].rs;
    Merge(x,y);return u;
}
int Find_nxt(int k){
    int x=0,y=0;Split(root,k,x,y);
    int u=y;while(tree[u].ls)u=tree[u].ls;
    Merge(x,y);return u;
}
int main(){
    Q=read();
    while(Q--){
        int opt=read(),x=read();
        if(opt==1)Insert(x);
        else if(opt==2)Delete(x);
        else if(opt==3)cout<<Get_rank(x)<<"\n";
        else if(opt==4)cout<<Get_number(x)<<"\n";
        else if(opt==5)cout<<tree[Find_pre(x)].val<<"\n";
        else cout<<tree[Find_nxt(x)].val<<"\n";
    }
    return 0;
}

【模版】文艺平衡树

就是 https://www.luogu.com.cn/problem/P3391 啦!

这里维护的是区间,所以在上面分裂那里要按大小分裂。

问题在于……不管你维护的什么,这里的翻转操作我也不会弄啊!不过其实没多难,就是把左右子树交换 swap 一下。

可是这么暴力弄都是 \(O(n \times m)\) 的,不超时才怪呢。咋搞?

还记得线段树是如何让区间操作的时间复杂度降下来的?你说对了——懒标记!这里也是一样的道理!

在结构体 FHQ_Trp 里多加 \(tag\) 一维,表示这里的标记,每次需要修改,我们就把 \(tag\) 进行反转。涉及子树修改的情况下,我们再进行 push_down 下传标记。

当然了,我们要进行拆树(不然你给谁标记呢?),拆成 \([1,l-1]\)\([l,r]\)\([r+1,n]\) 三个部分,然后需要翻转的当然就是中间 \([l,r]\) 的那一部分啦!最后别忘了并回去。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
using namespace std;
const int N = 1e5+5;
mt19937 num(998244353);
int n,m,cnt,root;
struct FHQ_Trp{int ls,rs,val,key,sz;bool tag;}tree[N];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
int New_node(int k){
    ++cnt;
    tree[cnt].val=k;
    tree[cnt].key=num();
    tree[cnt].sz=1;
    return cnt;
}
void push_down(int u){
    if(!tree[u].tag)return;
    swap(tree[u].ls,tree[u].rs);
    tree[tree[u].ls].tag^=1;
    tree[tree[u].rs].tag^=1;
    tree[u].tag=0;return;
}
void upd(int u){
    tree[u].sz=tree[tree[u].ls].sz+tree[tree[u].rs].sz+1;return;
}
void Split(int u,int k,int &x,int &y){
    if(!u){x=0,y=0;return;}
    push_down(u);
    if(tree[tree[u].ls].sz+1<=k){
        x=u;
        Split(tree[u].rs,k-tree[tree[u].ls].sz-1,tree[u].rs,y);
    }else{
        y=u;
        Split(tree[u].ls,k,x,tree[u].ls);
    }upd(u);return;
}
int Merge(int x,int y){
    if(!x||!y)return x+y;
    if(tree[x].key>tree[y].key){
        push_down(x);
        tree[x].rs=Merge(tree[x].rs,y);
        upd(x);return x;
    }else{
        push_down(y);
        tree[y].ls=Merge(x,tree[y].ls);
        upd(y);return y;
    }
}
void Insert(int k){
    int x=0,y=0;
    Split(root,k,x,y);
    root=Merge(Merge(x,New_node(k)),y);
    return;
}
void Delete(int k){
    int x=0,y=0,z=0;
    Split(root,k,x,z);
    Split(x,k-1,x,y);
    y=Merge(tree[y].ls,tree[y].rs);
    root=Merge(Merge(x,y),z);
    return;
}
void Sol(int l,int r){
    int x=0,y=0,z=0;
    Split(root,r,x,z);
    Split(x,l-1,x,y);
    tree[y].tag^=1;
    root=Merge(Merge(x,y),z);
    return;
}
void DFS(int u){
    if(!u)return;
    push_down(u);
    DFS(tree[u].ls);
    cout<<tree[u].val<<" ";
    DFS(tree[u].rs);
    return;
}
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;i++)Insert(i);
    for(int i=1;i<=m;i++){
        int l=read(),r=read();
        Sol(l,r);
    }DFS(root);cout<<"\n";
    return 0;
}
posted @ 2025-10-29 17:44  嘎嘎喵  阅读(17)  评论(0)    收藏  举报