平衡树 - FHQ 学习笔记

平衡树 - FHQ 学习笔记

主要参考万万没想到 的 FHQ-Treap学习笔记

本片文章的姊妹篇:平衡树 - Splay 学习笔记

感觉完全不会平衡树,又重新学习了一遍 FHQ,一口气把常见套路都学完了。

一、大致内容及分类

FHQ(???),全称非旋转 Treap,是一种可以用于维护按权值、排名分裂的数据结构。它相比与 Splay 虽然常数较大,但是实现起来代码难度相对容易,而且由于它非旋的特点,也可以用来实现可持久化。

既然叫做非旋 Treap,它兼有 Treap 的特点又有非旋转独特的优势。

  • 从 Treap 角度看,他们同样都是依赖修正值 rnd 是随机的,用将他们按照 rnd 形成一个小根堆。与 Treap 相同,它也满足笛卡尔树的性质,它的中序遍历和它的插入顺序相同,即 \(1\)\(n\) 的序列。
  • 从非旋角度看,FHQ 直接通过 splitmerge 操作实现添加、删除元素,不用再树上旋转了。

根据不同题目要求,将平衡树分为序列平衡树权值平衡树

  • 序列平衡树的中序遍历为每个元素在序列中的下标,权值为每个元素具体的值,常见题型为区间翻转等。
  • 权值平衡树的中序遍历是所有元素从小到大排序,即按照中序遍历提取所有元素后元素权值递增,常见操作为第 \(k\) 大等。

如果毒瘤出题人同时综合了以上两种操作,即区间翻转 \(+\) 区间第 \(k\) 大,应该怎么做呢?好吧,如果真是这样,这篇文章可能不能够帮到你,直接上一个根号做法!

二、基本操作

FHQ 的核心操作就是 split 出操作区间,操作完后 merge 回去。

下边讲解中默认的平衡树类型为权值平衡树,序列平衡树其实是将某些 \(val\) 改为了 \(siz\)

分裂 split

无论是按照排名还是权值分裂,他们都是将原树分为左右两半,可以利用中序遍历的性质进行分裂。

具体操作时,我们新建两个临时变量 \(x,y\) 分别表示分裂出来的左边、右边的那颗平衡树。

如果我们遇到一个应该属于 \(x\) 树的节点,就将这个点以及这个点的左子树加入 \(x\) 树中,并递归分裂右子树;如果遇到属于 \(y\) 的,就将这个点与它的右子树加入 \(y\) 树中,并递归分裂左子树。

需要注意到是,如果使用序列平衡树,下面和 \(k\) 的大小判断应该变为 \(ls.siz+1\le k\)

\(\bigstar\texttt{Attention}\):如果需要 split 一个区间,记得先 split 右端点再 split 左端点!

可以写出伪代码如下:

void split(int p,int k,int &x,int &y) // 分裂出 (-infty,k],(k,+infty) 
{
	 if(!p) { x=y=0; return; }
	 pushdown(p);
	 if(tree[p].val<=k) x=p,split(rs,k,rs,y);
	 else y=p,split(ls,k,x,ls);
	 pushup(p);
}

合并 merge

由于这是 FHQ 的 merge,需要在合并时既保证小根堆性质又不破坏中序遍历的特点,对合并的两棵树有特殊的要求:左右区间不能够相交或者顺序颠倒!

所以我们在合并时必须按照顺序从左到右合并。

具体操作时,可以直接将 rnd 小的作为新树的根节点,如果这个根节点来自左子树就递归右子树,相反来自右子树就递归左子树(由于满足上面区间不相交也不颠倒的特点)。

写出伪代码:

int merge(int x,int y)
{
	 if(!x || !y) return x+y;
	 if(tree[x].rnd<tree[y].rnd)
	 	 { pushdown(x),tree[x].pr=merge(tree[x].pr,y),pushup(x); return x; }
	 else
	 	 { pushdown(y),tree[y].pl=merge(x,tree[y].pl),pushup(y); return y; }
}

新建节点 new

没什么可说的,就是给新节点附一个随机的 rnd

inline int New(ll Val)
{
	 int p=++All;
	 tree[p].rnd=rand(),tree[p].val=Val;
	 tree[p].siz=tree[p].cnt=1;
	 tree[p].pl=tree[p].pr=0;
	 return p;
}

插入 insert

直接分裂出两端区间,把新建的加点放到两棵树中间在合并即可。

inline void Insert(ll Val)
{
	 split(root,Val,x,y);
	 root=merge(merge(x,New(Val)),y);
}

删除 delete

FHQ 可以实现删除一个数或删除这个值的所有数,唯一区别就在于分裂时的不同。

inline void Delete_one(ll Val)
{
	 split(root,Val,x,z),split(x,Val-1,x,y);
	 y=merge(tree[y].pl,tree[y].pr);
	 root=merge(merge(x,y),z);
}
inline void Delete_All(ll Val)
{
	 split(root,Val,x,z),split(x,Val-1,x,y);
	 root=merge(x,z);
}

查询排名对应权值 Rank_to_Value

根据每颗子树的 \(siz\) 暴力跳即可。

inline int kth(int p,int Rank) // 这里返回的是找到节点的下标 
{
	 while(p)
	 {
	 	 if(tree[ls].siz>=Rank) p=ls;
	 	 else if(tree[ls].siz+tree[p].cnt>=Rank) return p;
	 	 else Rank-=tree[ls].siz+tree[p].cnt,p=rs;
	 }
	 return p;
}

查询权值对应排名 Value_to_Rank

按照权值分裂出来后左区间树的 \(siz\) 就是排名。

inline int Value_to_Rank(ll Value)
{
	 split(root,Value-1,x,y);
	 int ret=tree[x].siz+1;
	 root=merge(x,y);
	 return ret;
}

查询前驱 Findpre

分裂出来后在左子树中排名最靠后的是前驱。

inline ll Findpre(ll Value)
{
	 split(root,Value-1,x,y);
	 ll ret=tree[kth(x,tree[x].siz)].val;
	 root=merge(x,y);
	 return ret;
}

查询后继 Findnex

分裂出来后在右子树中排名最靠前的是后继。

inline ll Findnex(ll Value)
{
	 split(root,Value,x,y);
	 ll ret=tree[kth(y,1)].val;
	 root=merge(x,y);
	 return ret;
}

三、可持久化平衡树

平衡树上的可持久化和线段树的可持久化其实差别不大,每次修改的时候需要建立新的节点,对于每个版本也需要保存根节点。

新建一个点的原则:如果我们把版本最新的点叫做新点,那么我们只能够在可持久化平衡树中对新点修改,不然就会出错,所以在 splitmerge 中,我们一旦对一个值进行了修改,就需要新建一个节点。

伪代码如下:

inline void pushdown(int p)
{
	 if(!tree[p].tag) return;
	 if(ls) ls=clone(ls),tree[ls].tag^=1,swap(tree[ls].pl,tree[ls].pr);
	 if(rs) rs=clone(rs),tree[rs].tag^=1,swap(tree[rs].pl,tree[rs].pr);
	 tree[p].tag=false;
}
inline int clone(int p) { tree[++All]=tree[p]; return All; }
void split(int p,ll k,int &x,int &y)
{
	 if(!p) { x=y=0; return; }
	 pushdown(p);
	 if(tree[p].val<=k) x=clone(p),split(rs,k,tree[x].pr,y),pushup(x);
	 else y=clone(p),split(ls,k,x,tree[y].pl),pushup(y);
}
int merge(int x,int y)
{
	 if(!x || !y) return x+y;
	 if(tree[x].rnd<tree[y].rnd)
	 {
	 	 pushdown(x),x=clone(x),tree[x].pr=merge(tree[x].pr,y),pushup(x);
	 	 return x;
	 }
	 else
	 {
	 	 pushdown(y),y=clone(y),tree[y].pl=merge(x,tree[y].pl),pushup(y);
	 	 return y;
	 }
}

四、常见优化技巧

垃圾回收

在每次新建节点的时候先从垃圾桶中捡,如果垃圾捡光了再新开节点。

笛卡尔树方式建树

由于我们的 FHQ 是一种笛卡尔树,所以如果给定了一堆点,完全可以直接用笛卡尔树的方式 \(\mathcal{O(n)}\) 建树。

inline int build()
{
	 int tp=0,p=0,Last;
	 for(int i=1;i<=n;i++)
	 {
	 	 p=New(v[i]),Last=0;
	 	 while(tp && tree[sta[tp]].rnd>tree[p].rnd) pushup(Last=sta[tp--]);
	 	 if(tp) tree[sta[tp]].pr=p;
	 	 tree[p].pl=Last,sta[++tp]=p;
	 }
	 while(tp) pushup(sta[tp--]);
	 return sta[1];
}

定期重构

如果题目中对空间有限制而且不要求查询历史版本,可以定期重构。当使用的空间超过一定值的时候,我们中序遍历整棵树并放入数组中,在线性建树。

启发式合并

如果需要合并两个有交集的 Treap 时该怎么做?我们可以每次将较小的数合并到较大的树中去,这样每个点最多只会合并 \(\log n\) 次,每次合并复杂度 \(n\log n\),总时间复杂度 \(\mathcal{O(n\log ^2 n)}\)

区间缩点

详见万万没想到的博客,咕咕咕。

五、模板

struct FHQ_number
{
	 #define Maxn 点数
	 #define ls tree[p].pl
	 #define rs tree[p].pr
	 private:
		 int All=0,root=0;
		 struct NODE { int pl,pr,siz,cnt,rnd; ll val; };
		 NODE tree[Maxn];
		 inline int Dot() { return ++All; }
		 inline int New(ll Val)
		 {
		 	 int p=Dot();
		 	 tree[p].rnd=rand(),tree[p].val=Val;
		 	 tree[p].siz=tree[p].cnt=1;
		 	 tree[p].pl=tree[p].pr=0;
		 	 return p;
		 }
		 inline void pushdown(int p) { p--; }
		 inline void pushup(int p)
		 	 { tree[p].siz=tree[ls].siz+tree[rs].siz+tree[p].cnt; }
		 void split(int p,int k,int &x,int &y)
		 {
		 	 if(!p) { x=y=0; return; }
		 	 pushdown(p);
		 	 if(tree[p].val<=k) x=p,split(rs,k,rs,y);
		 	 else y=p,split(ls,k,x,ls);
		 	 pushup(p);
		 }
		 int merge(int x,int y)
		 {
		 	 if(!x || !y) return x+y;
		 	 if(tree[x].rnd<tree[y].rnd)
		 	 {
		 	 	 pushdown(x),tree[x].pr=merge(tree[x].pr,y),pushup(x);
		 	 	 return x;
			 }
		 	 else
		 	 {
		 	 	 pushdown(y),tree[y].pl=merge(x,tree[y].pl),pushup(y);
		 	 	 return y;
			 }
		 }
		 inline int kth(int p,int Rank)
		 {
		 	 while(p)
		 	 {
		 	 	 if(tree[ls].siz>=Rank) p=ls;
		 	 	 else if(tree[ls].siz+tree[p].cnt>=Rank) return p;
		 	 	 else Rank-=tree[ls].siz+tree[p].cnt,p=rs;
			 }
			 return p;
		 }
	 	 int x,y,z;
	 public:
	 	 inline void Insert(ll Val)
	 	 	 { split(root,Val,x,y),root=merge(merge(x,New(Val)),y); }
	 	 inline void Delete_one(int Val)
	 	 {
	 	 	 split(root,Val,x,z),split(x,Val-1,x,y);
	 	 	 y=merge(tree[y].pl,tree[y].pr);
	 	 	 root=merge(merge(x,y),z);
		 }
		 inline ll Rank_to_Value(int Rank)
		 	 { return tree[kth(root,Rank)].val; }
		 inline int Value_to_Rank(ll Value)
		 {
		 	 split(root,Value-1,x,y);
		 	 int ret=tree[x].siz+1;
		 	 root=merge(x,y);
		 	 return ret;
		 }
		 inline ll Findpre(ll Value)
		 {
		 	 split(root,Value-1,x,y);
		 	 ll ret=tree[kth(x,tree[x].siz)].val;
		 	 root=merge(x,y);
		 	 return ret;
		 }
		 inline ll Findnex(ll Value)
		 {
		 	 split(root,Value,x,y);
		 	 ll ret=tree[kth(y,1)].val;
		 	 root=merge(x,y);
		 	 return ret;
		 }
}T;
struct FHQ_sequence
{
	 #define Maxn 点数
	 #define ls tree[p].pl
	 #define rs tree[p].pr
	 int All=0,root=0;
	 struct NODE { int pl,pr,siz,cnt,rnd,val; bool tag; };
	 NODE tree[Maxn];
	 inline int Dot() { return ++All; }
	 inline int New(int Val)
	 {
	 	 int p=Dot();
	 	 tree[p].rnd=rand(),tree[p].val=Val;
	 	 tree[p].cnt=tree[p].siz=1;
	 	 tree[p].pl=tree[p].pr=0;
	 	 return p;
	 }
	 inline void pushdown(int p)
	 {
	 	 if(!tree[p].tag) return;
	 	 swap(tree[ls].pl,tree[ls].pr);
	 	 swap(tree[rs].pl,tree[rs].pr);
	 	 tree[ls].tag^=1,tree[rs].tag^=1;
	 	 tree[p].tag=false;
	 }
	 inline void pushup(int p)
	 	 { tree[p].siz=tree[ls].siz+tree[p].cnt+tree[rs].siz; }
	 void split(int p,int k,int &x,int &y)
	 {
	 	 if(!p) { x=y=0; return; }
	 	 pushdown(p);
	 	 if(tree[ls].siz<k) x=p,split(rs,k-tree[ls].siz-1,rs,y);
		 else y=p,split(ls,k,x,ls);
		 pushup(p);
	 }
	 int merge(int x,int y)
	 {
	 	 if(!x || !y) return x+y;
	 	 if(tree[x].rnd<tree[y].rnd)
	 	 {
	 	 	 pushdown(x),tree[x].pr=merge(tree[x].pr,y),pushup(x);
	 	 	 return x;
		 }
		 else
		 {
		 	 pushdown(y),tree[y].pl=merge(x,tree[y].pl),pushup(y);
		 	 return y;
		 }
	 }
	 int kth(int p,int Rank)
	 {
	 	 while(p)
	 	 {
	 	 	 if(tree[ls].siz>=Rank) p=ls;
	 	 	 else if(tree[ls].siz+tree[p].cnt>=Rank) return p;
	 	 	 else Rank-=tree[ls].siz+tree[p].cnt,p=ls;
		 }
		 return p;
	 }
	 void Insert(int Val) // 插到末尾 
	 	 { root=merge(root,New(Val)); }
	 int x,y,z;
	 inline void Reverse(int l,int r)
	 {
	 	 split(root,r,x,z),split(x,l-1,x,y);
	 	 swap(tree[y].pl,tree[y].pr),tree[y].tag^=1;
	 	 root=merge(merge(x,y),z);
	 }
	 void print(int p)
	 {
	 	 pushdown(p);
	 	 if(ls) print(ls);
	 	 printf("%d ",tree[p].val);
	 	 if(rs) print(rs);
	 }
}T;

六、例题

【模板】普通平衡树

【模板】普通平衡树(数据加强版)

【模板】文艺平衡树

【模板】可持久化平衡树

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

平衡树题单

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可持久化splay是一种数据结构,它是对splay树进行修改和查询的一种扩展。在传统的splay树中,对树的修改操作会破坏原有的树结构,而可持久化splay树则允许我们对树进行修改、查询,并且可以保存修改后的每个版本的树结构。 在可持久化splay树中,我们不会直接对原树进行修改,而是通过复制每个节点来创建新的版本。这样,每个版本都可以独立地修改和查询,保留了原有版本的结构和状态。每个节点保存了其左子树和右子树的引用,使得可以在不破坏原有版本的情况下进行修改和查询。 为了实现可持久化splay树,我们可以使用一些技巧,比如引用中提到的哨兵节点和假的父节点和孩子节点。这些技巧可以帮助我们处理根节点的旋转和其他操作。 此外,可持久化splay树还可以与其他数据结构相结合,比如引用中提到的可持久化线段树。这种结合可以帮助我们解决更复杂的问题,比如区间修改和区间查询等。 对于可持久化splay树的学习过程,可以按照以下步骤进行: 1. 理解splay树的基本原理和操作,包括旋转、插入、删除和查找等。 2. 学习如何构建可持久化splay树,包括复制节点、更新版本和保存历史版本等。 3. 掌握可持久化splay树的常见应用场景,比如区间修改和区间查询等。 4. 深入了解与可持久化splay树相关的其他数据结构和算法,比如可持久化线段树等。 在解决问题时,可以使用二分法来确定答案,一般称为二分答案。通过对答案进行二分,然后对每个答案进行检查,以确定最终的结果。这种方法可以应用于很多问题,比如引用中提到的在线询问问题。 综上所述,可持久化splay是一种对splay树进行修改和查询的扩展,可以通过复制节点来创建新的版本,并且可以与其他数据结构相结合解决更复杂的问题。学习过程中可以按照一定的步骤进行,并且可以使用二分法来解决一些特定的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [[学习笔记]FHQ-Treap及其可持久化](https://blog.csdn.net/weixin_34283445/article/details/93207491)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [可持久化数据结构学习笔记](https://blog.csdn.net/weixin_30376083/article/details/99902410)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值