浅谈fhq_Treap

浅谈 \(fhq\_Treap\)

前言

fhq_Treap \(yyds\)!

\(sto\ FHQ\ orz\)

机房大佬们都打的 \(Splay\) 只有蒟蒻打的 \(fhq\)防火墙)(范浩强)_\(Treap\) QAQ!

\(fhq\) 代码短,好理解,操作少,常数小,可区间,可持久化,\(Splay\) 行的它都行(先不说LCT ),\(Splay\) 不行的它也行,除了不能旋,哪里不如 \(Slpay\) ? 那为什么不学一手呢?


引入

正常的平衡树,像 \(Treap\)\(Splay\) 这样的,维持平衡的主要操作都是靠旋转

通过不断将节点向上层旋转,以满足二叉树的性质的同时,使另一值保持堆的性质,以达到维持二叉树平衡(不让其退化成一条链)的目的。

这样虽然可以平衡二叉树,但不利于可持续化。

因为你每一次旋转都会破环树的形态(几乎完全改变),难以维护历史版本。

于是,神犇 \(FHQ\) 便发明了不用旋转的平衡树 —— \(fhq\_Treap\)


基操

先把一些基本的东西给出来。

以下的 ”“ 都是指平衡树维护的值(一般题目给出的或求出的 \(dp\) 值);以下的 ”标志“ 都是指随机 \(rand()\) 给点的值。

这是一棵平衡树

struct Treap
{
	int val,rand,ls,rs,size;
    	//分别指:“值”,“标志”,左儿子,右儿子,子树大小
}tr[N];

新建一个节点

inline int New(int val)
{
	tr[++num]={val,rand(),0,0,1};
	return num;//返回节点编号
}

更新(\(pushup\)

inline void Update(int x)
{
	tr[x].size=tr[tr[x].ls].size+tr[tr[x].rs].size+1;
}

不用旋转,\(fhq\_Treap\) 靠两种操作解决一切问题。

分裂(Split)

将一棵平衡树以某个\(val\))为界,分成两棵平衡树,一棵树上的节点的全部小于等于 \(val\) ,另一棵树的则全部大于 \(val\)

inline void Split(int p,int val,int &x,int &y)
//当前在 p 节点,以 val 为界,划分出分别以 x,y 为根的两棵平衡树
{
	if(!p) return (void)(x=y=0);//搜到空节点,返回空节点
	if(tr[p].val<=val)//当前值小于 val ,界线在右边,向右划分
	{
    	x=p;//更新较小树的根
		Split(tr[p].rs,val,tr[p].rs,y);
	}
	else//同理
	{
		y=p;
		Split(tr[p].ls,val,x,tr[p].ls);
	}
	Update(p);//由于分裂完后左右儿子可能会变,所以要更新一下节点信息
}

时间复杂度:\(\Theta(logn)\)

合并(Merge)

将两棵一大一小(一棵的全部大于另一棵的,即域无交集)的平衡树树合并成一棵平衡树,并满足平衡树的性质:为二叉树,标志为堆。

inline int Merge(int x,int y)//x为较小树的根,y为较大树的根 ,返回合并后树的根
{
	if(!x||!y) return x+y;
	if(tr[x].rand<tr[y].rand)//满足堆的性质
	{//因为x的值比y小,而x又必须在y的上面,所以根据二叉树性质,y只能在x右子树中
		tr[x].rs=Merge(tr[x].rs,y);//让y继续合并
		Update(x);
		return x;
	}
	else
	{//因为x的值比y小,而y又必须在x的上面,所以根据二叉树性质,x只能在y左子树中
		tr[y].ls=Merge(x,tr[y].ls);
		Update(y);
		return y;
	}
}

注意:由 \(Merge(x,y)\) 中的\(x\)\(y\) 的树的值之间有大小关系,故不能随意交换 \(x\) , \(y\) 的顺序写成 \(Merge(y,x)\)

时间复杂度:\(\Theta(logn)\)


有了这两个,直接上题

P3369 【模板】普通平衡树

1.插入(Insert)

我们可以把要插入的直接调用 \(New()\) 弄成一个点,再把它看作一颗只含根的树与原平衡树合并。

但合并具有大小关系的限制,所以我们先把原树裂成以这个为界的两棵树,再进行两次合并。

inline void Insert(int val)
{
	int x,y;
	Split(rt,val,x,y);
	rt=Merge(Merge(x,New(val)),y);//注意这里Merge里两根的位置
}

2.删除(Delete)

删除就是插入反着来。

先把树分成三棵,把要删的孤立出来,再把要删节点的左右儿子合并成一棵树代替它,最后把三棵树合并起来。

inline void Delete(int val)
{
	int x,y,z;
	Split(rt,val,x,z);Split(x,val-1,x,y);
	y=Merge(tr[y].ls,tr[y].rs);
	rt=Merge(Merge(x,y),z);
}

3.查询值的排名(Rank)

到这里我们之前 \(Update()\) 维护的 \(size\) 便排上用场了。

还是按给定的把树裂成两棵,这样比这个小(严格小于)的都分到一棵树里去了,那么这个的排名就是这棵树的大小+1。

inline int Rank(int val)
{
	int x,y;
	Split(rt,val-1,x,y);
//不能以val为界,因为我们要求的是严格小于val的树,当然这取决于你Split中是“<”还是“<=”
	rt=Merge(x,y);
	return tr[x].size+1;
}

4.查询排名的值(Find)

这个你甚至都不需要分裂或是合并。

inline int Find(int p,int Kth)//在以p为根的树中找排名为Kth的值(从小到大排)
{
	if(Kth==tr[tr[p].ls].size+1) return p;//这个很显然吧。。。
	if(Kth<=tr[tr[p].ls].size) return Find(tr[p].ls,Kth);//往左子树找
	return Find(tr[p].rs,Kth-tr[tr[p].ls].size-1);//往右子树找
}

5.前驱

很容易想到,把比给定严格小的树裂出来,这棵树中最大的就是这个的前驱。

inline int Pre(int val)
{
	int x,y;
	Split(rt,val-1,x,y);
	if(!x) return -inf;//没有前驱(有些题会考)
	int ans_p=Find(x,tr[x].size);
	rt=Merge(x,y);
	return tr[ans_p].val;
}

6.后继

后继和前驱思路基本一致,把比给定严格大的树裂出来,这棵树中最小的就是这个的后继。

inline int Next(int val)
{
	int x,y,anss;
	Split(rt,val,x,y);
	if(!y) return inf;//没有后继
	int ans_p=Find(y,1);
	rt=Merge(x,y);
	return tr[ans_p].val;
}

然后就完了。

最后奉上完整代码。

#include <bits/stdc++.h>
#define re register
//#define int long long
#define lss tr[p].ls
#define rss tr[p].rs
#define END system("pause")
#define inf 0x7fffffff
using namespace std;
inline int read()
{
	re int x=0,f=1;
	re char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+c-48;c=getchar();}
	return x*f;
}
const int N=1e5+5;
int rt,num;
struct Treap{int val,rand,ls,rs,size;}tr[N];
inline int New(int val){tr[++num]={val,rand(),0,0,1};return num;}
inline void Update(int p){tr[p].size=tr[lss].size+tr[rss].size+1;}
inline void Split(int p,int val,int &x,int &y)
{
	if(!p) return (void)(x=y=0);
	if(tr[p].val<=val){x=p;Split(rss,val,rss,y);}
	else{y=p;Split(lss,val,x,lss);}
	Update(p);
}
inline int Merge(int x,int y)
{
	if(!x||!y) return x+y;
	if(tr[x].rand<tr[y].rand){tr[x].rs=Merge(tr[x].rs,y);return Update(x),x;}
	tr[y].ls=Merge(x,tr[y].ls);return Update(y),y;
}
inline void Insert(int val)
{
	int x,y;Split(rt,val-1,x,y);
	rt=Merge(Merge(x,New(val)),y);
}
inline void Delete(int val)
{
	int x,y,z;Split(rt,val,x,z);Split(x,val-1,x,y);
	rt=Merge(Merge(x,Merge(tr[y].ls,tr[y].rs)),z);
}
inline int Rank(int val)
{
	int x,y,anss;Split(rt,val-1,x,y);
	anss=tr[x].size+1;rt=Merge(x,y);
	return anss;
}
inline int Find(int p,int Kth)
{
	if(tr[lss].size+1==Kth) return p;
	if(Kth<=tr[lss].size) return Find(lss,Kth);
	return Find(rss,Kth-tr[lss].size-1);
}
inline int Pre(int val)
{
	int x,y,anss;Split(rt,val-1,x,y);
	anss=tr[Find(x,tr[x].size)].val;rt=Merge(x,y);
	return anss;
}
inline int Next(int val)
{
	int x,y,anss;Split(rt,val,x,y);
	anss=tr[Find(y,1)].val;rt=Merge(x,y);
	return anss;
}
signed main()
{
	srand(998244353);
	int T=read();
	while(T--)
	{
		int op=read(),val=read();
		if(op==1) Insert(val);
		else if(op==2) Delete(val);
		else if(op==3) printf("%d\n",Rank(val));
		else if(op==4) printf("%d\n",tr[Find(rt,val)].val);
		else if(op==5) printf("%d\n",Pre(val));
		else printf("%d\n",Next(val));
	}
	return 0;
}

人性压行下 \(fhq\) 主体部分也就 \(50\) 行,总代码 \(2kb\) 左右,跑得还贼快。

再贴一个指针版的:

#include <bits/stdc++.h>
#define re register
//#define int long long
#define mp(a,b) make_pair(a,b)
#define inf 0x7fffffff7fffffff
using namespace std;
inline int read()
{
	re int x=0,f=1;
	re char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+c-48;c=getchar();}
	return x*f;
}
const int N=1e5+5;
int num;
struct node
{
	int val,rand,siz;
	inline void clear(){val=rand=siz=0;}
	node *ls,*rs;
	inline void Update(){siz=ls->siz+rs->siz+1;}
}tr[N],*null,*rt;
inline node* New(int val)
{
	tr[++num]={val,rand(),1,null,null};
	return &tr[num];
}
inline void Split(node *p,int val,node *&x,node *&y)
{
	if(p==null){x=y=null;return;}
	if(p->val<=val)
	{
		x=p;
		Split(p->rs,val,p->rs,y);
	}
	else
	{
		y=p;
		Split(p->ls,val,x,p->ls);
	}
	p->Update();
}
inline node* Merge(node *x,node *y)
{
	if(x==null) return y;
	if(y==null) return x;
	if(x->rand<y->rand)
	{
		x->rs=Merge(x->rs,y);
		x->Update();
		return x;
	}
	else
	{
		y->ls=Merge(x,y->ls);
		y->Update();
		return y;
	}
}
inline void Insert(int val)
{
	node *x,*y;
	Split(rt,val,x,y);
	rt=Merge(Merge(x,New(val)),y);
}
inline void Delete(int val)
{
	node *x,*y,*z;
	Split(rt,val,x,y);Split(x,val-1,x,z);
	z=Merge(z->ls,z->rs);
	rt=Merge(Merge(x,z),y);
}
inline void Rank(int val)
{
	node *x,*y;
	Split(rt,val-1,x,y);
	printf("%d\n",x->siz+1);
	rt=Merge(x,y);
}
inline node* Find(node *p,int Kth)
{
	if(Kth==p->ls->siz+1) return p;
	if(Kth<=p->ls->siz) return Find(p->ls,Kth);
	return Find(p->rs,Kth-p->ls->siz-1);
}
inline void Pre(int val)
{
	node *x,*y;
	Split(rt,val-1,x,y);
	node *ans_p=Find(x,x->siz);
	printf("%d\n",ans_p->val);
	rt=Merge(x,y);
}
inline void Next(int val)
{
	node *x,*y;
	Split(rt,val,x,y);
	node *ans_p=Find(y,1);
	printf("%d\n",ans_p->val);
	rt=Merge(x,y);
}
signed main()
{
	srand(98244353);
	tr[0].clear();
	null=rt=&tr[0];
	int T=read();
	while(T--)
	{
		int op=read(),val=read();
		if(op==1) Insert(val);
		if(op==2) Delete(val);
		if(op==3) Rank(val);
		if(op==4) printf("%d\n",Find(rt,val)->val);
		if(op==5) Pre(val);
		if(op==6) Next(val);
	}
	getchar();
	return 0;
}

区间

然后我们迎来了区间问题。

这类问题分裂时是将树分成两种大小,又由于是用平衡树来维护下标,所以可以看作分裂成两段区间。

对于区间操作,我们只需将要操作的那段区间用 \(Split()\) 出来,然后操作,然后再 \(Merge()\) 回去。

大致格式如下

int l=read(),r=read();
int x,y,z;
Split(rt,r,x,z);
Split(x,l-1,x,y);
Work(y);
Merge(Merge(x,y),z);

\(Work(y)\) 就是对以 \(y\) 为根的树进行具体操作。


直接上题

P3391 【模板】文艺平衡树

这道题求的是区间翻转。

很显然平衡树上直接翻等价于暴力。

但不要忘了利用相对平衡的树形结构。

而这让我们想到了线段树的区间操作。

对,没错,打标记,操作时下传。

而且我们发现:一段区间翻转两次后相当于没翻。

直接可以做了。

还是一棵平衡树,只是多了个信息( \(tag\) )。

struct Treap
{
	int val,rand,ls,rs,size,tag;
}tr[N];

\(tag\) 初始值为 \(0\) (表示没有反转)。

然后是 \(pushdown()\)

inline void pushdown(int p)
{
	if(!tr[p].tag) return;
	swap(tr[p].ls,tr[p].rs);//翻转序列==翻转下标
	//左右下传
	tr[tr[p].ls].tag^=1;
	tr[tr[p].rs].tag^=1;
	tr[p].tag=0;//不要忘了自身清零
}

\(pushdown()\) 一定要在递归下一层之前运行。

Split()

inline void Split(int p,int val,int &x,int &y)
{
	if(!p) return (void)(x=y=0);
	pushdown(p);//先下传
	if(tr[tr[p].ls].size+1<=val)//这里变了,比较下标就和Rank()差不多
	{
		x=p;
		Split(tr[p].rs,val-tr[tr[p].ls].size-1,tr[p].rs,y);//这里也不一样
	}
	else
	{
		y=p;
		Split(tr[p].ls,val,x,tr[p].ls);
	}
	Update(p);
}

Merge()

inline int Merge(int x,int y)
{
	if(!x||!y) return x+y;
	if(tr[x].rand<tr[y].rand)
	{
		pushdown(x);//先下传
		tr[x].rs=Merge(tr[x].rs,y);
		Update(x);return x;
	}
	else
	{
		pushdown(y);//先下传
		tr[y].ls=Merge(x,tr[y].ls);
		Update(y);return y;
	}
}

对于每一次询问

int l=read(),r=read();
int x,y,z;
Split(rt,r,x,z);
Split(x,l-1,x,y);
tr[y].tag^=1;//在树根打标记
rt=Merge(Merge(x,y),z);

最后根据二叉树的性质,中序遍历一遍平衡树就是原序列了。

献上完整代码。

#include <bits/stdc++.h>
#define re register
#define int long long
#define END system("pause")
#define inf 0x7fffffff
using namespace std;
inline int read()
{
	re int x=0,f=1;
	re char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+c-48;c=getchar();}
	return x*f;
}
const int N=3e5+5,P=1e6;
int rt,num;
struct Treap{int val,rand,ls,rs,size,tag;}tr[N];
inline void Update(int x){tr[x].size=tr[tr[x].ls].size+tr[tr[x].rs].size+1;}
inline int New(int val){tr[++num]={val,rand(),0,0,1,0};return num;}
inline void pushdown(int p)
{
	if(!tr[p].tag) return;
	swap(tr[p].ls,tr[p].rs);
	tr[tr[p].ls].tag^=1;
	tr[tr[p].rs].tag^=1;
	tr[p].tag=0;
}
inline void Split(int p,int val,int &x,int &y)
{
	if(!p) return (void)(x=y=0);
	pushdown(p);
	if(tr[tr[p].ls].size+1<=val)
	{
		x=p;
		Split(tr[p].rs,val-tr[tr[p].ls].size-1,tr[p].rs,y);
	}
	else
	{
		y=p;
		Split(tr[p].ls,val,x,tr[p].ls);
	}
	Update(p);
}
inline int Merge(int x,int y)
{
	if(!x||!y) return x+y;
	if(tr[x].rand<tr[y].rand)
	{
		pushdown(x);
		tr[x].rs=Merge(tr[x].rs,y);
		Update(x);return x;
	}
	else
	{
		pushdown(y);
		tr[y].ls=Merge(x,tr[y].ls);
		Update(y);return y;
	}
}
inline void Insert(int val)
{
	int x,y;
	Split(rt,val,x,y);
	rt=Merge(Merge(x,New(val)),y);
}
inline void dfs(int p)//中序遍历
{
	pushdown(p);//遍历前先下传
	if(tr[p].ls) dfs(tr[p].ls);
	printf("%d ",tr[p].val);
	if(tr[p].rs) dfs(tr[p].rs);
}
signed main()
{
	srand(98244353);
	int n=read(),m=read();
	for(re int i=1;i<=n;++i) Insert(i);
	while(m--)
	{
		int l=read(),r=read();
		int x,y,z;
		Split(rt,r,x,z);Split(x,l-1,x,y);
		tr[y].tag^=1;
		rt=Merge(Merge(x,y),z);
	}
	dfs(rt);
	//END;
	return 0;
}

个人认为区间操作挺好理解的。


可持续化

我们发现在进行 \(Split()\)\(Merge()\) 操作时,我们其实就只是变动了原树的不到一半,而我们完全可以在剩下的节点上建立新的节点来可持续化。

从网上嚯了一张图片:

如图,深色 \(4\)\(8\)\(7\) 为新建节点,\(5\) 为新插入的节点。

于是:

Split()

inline void Split(int p,int val,int &x,int &y)
{
	if(!p) return (void)(x=y=0);
	if(tr[p].val<=val)
	{
		x=New(0);//x为新建节点
		tr[x]=tr[p];
		Split(tr[x].rs,val,tr[x].rs,y);
		Update(x);//与之前不同,由于x为独立的节点,与下面的y没有关系,所以要各自单独Update()
	}
	else
	{
		y=New(0);
		tr[y]=tr[p];
		Split(tr[y].ls,val,x,tr[y].ls);
		Update(y);
	}
}

Merge()

inline int Merge(int x,int y)
{
	if(!x||!y) return x+y;
	if(tr[x].rand<tr[y].rand)
	{
		int p=New(0);//这里也是,新建节点
		tr[p]=tr[x];
		tr[p].rs=Merge(tr[p].rs,y);
		Update(p);
		return p;
	}
	int p=New(0);
	tr[p]=tr[y];
	tr[p].ls=Merge(x,tr[p].ls);
	Update(p);
	return p;
}

上题

P3835 【模板】可持久化平衡树

只需将 \(rt\) 开成数组,保存每个时刻的根,然后其他操作都在普通平衡树上稍微改一下即可。

#include <bits/stdc++.h>
#define re register
//#define int long long
#define lss tr[p].ls
#define rss tr[p].rs
#define END system("pause")
#define inf 0x7fffffff
using namespace std;
inline int read()
{
	re int x=0,f=1;
	re char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+c-48;c=getchar();}
	return x*f;
}
const int N=5e5+5;
int num;
int rt[N];
struct Treap{int val,rand,ls,rs,size;}tr[N<<6];
inline int New(int val)
{
	tr[++num]={val,rand(),0,0,1};
	return num;
}
inline void Update(int p)
{
	tr[p].size=tr[lss].size+tr[rss].size+1;
}
inline void Split(int p,int val,int &x,int &y)
{
	if(!p) return (void)(x=y=0);
	if(tr[p].val<=val)
	{
		x=New(0);
		tr[x]=tr[p];
		Split(tr[x].rs,val,tr[x].rs,y);
		Update(x);
	}
	else
	{
		y=New(0);
		tr[y]=tr[p];
		Split(tr[y].ls,val,x,tr[y].ls);
		Update(y);
	}
}
inline int Merge(int x,int y)
{
	if(!x||!y) return x+y;
	if(tr[x].rand<tr[y].rand)
	{
		int p=New(0);
		tr[p]=tr[x];
		tr[p].rs=Merge(tr[p].rs,y);
		Update(p);
		return p;
	}
	int p=New(0);
	tr[p]=tr[y];
	tr[p].ls=Merge(x,tr[p].ls);
	Update(p);
	return p;
}
inline void Insert(int ver,int val)
{
	int x,y;
	Split(rt[ver],val-1,x,y);
	rt[ver]=Merge(Merge(x,New(val)),y);
}
inline void Delete(int ver,int val)
{
	int x,y,z;
	Split(rt[ver],val,x,z);Split(x,val-1,x,y);
	y=Merge(tr[y].ls,tr[y].rs);
	rt[ver]=Merge(Merge(x,y),z);
}
inline int Rank(int ver,int val)
{
	int x,y,anss;
	Split(rt[ver],val-1,x,y);
	anss=tr[x].size+1;
	rt[ver]=Merge(x,y);
	return anss;
}
inline int Find(int p,int Kth)
{
	if(tr[lss].size+1==Kth) return p;
	if(Kth<=tr[lss].size) return Find(lss,Kth);
	return Find(rss,Kth-tr[lss].size-1);
}
inline int Pre(int ver,int val)
{
	int x,y,anss;
	Split(rt[ver],val-1,x,y);
	anss=tr[Find(x,tr[x].size)].val;
	rt[ver]=Merge(x,y);
	return anss;
}
inline int Next(int ver,int val)
{
	int x,y,anss;
	Split(rt[ver],val,x,y);
	anss=tr[Find(y,1)].val;
	rt[ver]=Merge(x,y);
	return anss;
}
signed main()
{
	int T=read();
	for(re int i=1;i<=T;++i)
	{
		int ver=read(),op=read(),val=read();
		rt[i]=rt[ver];
		if(op==1) Insert(i,val);
		else if(op==2) Delete(i,val);
		else if(op==3) printf("%d\n",Rank(i,val));
		else if(op==4) printf("%d\n",tr[Find(rt[i],val)].val);
		else if(op==5) printf("%d\n",Pre(i,val));
		else printf("%d\n",Next(i,val));
	}
	return 0;
}

后记

后面还有一些更高级的操作,蒟蒻太弱了根本不会,还请大佬自行研究。

这里附上几道题目:

P5055 【模板】可持久化文艺平衡树

P2042 [NOI2005] 维护数列(这是一道毒瘤题)

P1486 [NOI2004] 郁闷的出纳员

P2286 [HNOI2004]宠物收养场

乌拉!!!

posted @ 2021-07-07 20:17  After-glow  阅读(141)  评论(3编辑  收藏  举报