P3369 【模板】普通平衡树 Treap树堆学习笔记

Treap = Tree + Heap (树堆 = 树 + 堆)

大佬的博客https://www.luogu.com.cn/blog/HOJQVFNA/qian-xi-treap-ping-heng-shu

简介 : 本质上是一种使用 随机权值维护堆的性质的二叉查找树, 通过基础的树旋转维持堆的性质,
由随机权值保证树高度为 logN

具体做法 : 插入或删除后, 若不满足 堆的性质 则旋转
优点 : 防止 退化成链表 简单好写, 是最好写的平衡树之一, 好理解
缺点 : 常数大

在这里插入图片描述


1. 例题1 洛谷P3369 【模板】普通平衡树

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

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


1. 结构体定义如下
#define lson(u) (tree[u].chl[0])
#define rson(u) (tree[u].chl[1])
struct Node {
	int chl[2],     //chl[0]是左儿子 , chl[1]是右儿子
		val,        //节点值
		sz,         //以该节点的为根节点个数
		recy,       //该节点重复了几次
		rnd;        //随机优先级 
} tree[MAXN<<4];
2. 向上更新以u为根的树的节点个数
  1. 当前节点u的sz = 左子树sz + 右子树sz + u的重复次数recy(U)
inline void push_up(int u) {
	//更新节点以u为根的树的节点个数
	tree[u].sz = tree[lson(u)].sz + tree[rson(u)].sz + tree[u].recy;
}
3. 旋转操作 route
  1. 我们可以把左旋和右旋合并成为一个统一的函数
  2. 通过传入的操作数cmd实现相应的旋转
#define LROUTE 0
#define RROUTE 1
void route(int& u, int cmd) { //cmd=0左旋, cmd=1右旋
	int tmp = tree[u].chl[cmd^1];
	tree[u].chl[cmd^1] = tree[tmp].chl[cmd];
	tree[tmp].chl[cmd] = u;
	push_up(u), push_up(tmp); //不要忘了更新对应的sz
	u = tmp;
}
4. 插入操作 insert
  1. 和普通BST一样的插入, 并建立随机权值即可
  2. 插入后, 需要向上更新节点的 以该节点的为根节点个数sz
void insert(int& u, int key) {
	if(!u) { //走到叶子就新建节点
		u = ++tot;
		tree[u].sz = tree[u].recy = 1;
		tree[u].val = key;
		tree[u].rnd = rand(); //随机权值
		return ;
	}

	tree[u].sz ++;
	if(key == tree[u].val) { tree[u].recy ++; return ; }
	int cmd = (key > tree[u].val); //大于则向右插入
	insert(tree[u].chl[cmd], key);

	//插入后维护大根堆的性质
	//失衡的时候, 往与插入相反的方向旋转
	if(tree[u].rnd < tree[tree[u].chl[cmd]].rnd)
		route(u, cmd^1);
	push_up(u);
}
5. 删除操作 del
  1. 和普通的BST一样的删除, 分为 4种情况
    1. 没有这值为 key 的节点, 直接返回
    2. 当前根和key不相等, 就去相应的子节点解决
    3. 叶子节点, 直接删除
    4. 有左儿子, 没有右儿子, 右旋并去右儿子解决 (因为右旋后root转到了rig)
    5. 没有左儿子, 有右儿子, 左旋并去左儿子解决(因为左旋后root转到了lef)
    6. 左右都有, 就把 大的转上来 并去 另一个儿子 解决
  2. 使用 堆删除的方式, 把key 旋转到叶子 节点后删除
void del(int& u, int key) {
	if(!u) return ;

	if(key < tree[u].val) del(lson(u), key);
	else if(key > tree[u].val) del(rson(u), key);
	else { //4种删除情况
		if(!lson(u) && !rson(u)) { //叶子节点直接删除
			tree[u].sz --,
			tree[u].recy --;
			if(tree[u].recy == 0) u = 0;
		} else if(lson(u) && !rson(u)) { //只有左节点
			route(u, 1);
			del(rson(u), key);
		} else if(!lson(u) && rson(u)) { //只有右节点
			route(u, 0);
			del(lson(u), key);
		} else if(lson(u) && rson(u)) { //左右都有
			//把大的那个转上来,并去另一个子树解决
			int cmd = (tree[lson(u)].rnd > tree[rson(u)].rnd);
			route(u, cmd);
			del(tree[u].chl[cmd], key);
		}
	}
	push_up(u);
}
6. 查询排名rand_x
  1. 还是比较key, 小于往左, 大于往右
  2. 往右说明 root的左儿子中的所有节点都 小于key
    所以排名应加上 左儿子的节点的个数根节点的重复次数
int rank_x(int u, int key) { //求key的排名
	if(!u) return 0;
	if(key == tree[u].val) //返回左子树个数 + 1
		return tree[lson(u)].sz + 1;
	else if(key < tree[u].val) //小于就去左子树找
		return rank_x(lson(u), key);
	else //大于就去右子树, 类似于权值线段树
		return   tree[lson(u)].sz +
				 tree[u].recy +
				 rank_x(rson(u), key);
}
7. 查询 K 小值 kth
  1. 如果左子树节点个数大于等于 k , 说明 k 小值还在左子树
  2. 如果左子树的节点个数加上根的重复次数仍然小于 k, 说明k小值在右子树
int kth(int u, int k) { //查询k小值
	if(!u) return 0;
	if(tree[lson(u)].sz >= k) return kth(lson(u), k);
	else if(tree[lson(u)].sz + tree[u].recy < k)
		return kth(rson(u), k-tree[lson(u)].sz-tree[u].recy);
	else return tree[u].val;
}
8. 求key的前驱 get_pre
  1. 只要根仍然大于等于key, 就向左子树找
  2. 向右走记得取max, 前驱定义为小于key,且最大的数
int get_pre(int u, int key) {
	if(!u) return -INF;
	if(key <= tree[u].val) 
		return get_pre(lson(u), key);
	else
		return max(tree[u].val, get_pre(rson(u), key));
}
9. 求key的后继 get_suf
int get_suf(int u, int key) {
	if(!u) return INF;
	if(key >= tree[u].val) 
		return get_suf(rson(u), key);
	else
		return min(tree[u].val, get_suf(lson(u), key));
}
完整代码
// #define debug
#ifdef debug
#include <time.h>
#include "win_majiao.h"
#endif

#include <iostream>
#include <algorithm>
#include <vector>
#include <string.h>
#include <map>
#include <set>
#include <stack>
#include <queue>
#include <math.h>

#define MAXN ((int)1e5+7)
#define ll long long int
#define INF (0x7f7f7f7f)
#define QAQ (0)

using namespace std;
typedef vector<vector<int> > VVI;

#define show(x...) \
	do { \
		cout << "[" << #x << " -> "; \
		err(x); \
	} while (0)

void err() { cout << "]" << endl; }
template<typename T, typename... A>
void err(T a, A... x) { cout << a << ' '; err(x...); }

namespace FastIO{

	char print_f[105];
	void read() {}
	void print() { putchar('\n'); }

	template <typename T, typename... T2>
	   inline void read(T &x, T2 &... oth) {
		   x = 0;
		   char ch = getchar();
		   ll f = 1;
		   while (!isdigit(ch)) {
			   if (ch == '-') f *= -1; 
			   ch = getchar();
		   }
		   while (isdigit(ch)) {
			   x = x * 10 + ch - 48;
			   ch = getchar();
		   }
		   x *= f;
		   read(oth...);
	   }
	template <typename T, typename... T2>
	   inline void print(T x, T2... oth) {
		   ll p3=-1;
		   if(x<0) putchar('-'), x=-x;
		   do{
				print_f[++p3] = x%10 + 48;
		   } while(x/=10);
		   while(p3>=0) putchar(print_f[p3--]);
		   putchar(' ');
		   print(oth...);
	   }
} // namespace FastIO
using FastIO::print;
using FastIO::read;

int n, m, Q, K, root, tot;

#define lson(u) (tree[u].chl[0])
#define rson(u) (tree[u].chl[1])
struct Node {
	int chl[2], //chl[0]是左儿子 , chl[1]是右儿子
		val, //节点值
		sz, //该节点的为根节点个数
		recy, //该节点重复了几次
		rnd; //随机优先级 
} tree[MAXN<<4];


void push_up(int u) {
	//更新节点以u为根的树的节点个数
	tree[u].sz = tree[lson(u)].sz + tree[rson(u)].sz + tree[u].recy;
}

#define LROUTE 0
#define RROUTE 1
void route(int& u, int cmd) { //cmd=0左旋, cmd=1右旋
	int tmp = tree[u].chl[cmd^1];
	tree[u].chl[cmd^1] = tree[tmp].chl[cmd];
	tree[tmp].chl[cmd] = u;
	push_up(u), push_up(tmp);
	u = tmp;
}

void insert(int& u, int key) {
	if(!u) {
		u = ++tot;
		tree[u].sz = tree[u].recy = 1;
		tree[u].val = key;
		tree[u].rnd = rand(); //随机权值
		return ;
	}

	tree[u].sz ++;
	if(key == tree[u].val) { tree[u].recy ++; return ; }
	int cmd = (key > tree[u].val); //大于则向右插入
	insert(tree[u].chl[cmd], key);

	//插入后维护大根堆的性质
	//失衡的时候, 往与插入相反的方向旋转
	if(tree[u].rnd < tree[tree[u].chl[cmd]].rnd)
		route(u, cmd^1);
	push_up(u);
}

void del(int& u, int key) {
	if(!u) return ;

	if(key < tree[u].val) del(lson(u), key);
	else if(key > tree[u].val) del(rson(u), key);
	else { //4种删除情况
		if(!lson(u) && !rson(u)) { //叶子节点直接删除
			tree[u].sz --,
			tree[u].recy --;
			if(tree[u].recy == 0) u = 0;
		} else if(lson(u) && !rson(u)) { //只有左节点
			route(u, 1);
			del(rson(u), key);
		} else if(!lson(u) && rson(u)) { //只有右节点
			route(u, 0);
			del(lson(u), key);
		} else if(lson(u) && rson(u)) { //左右都有
			//把大的那个转上来,并去另一个子树解决
			int cmd = (tree[lson(u)].rnd > tree[rson(u)].rnd);
			route(u, cmd);
			del(tree[u].chl[cmd], key);
		}
	}
	push_up(u);
}

int rank_x(int u, int key) { //求key的排名
	if(!u) return 0;
	if(key == tree[u].val) //返回左子树个数 + 1
		return tree[lson(u)].sz + 1;
	else if(key < tree[u].val) //小于就去左子树找
		return rank_x(lson(u), key);
	else //大于就去右子树, 类似于权值线段树
		return   tree[lson(u)].sz +
				 tree[u].recy +
				 rank_x(rson(u), key);
}

int kth(int u, int k) { //查询k大值
	if(!u) return 0;
	if(tree[lson(u)].sz >= k) return kth(lson(u), k);
	else if(tree[lson(u)].sz + tree[u].recy < k)
		return kth(rson(u), k-tree[lson(u)].sz-tree[u].recy);
	else return tree[u].val;
}

int get_pre(int u, int key) {
	if(!u) return -INF;
	if(key <= tree[u].val) 
		return get_pre(lson(u), key);
	else
		return max(tree[u].val, get_pre(rson(u), key));
}

int get_suf(int u, int key) {
	if(!u) return INF;
	if(key >= tree[u].val) 
		return get_suf(rson(u), key);
	else
		return min(tree[u].val, get_suf(lson(u), key));
}

signed main() {
#ifdef debug
	freopen("test.txt", "r", stdin);
	clock_t stime = clock();
#endif

	read(n);
	int cmd, val, ans;
	for(int i=1; i<=n; i++) {
		read(cmd, val);
		switch (cmd) {
		case 1:
			insert(root, val);
			break;
		case 2:
			del(root, val);
			break;
		case 3:
			ans = rank_x(root, val);
			printf("%d\n", ans);
			break;
		case 4:
			ans = kth(root, val);
			printf("%d\n", ans);
			break;
		case 5:
			ans = get_pre(root, val);
			printf("%d\n", ans);
			break;
		case 6:
			ans = get_suf(root, val);
			printf("%d\n", ans);
			break;
		default:
			break;
		}
	}
#ifdef debug
	clock_t etime = clock();
	printf("rum time: %lf 秒\n",(double) (etime-stime)/CLOCKS_PER_SEC);
#endif 
	return 0;
}

扩展应用

1. 线段树套平衡树,求区间前驱后继排名(就是线段树的每个节点都是一个平衡树)
2. 伸展树, 区间反转分割
3. 双端优先队列的实现 (可以 取最大最小值的堆)
  1. 使用Treap实现取出最大最小值即可
4. 游戏排名系统 (动态数据容器) 原文链接https://blog.csdn.net/yang_yulei/article/details/46005845
  1. 实现三种操作
    • 上传一条得分记录
    • 查看当前排名
    • 返回区间 [L,R] 内的排名记录
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值