算法随笔——平衡树 FHQ-Treap(附笛卡尔树)

参考博客:https://www.cnblogs.com/TimelessWelkinBlog/p/17610065.html
https://blog.csdn.net/luhaoren2009/article/details/131880277
https://henryhuang.blog.luogu.org/solution-p4402#
https://blog.csdn.net/yyh_getAC/article/details/125710091
感谢大佬的博客。

FHQ-Treap 介绍

FHQ-Treap 可以说是最强大的平衡树之一,具有马亮小、拓展性强等特点,其简洁的写法让人们浴霸不能

FHQ-Treap 本质上还是一颗 Treap 树,但没有 Treap 中令人头晕的左转右转,取而代之的只有两个简洁的函数 MergeSplit,即合并和分裂。通过这两个函数,可以几乎实现 Treap 和 Splay 树中的所有功能。

分裂

分裂,又称 split 操作,是将一个 Treap 树分裂成两棵 Treap 树的操作。
可分为按权值分裂和按大小分裂两种操作。

按权值分裂

将原树分裂为一棵权值均小于等于 x 的树,另一棵权值均大于 x 的树。
可以把这里的 l,r 当成是分裂出的左右子树接下来要接上其他数的地方,具体见代码。

// u是递归的当前节点,val是值.
// l是分裂的左树(<=val)当前的节点,r是右树(>val)当前的节点。
void split(int u,int val,int &l,int &r) //按照权值分裂
{
	if (u == 0) {l = r = 0; return;}
	//比val说明左子树都小,全部分裂到左边
	//这里递归的l = t[u].r 是因为已经接上了u,继续从其左子树递归。
	if (t[u].val <= val) l = u,split(t[u].r,val,t[u].r,r);
	else r = u,split(t[u].l,val,l,t[u].l); //同理
	pushup(u);
}

见图:
image
image
image
image
image

顺便看一下 pushup 操作:

il void pushup(int x)
{
	t[x].size = t[t[x].l].size + t[t[x].r].size + 1;
}

按大小分裂

其实也是一样的,就是分裂成一棵大小为 x的树和另一棵树,直接见代码。

void split(int u,int siz,int &l,int &r)
{
	if (u == 0){l = r = 0;return;}
	pushdown(u);
	//size要减去左子树加自己
	if (t[t[u].l].size < siz)l = u,split(t[u].r,siz-t[t[u].l].size-1,t[u].r,r);
	else r = u,split(t[u].l,siz,l,t[u].l);
	pushup(u);
}

合并

是将两个树合并在一起的操作。
这里需要用到 Treap 的经典思想:随机,通过随机权值使得整棵树更加平衡。
在这里要保证l的所有节点均小于等于r的任何节点

int merge(int l,int r) //l,r分别指左右子树
{
	if (!l || !r) return l + r;
	if (t[l].dat < t[r].dat) // dat 随机值
	{
		t[r].l = merge(l,t[r].l); //将r的左子树和l相合并
		pushup(r); //同Treap的pushup
		return r;
	}
	else
	{
		t[l].r = merge(t[l].r,r); //同上
		pushup(l);
		return l;
	}
}

拓展操作

自此,FHQ-Treap 的所有主要函数就没了,一共就两个。
但根据这两个函数的组合,可以拓展出很多操作。

插入

思路:要插入 x,将树分为 AxB>x 两棵树,依次合并 A,x,B 即可。

void insert(int x)
{
	int l,r;
	split(root,x,l,r);
	New(x);
	root = merge(merge(l,idx),r); //记得更新根节点
}

注意 Treap 树中是没有 cnt 的,相同的数单独存储。

删除

思路:分裂出 AxB>x 两棵树,再从 A 中分裂出 Cx1Dx1<valx 两棵树,将 D 删除即可。

int delnum(int x)
{
	int L,M,R;
	split(root,x,L,R);
	split(L,x-1,L,M); // x-1 < M <= x 即 M树均为x
	M = merge(t[M].l,t[M].r);//合并左右子树即删掉根节点
	root = merge(merge(L,M),R);//合并回去
	/*
	一次删除所有x:
	root = merge(L,R);
	*/
	return root;
}

通过权值查询排名

思路:直接分裂出一棵 <=x1 的树,排名为该树的大小 + 1。

int getrank(int val)
{
	int L,R,res;
	split(root,val-1,L,R);
	res = t[L].size + 1;
	root = merge(L,R);
	return res;
}

根据排名查询权值

思路:与 Treap 中的一样,递归查询,若左边超过了该排名,则递归左边。否则递归右边。

int getval(int u,int rank)
{
	if (rank == t[t[u].l].size + 1) return u;
	if (rank <= t[t[u].l].size) return getval(t[u].l,rank);
	return getval(t[u].r,rank-t[t[u].l].size-1);
}

求前驱

思路:分裂出一棵 <=x1 的树,查这棵树的最大值。

int pre(int x)
{
	int L,R,res;
	split(root,x-1,L,R);
	res = t[getval(L,t[L].size)].val;
	root = merge(L,R);
	return res;
}

求后继

思路:分裂出一棵 >x 的树,查这棵树最小值。

int suc(int x)
{
	int L,R,res;
	split(root,x,L,R);
	res = t[getval(R,1)].val;
	root = merge(L,R);
	return res;
}

FHQ-Treap 例题

【模板】普通平衡树

P3369

题目描述

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

  1. 插入一个数 x
  2. 删除一个数 x(若有多个相同的数,应只删除一个)。
  3. 定义排名为比当前数小的数的个数 +1。查询 x 的排名。
  4. 查询数据结构中排名为 x 的数。
  5. x 的前驱(前驱定义为小于 x,且最大的数)。
  6. x 的后继(后继定义为大于 x,且最小的数)。

对于操作 3,5,6,不保证当前数据结构中存在数 x

平衡树模板,直接见代码。

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
struct node
{
	int l,r,val,dat,size;
}t[N];
int n,idx,root;
int New(int x) //新建节点并初始化
{
	idx++;
	t[idx].val = x;
	t[idx].dat = rand();
	t[idx].size = 1;
	t[idx].l = t[idx].r = 0;
	return idx;
}
void pushup(int x) // 更新size
{
	t[x].size = t[t[x].l].size + t[t[x].r].size + 1;
}
// u是递归的当前节点,val是值.
// x是分裂的左树(<=val),y是右树(>val)。
void split(int u,int val,int &l,int &r) //按照权值分裂
{
	if (u == 0) {l = r = 0; return;}
	//比val说明左子树都小,全部分裂到左边
	if (t[u].val <= val) l = u,split(t[u].r,val,t[u].r,r);
	else r = u,split(t[u].l,val,l,t[u].l);
	pushup(u);
}
int merge(int l,int r)
{
	if (!l || !r)  return l + r;
	if (t[l].dat  < t[r].dat) //按照随机权值合并
	{
		t[r].l = merge(l,t[r].l);
		pushup(r);
		return r;	
	}
	else
	{
		t[l].r = merge(t[l].r,r);
		pushup(l);
		return l;
	}
}
void insert(int x)
{
	int l,r;
	split(root,x,l,r);
	New(x);
	root = merge(merge(l,idx),r);
}
int delnum(int x)
{
	int L,M,R;
	split(root,x,L,R);
	split(L,x-1,L,M); // x-1 < M <= x 即 M树均为x
	M = merge(t[M].l,t[M].r);//合并左右子树即删掉根节点
	root = merge(merge(L,M),R);//合并回去
	/*
	一次删除所有x:
	root = merge(L,R);
	*/
	return root;
}
int getrank(int val)
{
	int L,R,res;
	split(root,val-1,L,R);
	res = t[L].size + 1;
	root = merge(L,R);
	return res;
}
int getval(int u,int rank)
{
	if (rank == t[t[u].l].size + 1) return u;
	if (rank <= t[t[u].l].size) return getval(t[u].l,rank);
	return getval(t[u].r,rank-t[t[u].l].size-1);
}
int pre(int x)
{
	int L,R,res;
	split(root,x-1,L,R);
	res = t[getval(L,t[L].size)].val;
	root = merge(L,R);
	return res;
}
int suc(int x)
{
	int L,R,res;
	split(root,x,L,R);
	res = t[getval(R,1)].val;
	root = merge(L,R);
	return res;
}
int main()
{
	scanf("%d",&n);
	while (n--)
	{
		int opt,x;
		scanf("%d%d",&opt,&x);
		if (opt == 1) insert(x);
		else if (opt == 2) delnum(x);
		else if (opt == 3) printf("%d\n",getrank(x));
		else if (opt == 4) printf("%d\n",t[getval(root,x)].val);
		else if (opt == 5) printf("%d\n",pre(x));
		else printf("%d\n",suc(x));
	}
	
	return 0;
}

P3391 【模板】文艺平衡树

链接
一句话题意:维护一个序列,支持区间翻转操作。
这里需要用到 split 操作中的按照大小分裂。
设翻转区间为 [l,r]
将原树分裂出 Ar,再从 A 中分裂出 Bl1Cl1<valr 两棵树,这样就分裂出需要操作的树 C 了。

再思考区间翻转,因为 BST 中序遍历后得到原序列,所以翻转其本质上就是每个节点从上到下交换左右子树,相当于中序遍历从左中右的顺序变成右中左,即区间翻转。

同时,我们肯定不能用暴力一个一个节点去交换,可以借用线段树中 lazytag 的思想,这样这题就搞定了。

// Problem: P3391 【模板】文艺平衡树
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3391
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author: Eason
// Date:2024-01-20 08:11:57
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
const int N = 100005;
int n,m,a[N];
struct node
{
	int val,dat,size,l,r,tag;
}t[N];
int idx = 0,root = 0;
int New(int x)
{
	++idx;
	t[idx].val = x;
	t[idx].dat = rand();
	t[idx].l = t[idx].r = 0;
	t[idx].size = 1;
	return idx;
}
void pushup(int x)
{
	t[x].size = t[t[x].l].size + t[t[x].r].size + 1;
}
void pushdown(int x)
{
	if (t[x].tag) 
	{
		swap(t[x].l,t[x].r);
		t[x].tag = 0;
		t[t[x].l].tag ^= 1;  
		t[t[x].r].tag ^= 1;
	}
}
void split(int u,int siz,int &l,int &r)
{
	if (u == 0){l = r = 0;return;}
	pushdown(u);
	if (t[t[u].l].size < siz)  l = u,split(t[u].r,siz-t[t[u].l].size-1,t[u].r,r);
	else r = u,split(t[u].l,siz,l,t[u].l);
	pushup(u);
}
int merge(int l,int r) //l,r分别指左右子树
{
	if (!l || !r) return l + r;
	if (t[l].dat < t[r].dat) // dat 随机值
	{
		pushdown(r); //先下传标记
		t[r].l = merge(l,t[r].l); //将r的左子树和l相合并
		pushup(r); //同Treap的pushup
		return r;
	}
	else
	{
		pushdown(l);
		t[l].r = merge(t[l].r,r); //同上
		pushup(l);
		return l;
	}
}

void output(int x)
{
	if (!x) return;
	pushdown(x);
	output(t[x].l); //中序遍历输出
	printf("%d ",t[x].val);
	output(t[x].r);
}


int main()
{
 	cin >> n >> m;
 	for (int i = 1;i <= n;i++) 
 		root = merge(root,New(i)); //按照顺序插入平衡树
 	
 	for (int i = 1;i <= m;i++)
 	{
 		int l,r ;
 		cin >> l >> r;
		int L,M,R;
		split(root,r,L,R);
		split(L,l-1,L,M);
		t[M].tag ^= 1;
		root = merge(merge(L,M),R);
 	}
 	output(root);
 	return 0;
} 

[Cerc2007] robotic sort 机械排序

P4402

题目描述

SORT公司是一个专门为人们提供排序服务的公司,该公司的宗旨是:“顺序是最美丽的”。他们的工作是通过一系列移动,将某些物品按顺序摆好。他们的工作规定只能使用如下方法排序:

先找到编号最小的物品的位置P1,将区间[1,P1]反转,再找到编号第二小的物品的位置P2,将区间[2,P2]反转.........

上图是有6个物品的例子,编号最小的一个是在第4个位置。因此,最开始把前面4个物品反转,第二小的物品在最后一个位置,所以下一个操作是把2-6的物品反转,第三步操作是把3-4的物品进行反转……

在数据中可能存在有相同的编号,如果有多个相同的编号,则按输入的原始次序操作。

暂略。

posted @   codwarm  阅读(42)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示