treap入门
这几天刚学了treap,听起来还行,就是调题调到恶心了……
就以这道题作为板子吧(”你本来也就做了一道题!”)
https://www.luogu.org/problemnew/show/P3369
先谈谈我对treap的理解
treap是一种二叉搜索树,二叉搜索树是这么一回事:
1.可以是一棵空树
2.若不空,那么左子树上所有节点的值都小于根节点的值,右子树上所有节点的值都大于根节点的值
3.左右子树分别为一棵二叉搜索树
treap是由tree和heap组合而来的,可见他还满足堆的性质。
通过随机一个额外的数值作为优先级,来构成一个堆。可以证明,随机顺序建立的二叉排序树的期望高度是O(logn)(虽然我不会证),所以我认为,之所以用这个随机值,就是为了防止树退化成一个链的情况,导致时间复杂度变大
treap最重要也是最基础的操作是旋转,分为左右旋,为什么要有旋转呢?拿插入举例,插入一个节点当然是先根据二叉搜索树的性质插入到该插入的叶节点,
但是为了防止其退化成一条链,我们就需要通过旋转操作使该节点的优先级满足堆的性质。
总结一下,旋转操作是为了在符合二叉搜索树的前提下,让树满足堆的性质。
那具体怎么转呢?由上文可知,改变两个节点的父子关系的同时,还有满足二叉搜索树的性质,即旋转不影响二叉搜索树的性质。
直接上图:
(感谢hzh巨佬的图)
从图中可以很直观的看出,通过右旋改变了左孩子和根节点的父子关系,通过右旋改变了右孩子和根节点的关系。
而且可以验证,这么做二叉搜索树的性质并没有改变。
于是左右旋的代码就可以模拟出来
void right_rotate(int& Q)
{
int P = lson[Q];
lson[Q] = rson[P]; //这个和下面那句不能反
rson[P] = Q;
update(Q); update(P);
Q = P;
}
void left_rotate(int& Q)
{
int P = rson[Q];
rson[Q] = lson[P];
lson[P] = Q;
update(Q); update(P);
Q = P;
}
其中update函数用来维护该节点储存的信息,比如字数大小,该节点深度
那咱们现在看看题吧。
对了,我维护的是小根堆,大根堆也应该是一样的吧,没试过
先说一下变量含义:
int n, root = ; //n种操作;root记录根节点是谁(因为进行某一操作后,根节点可能改变,所以要随时记录)
int cnt = , lson[maxn], rson[maxn]; //cnt:节点总数(即每一个节点的编号);lson[now],rson[now]:节点now的左右孩子
int val[maxn], ran[maxn], size[maxn], Cnt[maxn]; //val[now]:节点now的权值;ran[now]:随机出来的优先级;size[now]:子树大小;
4 //Cnt[now]记录和val[now]相同的节点多少个(用来处理数字重复)
第一种操作是插入。上文已述,插入是先按二叉搜索树的性质插入到叶节点,在通过旋转满足堆的性质
void insert(int& now, int v)
{
if(!now) //找到要插入的叶节点了
{
now = ++cnt; //新建节点
val[now] = v;
size[now] = Cnt[now] = ;
ran[now] = rand(); //随机优先级
return;
}
if(val[now] == v) Cnt[now]++; //若树中已经有了该数,就直接Cnt[]++了
else if(val[now] > v) //说明在左子树
{
insert(lson[now], v); //递归寻找
if(ran[lson[now]] < ran[now]) right_rotate(now);
//这一步放在了递归后面,说明此时节点已经插入好了(而且只是修改了左子树),那就判断并通过旋转维护堆
}
else
{
insert(rson[now], v);
if(ran[rson[now]] < ran[now]) left_rotate(now);
}
update(now);
}
第二种操作是删除。这咋办呢?如果找到后直接删除该点,那么他子树们就不知道该去哪儿了,显然乱套。
那怎么办呢,别忘了,旋转可以改变两个节点的父子关系,而仍不破坏这棵树,所以我们就可已找到他后,将他旋转下去直到叶节点,这时候再删除,就没什么关系了
void del(int& now, int v)
{
if(!now) return;
if(val[now] == v) //找到了该数
{
if(Cnt[now] > ) //有重复
{
Cnt[now]--;
update(now); return;
}
else if(lson[now] && rson[now]) //并没有旋转到根节点
{
left_rotate(now); //只要选任意一棵子树旋转就行
del(lson[now], v); //这两句等价于right_rotate(now); del(rson[now], v);
}
else //代表只剩一个孩子了,那么就直接用他的孩子代替他,相当于把他删除
{
now = lson[now] | rson[now]; //等价于now = lson[now] ? lson[now] : rson[now]
update(now); return;
}
}
else if(val[now] > v) del(lson[now], v); //没找到就接着找
else del(rson[now], v);
update(now);
}
第三种操作是查询排名,size[]就派上用场了。这跟线段树的查询第k小很像,就是查询到右子树时别忘加上左子树的大小和该点的重复个数
int Find_id(int now, int v)
{
if(!now) return ;
if(val[now] == v) return size[lson[now]] + ; //别忘加上自己
if(val[now] > v) return Find_id(lson[now], v);
else return Find_id(rson[now], v) + size[lson[now]] + Cnt[now];
}
第四种操作是查询排名为x的数,和操作3逻辑上很想,只不过有些步骤相反,还是看代码和注释比较直观
int Find_num(int now, int id)
{
if(!now) return INF;
if(size[lson[now]] >= id) return Find_num(lson[now], id); //在左子树
else if(id <= size[lson[now]] + Cnt[now]) return val[now]; //在左子树和自己,但因为左子树的已经走上面的语句了,就指自己
else return Find_num(rson[now], id - size[lson[now]] - Cnt[now]); //右子树,别忘减去(跟线段树找第k小挺像)
}
第五种操作是查询x的前驱。首先如果当前节点的权值比v大,那么很显然应该去左子树中找;如果当前节点的权值比v小,说明v的前驱在左子树或者当前节点就是他的前驱,但因为当前节点的左子树的右子树中(没绕,没绕)也可能存在v的前驱,所以就要在这两者之中取max,具体看代码吧
int Pre(int now, int v)
{
if(!now) return -INF;
if(val[now] < v) return max(val[now], Pre(rson[now], v)); //前驱在右子树或是当前节点
else return Pre(lson[now], v);
}
前驱都会了,那后继还会远吗? --雪mrclr莱
int Nex(int now, int v)
{
if(!now) return INF; //相当于停止搜索和比较
if(val[now] > v) return min(val[now], Nex(lson[now], v));
else return Nex(rson[now], v);
}
总算写完了,发一下完整代码
#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<cctype>
using namespace std;
#define enter printf("\n")
#define space printf(" ")
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int maxn = 1e5 + ;
inline ll read()
{
ll ans = ;
char ch = getchar(), last = ' ';
while(!isdigit(ch)) {last = ch; ch = getchar();}
while(isdigit(ch))
{
ans = ans * + ch - ''; ch = getchar();
}
if(last == '-') ans = -ans;
return ans;
}
inline void write(ll x)
{
if(x < ) x = -x, putchar('-');
if(x >= ) write(x / );
putchar('' + x % );
} int n, root = ;
int cnt = , lson[maxn], rson[maxn];
int val[maxn], ran[maxn], size[maxn], Cnt[maxn];
void update(int now)
{
if(!now) return;
size[now] = size[lson[now]] + size[rson[now]] + Cnt[now];
}
void right_rotate(int& Q)
{
int P = lson[Q];
lson[Q] = rson[P];
rson[P] = Q;
update(Q); update(P);
Q = P;
}
void left_rotate(int& Q)
{
int P = rson[Q];
rson[Q] = lson[P];
lson[P] = Q;
update(Q); update(P);
Q = P;
}
void insert(int& now, int v)
{
if(!now)
{
now = ++cnt;
val[now] = v;
size[now] = Cnt[now] = ;
ran[now] = rand();
return;
}
if(val[now] == v) Cnt[now]++;
else if(val[now] > v)
{
insert(lson[now], v);
if(ran[lson[now]] < ran[now]) right_rotate(now);
}
else
{
insert(rson[now], v);
if(ran[rson[now]] < ran[now]) left_rotate(now);
}
update(now);
}
void del(int& now, int v)
{
if(!now) return;
if(val[now] == v)
{
if(Cnt[now] > )
{
Cnt[now]--;
update(now); return;
}
else if(lson[now] && rson[now])
{
left_rotate(now);
del(lson[now], v);
}
else
{
now = lson[now] | rson[now];
update(now); return;
}
}
else if(val[now] > v) del(lson[now], v);
else del(rson[now], v);
update(now);
}
int Find_id(int now, int v)
{
if(!now) return ;
if(val[now] == v) return size[lson[now]] + ;
if(val[now] > v) return Find_id(lson[now], v);
else return Find_id(rson[now], v) + size[lson[now]] + Cnt[now];
}
int Find_num(int now, int id)
{
if(!now) return INF;
if(size[lson[now]] >= id) return Find_num(lson[now], id);
else if(id <= size[lson[now]] + Cnt[now]) return val[now];
else return Find_num(rson[now], id - size[lson[now]] - Cnt[now]);
}
int Pre(int now, int v)
{
if(!now) return -INF;
if(val[now] < v) return max(val[now], Pre(rson[now], v));
else return Pre(lson[now], v);
}
int Nex(int now, int v)
{
if(!now) return INF;
if(val[now] > v) return min(val[now], Nex(lson[now], v));
else return Nex(rson[now], v);
} int main()
{
n = read();
while(n--)
{
int d = read(), x = read();
if(d == ) insert(root, x);
else if(d == ) del(root, x);
else if(d == ) {write(Find_id(root, x)); enter;}
else if(d == ) {write(Find_num(root, x)); enter;}
else if(d == ) {write(Pre(root, x)); enter;}
else {write(Nex(root, x)); enter;}
}
return ;
}
彩蛋:找名次写错了能MLE……求大佬解答……
treap入门的更多相关文章
- Treap入门(转自NOCOW)
Treap 来自NOCOW Treap,就是有另一个随机数满足堆的性质的二叉搜索树,其结构相当于以随机顺序插入的二叉搜索树.其基本操作的期望复杂度为O(log n). 其特点是实现简单,效率高于伸展树 ...
- poj2761(treap入门)
给n个数,然后m个询问,询问任意区间的第k小的数,特别的,任意两个区间不存在包含关系, 也就是说,将所有的询问按L排序之后, 对于i<j , Li < Lj 且 Ri < Rj ...
- 【bzoj3173-最长上升子序列-一题两解】
这道题不就是简单的DP吗,BZOJ在水我!不,你是错的. ·本题特点: 不断向不同位置插入数字(按数字1,2,3,4,5,6……),需要求出每一次插入后的最长上升子序列. ·分析 ...
- 入门平衡树: Treap
入门平衡树:\(treap\) 前言: 如有任何错误和其他问题,请联系我 微信/QQ同号:615863087 前置知识: 二叉树基础知识,即简单的图论知识. 初识\(BST\): \(BST\)是\( ...
- [转载]无旋treap:从好奇到入门(例题:bzoj3224 普通平衡树)
转载自ZZH大佬,原文:http://www.cnblogs.com/LadyLex/p/7182491.html 今天我们来学习一种新的数据结构:无旋treap.它和splay一样支持区间操作,和t ...
- [您有新的未分配科技点]无旋treap:从好奇到入门(例题:bzoj3224 普通平衡树)
今天我们来学习一种新的数据结构:无旋treap.它和splay一样支持区间操作,和treap一样简单易懂,同时还支持可持久化. 无旋treap的节点定义和treap一样,都要同时满足树性质和堆性质,我 ...
- 快速入门Treap(代码实现)
学习数据结构对我来说真的相当困难,网上讲\(Treap\)的我也看不太懂,前前后后花了大概六天才把\(Treap\)学会.为了避免再次忘记,这里我整理一下\(Treap\)的基础知识和模板. 阅读此文 ...
- 洛谷 2234 [HNOI2002]营业额统计——treap(入门)
题目:https://www.luogu.org/problemnew/show/P2234 学习了一下 treap 的写法. 学习材料:https://blog.csdn.net/litble/ar ...
- Treap(树堆)入门
作者:zifeiy 标签:Treap 首先,我么要知道:Treap=Tree+Heap. 这里: Tree指的是二叉排序树: Heap指的是堆. 所以在阅读这篇文章之前需要大家对 二叉查找树 和 堆( ...
随机推荐
- SQL Server T—SQL 视图 事务
一 视图 视图是存储在数据库中的查询的SQL 语句, 视图是从一个或多个表或视图中导出的表,是一张虚表,只能对视图进行查询,不能增.删.改. 对视图进行修改要在相应的基本表中进行修改,修改会自动的反应 ...
- t3用户-角色-权限hibernate经典配置
用户-角色-权限hibernate经典配置. 既然有人问起,我就写下说明吧.在文章中间的配置文件那里.权当回忆一下,也帮助更多人.这是以前学校时写的,没有注释.都是贴的代码笔记.看到的莫要见怪.欢迎学 ...
- $.each()和$(selector).each()
转载:http://www.jb51.net/article/65215.htm $.each()与$(selector).each()不同, 后者专用于jquery对象的遍历, 前者可用于遍历任何的 ...
- 【游记】CCHO TY国初划水记
没想到第一篇游记竟然是化学国初(其实是上次SXACM时候懒得写 DAY0 一下午做了5个小时的校车,服务区水真贵 肝了4个小时模拟题,颠到崩溃. 下榻在距离山大附不远的一个酒店,高三人好多哇,我们年级 ...
- [日常] Go语言圣经--并发的web爬虫
两种: crawler.go package main import ( "fmt" "links" //"log" "os&qu ...
- Linux安装redis和部署
第一步:下载安装包 访问https://redis.io/download 到官网进行下载.这里下载最新的4.0版本. 第二步:安装 1.通过远程管理工具,将压缩包拷贝到Linux服务器中,执行解压 ...
- 了解java虚拟机—垃圾回收算法(5)
引用计数器法(Reference Counting) 引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器减1.只要对象A的引用计数器的 ...
- 0 or 1(hdu2608)数学题
0 or 1 Time Limit: 6000/2000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submiss ...
- 启动HDFS时datanode无法启动的坑
启动HDFS 启动hdfs,进入sbin目录,也可以执行./start-all.sh - $cd /app/hadoop/hadoop-2.2.0/sbin - $./start-dfs.sh 在此之 ...
- python-享元模式
源码地址:https://github.com/weilanhanf/PythonDesignPatterns 说明: 如果一个软件系统在运行时所创建的相同或相似对象数量太多,将导致运行代价过高,带来 ...