【学习笔记】FHQ Treap

\(\textbf{I. 基本操作}\)

\(\textbf{维护子树大小 / size_up()}\)

和线段树中的 \(\text{push_up}\) 操作类似,目的是维护以一个节点为根的子树的节点个数。

inline void size_up(int id)
{
	siz[id]=siz[lc[id]]+siz[rc[id]]+1;
}
//siz[id] 表示以节点 id 为根的子树的节点个数
//lc[id] 是其左孩子编号,rc[id] 是其右孩子编号。
//那么 siz[id] 是其左子树大小加上右子树大小加上一个根。

\(\textbf{分裂操作 / Split()}\)

顾名思义,将一颗 \(\text{FHQ Treap}\) 分裂成两颗。

但是自然不是乱分裂,我们给 \(\text{Split}\) 函数传一个参数 \(div\),表示目标为将这颗 \(\text{FHQ Treap}\) 分裂成两颗树,这两棵树满足一颗的节点权值全部 \(\le div\),另一颗反之,节点权值全部 \(>div\)

这就相当于人为的把 \([-\infty,div]\)\((div,\infty]\) 隔开。

我们规定,在 \(\text{FHQ Treap}\) 中,满足根节点左子树的权值全部小于根节点,右子树反之。

设分裂后,分裂出的两棵树分别为 \(x,y\)

那么当我们分裂到某个节点时,若它的权值 \(w\in[-\infty,div]\),那么它的左子树一定全是 \(x\) 的,但是右子树不一定,右子树可能有某一部分是属于 \(y\) 的。

\(w\in(div,\infty]\),则同理,它的左子树不一定全部属于 \(y\)

inline void split(int id,int div,int &x,int &y)
{
	if(!id)
	{
		x=y=0;
		return ;
	}
	if(val[id]<=div)
	{
		x=id;//先把这整棵树粘到 id 上去。
		spilt(rc[id],div,rc[id],y);//然后到右子树,把权值 >div 的点割出去。
	}else
	{
		y=id;
		spilt(lc[id],div,x,lc[id]);//同上,到左子树分裂出权值 <=div 的点。
	}
	size_up(id);//搞完之后记得维护树的大小。
	return ;
}

\(\textbf{合并操作 / Merge()}\)

既然有分裂,自然有合并。

话说天下大势,分久必合,合久必分。

平衡树同理(

平衡树满足堆的性质,那么 \(\text{FHQ Treap}\) 也满足。

既然是堆,那么直接合并就行了。

注意在下面的代码中,子树 \(x\) 的最大权值比子树 \(y\) 的最大权值小。

合并方向随机一下,不然容易被卡。

inline int merge(int x,int y)
{
	if(!x||!y)
		return x|y;//一个为空直接返回不为空的那个。
	if(c[x]>c[y])//给每个节点随机一个权值 c,以此决定合并方向。
	{
		rc[x]=merge(rc[x],y);//把 y 粘到 x 的右子树上
		size_up(x);
		return x;
	}else
	{
		lc[y]=merge(x,lc[y]);// 同上
		size_up(y);
		return y;
	}
}

\(\textbf{新建节点操作 / Create()}\)

思考一下,一个节点要记录那些信息?

首先,它的左右孩子我们要记录下来。

然后,以它为根的子树大小也要记录。

最后,这个节点的权值 \(w\) 和随机权值 \(c\) 也要记录和生成。

这个生成可以使用内存池的方法。

inline int create(int value)
{
	siz[len]=1;//只有一个节点。
	v[len]=value;//新建节点的权值
	c[len]=rerand();//给它随机一个。
	lc[len]=rc[len]=0;
	return len++;
}

\(\textbf{插入操作 / Insert()}\)

\(\text{FHQ Treap}\) 在实现插入操作时,直接使用合并和分裂在树上搞。

假设我们要插入值 \(value\)

这样想,我们先把原来的树拆开,拆成 \([-\infty,value]\)\((value,\infty]\),然后直接把这个插入的节点 \(\text{Merge}\) 上去不就行了。

这样就可以简单的完成 \(\text{Insert}\) 操作了!

inline void insert(int value)
{
	int x,y;
	split(root,value,x,y);
	root=merge(merge(x,create(value)),y);//先把 create(x) 和 x 合并起来,再把 y 粘回去。
}

\(\textbf{删除操作 / Erase()}\)

我们要删除权值为 \(value\) 的点,我们还是考虑像插入一样,先拆,操作后再合并。

先把树分裂成 \([-\infty,value)\)\(\{value\}\)\((value,\infty]\) 三个区间。

但是有没有一种可能,\(value\) 不止一个但你只删一个。

所以第二颗 \(\{value\}\) 的树删掉一个根就行。

inline void erase(int value)
{
	int x,y,z;
	split(root,value,x,z);//此时 z 确定为 (value,inf]。
	split(x,value-1,x,y);//此时,x 确定为 [-inf,value),y 确定为 {value}。
	root=merge(merge(x,/*把根删了*/merge(lc[y],rc[y])),z);//按序合并回来。
}

\(\textbf{查值排名 / Rnk()}\)

显然把这棵树分裂成 \([-\infty,value)\)\([value,\infty]\),然后查左边那区间的长度加一即可。

别忘了合并回来。

inline int rnk(int value)
{
	int x,y,ret;
	split(root,value-1,x,y);//如上分裂。
	ret=siz[x]+1;//记录长度的意义在这。
	root=merge(x,y);
	return ret;
}

\(\textbf{查询前驱、后继 / Pre()、Nxt()}\)

套路的,分裂再合并。

由于平衡树的性质,查前驱一路往右爬,查后继一路往左爬。

两个代码很像诶,毕竟原理是一致的。

inline int pre(int v)
{
	int x,y,p,res=no_sol;
	split(root,v-1,x,y);
	p=x;
	while(p)
	{
		res=val[p];
		p=rc[p];//一路往右爬
	}
	root=merge(x,y);
	return res;
}
inline int pre(int v)
{
	int x,y,p,res=no_sol;
	split(root,v,x,y);
	p=y;
	while(p)
	{
		res=val[p];
		p=lc[p];//一路往左爬
	}
	root=merge(x,y);
	return res;
}

\(\textbf{区间第 k 大 / Kth-number()}\)

终于不是分裂再合并了(

平衡树中序遍历是有序序列,所以直接决定当前往左爬还是往右爬即可。

比较简单不多说了。

inline int kth(int value)
{
	int p=root;
	while(true)
	{
		if(siz[lc[p]]+1==k)
			return val[p];//这些加一都是考虑根。
		if(siz[lc[p]]>=k)
			p=lc[p];
		else
		{
			k-=siz[lc[p]]+1;
			p=rc[p];
		}
	}
}

\(\textbf{II. 文艺平衡树(正在更)}\)

\(\textbf{懒标记及其下传 / Lazy Tag & push_down()}\)

这玩意不如去看那篇皎月半洒花写的线段树,我觉得介绍懒标记说的挺好的。

在树上翻转显然想到交换左右孩子。但是如果你给它一路翻转下去,容易发现你的复杂度上天,为 \(n\log n\)。使用懒标记,需要时再下传,复杂度变成 \(\log n\),总复杂度维持 \(O(n\log n)\)

如何下传?难道是 tag[lc[p]]=tag[rc[p]]=true

显然不是,如果一个区间翻转了偶数次,就相当于没有翻转。

故代码为:

inline void push_down(int p)
{
	if(tag[p])//若懒标记存在
	{
		swap(lc[p],rc[p]);//交换左右子树
		tag[lc[p]]^=1;
		tag[rc[p]]^=1;//下传懒标记
		//这样写能保证原来是 1 时变为 0,原来为 0 时变为 1。
		//也就是取反
		tag[p]=0;//清空懒标记。
	}
	return ;
}

\(\textbf{分裂操作 / Split()}\)

既然是 \(\text{FHQ Treap}\) 那么依然是分裂和合并!

文艺平衡树需要实现神秘的区间翻转,这一点说明我们像上面那样直接按权值分裂是完全不可行的。

换个思路,我们按照树的大小分裂。然后写一段几乎一样的代码即可。

inline void split(int id,int div,int &x,int &y)
{
	if(!id)
	{
		x=y=0;
		return ;
	}
	push_down(id);//你线段树 query/update 之前是不是也要下传?
	if(siz[lc[id]]<div)//实际上,是siz[lc[id]]+1<=div,即左子树加上根。 
	{
		x=id;
		split(rc[id],div-siz[lc[id]]-1,rc[id],y);//别忘了把根也分裂过去。
	}
	else
	{
		y=id;
		split(lc[id],div,x,lc[id]);//这几行和普通的 FHQ-Treap 含义差不多
	}
	size_up(id);//普通的 FHQ-Treap 也要这样。
	return ;
}

\(\textbf{合并操作 / Merge()}\)

联想一下线段树,很显然在普通平衡树板子上加个懒标记下传即可。

inline int merge(int x, int y)
{
    if (!x||!y)
        return x|y;
    if (c[x]>c[y])
    {
        push_down(x);//就这个点喔
        rc[x]=merge(rc[x],y);
        size_up(x);
        return x;
    }
    else
    {
        push_down(y);//你不 push_down 你不能确保下面的 lc/rc 正确
        lc[y]=merge(x,lc[y]);
        size_up(y);
        return y;
    }
}

\(\textbf{区间翻转操作 / Reverse()}\)

\(\text{FHQ-Treap}\) 伟大的地方就在于你理解了 \(\text{Merge}\)\(\text{Split}\) 之后就会写平衡树了。

inline void reverse(int l,int r)
{
	static int x,y,z;
	split(rt,l-1,x,y);//x -> [1,l) , y -> [l,n]
	split(y,r-l+1,y,z);//y -> [l,r] , z -> (r,n]
	tag[y]^=1;// 此时直接把区间 y 打上懒标记!
	root=merge(merge(x, y), z);//合并回来,完成!
	return ;
}

\(\textbf{输出区间 / Print()}\)

以洛谷的模板为例,你需要输出原序列。

直接从根往下深搜一遍即可,搜到空结点直接 return

inline void dfs(int id)
{
    if(id==0)//空节点
        return ;
    push_down(id);
    dfs(lc[id]);//先把在该节点之前的输出
    printf("%d ",val[id]);//输出该节点,运用了递归的性质。
    dfs(rc[id]);//再把后面的输出了
}

\(\textbf{III. 一些有点难度的例题}\)

比较弱智,就是维护几种操作。仔细想想,发现文艺平衡树和普通平衡树都是可以做的。

对于普通平衡树,你只要吧对应的 \(s\) 分裂出来在找到位置合并回去即可。

对于文艺平衡树,以 \(\text{Bottom}\) 为例,$\text{Reverse}(s,top) $ 之后 \(\text{Reserve}(s,top-1)\) 即可。其余同理。

仔细一想,感觉两种写法都差不多,随便写一种过了跑路!

posted @ 2023-02-12 20:50  Syara  阅读(46)  评论(0编辑  收藏  举报