平衡树1:无旋treap

无旋 treap

其实早就学了,但是忘了。既然复习了,顺便写一个笔记,下次遗忘时能够快速回忆吧。

简介

前置:平衡树定义、堆。

treap 是一种基于随机化实现的弱平衡平衡树,通过对原二叉搜索树的每一个点赋一个随机点权 \(key\),并使 \(key\) 满足堆的形式,来控制原二叉搜索树的子孙关系,达成期望 \(O(logn)\) 的深度。无旋 treap 是 treap 的一个版本,基于分裂和合并。

操作(与基础函数们)

其中,分裂(split)、合并(merge)和查找(find)为基础操作。其余操作可以由这三者完成。

分裂(split)

这里的分裂统一为按值(\(val\))分裂,按照排名分裂之类可以由基本操作完成,具体请看后面。

首先,确认我们的函数大致结构:我们要做的就是类似于可持久化数据结构一样,不断将原 treap 的子树连到分裂后的两颗 treap 上。

考虑目前考虑到了根节点为 \(root\),分裂后的两个 treap 在 \(root\) 的子树内的“预设”根节点(或者说需要移植的左/右子树)为 \(lf\)\(rg\)

假设一颗这颗 treap 的根节点要被分到左边(具体根据排名、值或者其他的,由你的 check 判断),则将 \(lg\) 赋值为 \(root\)(即类似可持久化数据结构一样直接用 \(root\) 的信息),然后把 \(root\) 的右子树给断开,然后去右子树上递归。其中 \(lf\) 变为现在 \(lf\) 的右子树(即原本 \(root\) 的右子树指针),\(rg\) 不变。

根节点在右边的情况同理。

void split(int root,int k,int &lf,int &rg)//拆分 
{
    if(!root) lf=rg=0;
    else
    {
        if(t[root].val<=k) lf=root,split(t[root].rc,k,t[root].rc,rg);
        else rg=root,split(t[root].lc,k,lf,t[root].lc);
        update(root);
    }
}

合并(merge)

……想不到什么要写的了。就是简单的模拟堆的合并即可。别把二叉搜索树的左右子树弄反了就行。

int merge(int x,int y)//合并 
{
    if(!x||!y) return x+y;
    if(t[x].key<t[y].key)
    {
        t[x].rc=merge(t[x].rc,y);
        update(x);
        return x;
    }
    else
    {
        t[y].lc=merge(x,t[y].lc);
        update(y);
        return y;
    }
}

查询指定排名的 \(val\)(find)

类似的,直接递归即可,十分显然。由于众所周知的递归大常数,这里写成循环。

int find(int root,int k)//查找第 x 个数 
{
    while(1)
    {
        if(k<=t[t[root].lc].siz) 
        {
            root=t[root].lc;
        }
        else if(k==t[t[root].lc].siz+1) 
        {
            return root;
        }
        else 
        {
            k-=t[t[root].lc].siz+1;
            root=t[root].rc;
        }
    }
}

至此,三个基础函数介绍完毕。

插入(insert)

先根据要插入的值找到将它插在哪里,然后根据那个地方 split,然后按顺序 merge 起来即可。

void insert(int a,int id,int &root)
{
    int x,y;
    split(root,a,x,y);
    root=merge(merge(x,New(id,a)),y);
}

删除(del)

和插入类似,将插入操作反过来即可。

具体的,先把拆成三部分——左边的,要删的和右边的,再把左边的和右边的拼起来即可。类似的,如果想要区间删除也可以,这里写的是单点删除。

void del(int a,int &root)
{
    int x,y,z; 
    split(root,a,x,z);
    split(x,a-1,x,y);
    y=merge(t[y].lc,t[y].rc);
    root=merge(merge(x,y),z);
}

排名、数值间的转化

如果是给定排名,就跑一遍 find 函数找到其数值即可。

给定数值,就把这个数值对应的节点 split 出来,然后查询其 \(siz\) 即可。

例题(P3369)

维护如下 \(6\) 种操作:

  1. 插入一个数 \(a\)
  2. 删除一个数 \(a\)(若有多个相同的应只删除一个,保证存在至少一个)。
  3. 查询小于 \(a\) 的数的数量并加 \(1\)
  4. 查询第 \(a\) 个大的数。
  5. \(a\) 的前驱,保证答案存在
  6. \(a\) 的后继,保证答案存在

其中,前驱/后继定义为小于/大于 \(a\) 的最大/小的数。

除了已经给出具体函数的前四种操作,剩下的两种操作该怎样维护呢?

由于二者本质相同,接下来以操作五为例,讲解一下排名、数值转化的具体应用。

操作五

首先,二叉搜索树的中序遍历模拟的是一个序列,并且从小到大排序。那么其前驱的数值恰为最靠前的 \(a\) 前面的数,于是其前驱的排名恰为 \(a\) 的排名减一。

于是,不难想到,先把 \(a\) 转化为排名,然后将其减一,再转化为数值,最后就能得到结果啦。

tp.split(root,a-1,x,y);
printf("%d\n",tp.t[tp.find(x,tp.t[x].siz)].val);
root=tp.merge(x,y);

代码

整道题代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct Node{
	int id,val,key,siz;
	int fa,lc,rc;
	Node(){
		fa=lc=rc=0;
		id=val;
		key=rand();
		siz=1;
	}
};
struct Treap{
	Node t[N];
	int size;
	void update(int root)
	{
		t[root].siz=1+t[t[root].lc].siz+t[t[root].rc].siz;
	}
	void split(int root,int k,int &lf,int &rg)//拆分 
	{
		if(!root) lf=rg=0;
		else
		{
			if(t[root].val<=k) lf=root,split(t[root].rc,k,t[root].rc,rg);
			else rg=root,split(t[root].lc,k,lf,t[root].lc);
			update(root);
		}
	}
	int merge(int x,int y)//合并 
	{
	    if(!x||!y) return x+y;
	    if(t[x].key<t[y].key)
	    {
	        t[x].rc=merge(t[x].rc,y);
	        update(x);
	        return x;
	    }
	    else
	    {
	        t[y].lc=merge(x,t[y].lc);
	        update(y);
	        return y;
	    }
	}
	int find(int root,int k)//查找第 x 个数 
	{
	    while(1)
	    {
	        if(k<=t[t[root].lc].siz) 
			{
				root=t[root].lc;
			}
	        else if(k==t[t[root].lc].siz+1) 
			{
				return root;
			}
	        else 
			{
				k-=t[t[root].lc].siz+1;
				root=t[root].rc;
			}
	    }
	}
	int New(int id,int val)
	{
		t[++size].key=rand();
		t[size].id=id;
		t[size].val=val;
		return size;
	}
	
	void insert(int a,int id,int &root)
	{
		int x,y; 
		split(root,a,x,y);
        root=merge(merge(x,New(id,a)),y);
	}
	void del(int a,int &root)
	{
		int x,y,z; 
		split(root,a,x,z);
        split(x,a-1,x,y);
        y=merge(t[y].lc,t[y].rc);
        root=merge(merge(x,y),z);
	}
}tp;
int n,m,ans;

int main()
{
	srand((unsigned)time(NULL));
	scanf("%d",&n);
	int root=0;
	tp.t[root].siz=0;
	for(int i=1;i<=n;i++)
	{
		int opt,a;
		scanf("%d %d",&opt,&a);
		int x,y;
		if(opt==1) tp.insert(a,i,root);//插入 a 
        else if(opt==2) tp.del(a,root);//删除 a 
        else if(opt==3)//查询小于 a 的数的数量并加 1 
        {
            tp.split(root,a-1,x,y);
            printf("%d\n",tp.t[x].siz+1);
            root=tp.merge(x,y);
        }
        else if(opt==4) printf("%d\n",tp.t[tp.find(root,a)].val);//查询第 a 个大的数 
        else if(opt==5)//查 a 的前驱,**保证答案存在** 
        {
            tp.split(root,a-1,x,y);
            printf("%d\n",tp.t[tp.find(x,tp.t[x].siz)].val);
            root=tp.merge(x,y);
        }
        else//查 a 的后继,**保证答案存在** 
        {
            tp.split(root,a,x,y);
            printf("%d\n",tp.t[tp.find(y,1)].val);
            root=tp.merge(x,y);
        }
	}
	return 0; 
}
posted @ 2026-02-07 18:19  FarrisL  阅读(40)  评论(0)    收藏  举报