前置知识:二叉搜索树

以下摘自 ↑:

二叉搜索树每次操作访问O(深度)个节点。

在刻意构造的数据中,树的形态会被卡成一条链,于是复杂度爆炸

它的复杂度与直接暴力删除类似。

但二叉搜索树扩展性强。更复杂的\(splay,treap,SGT\)等都基于二叉搜索树,只是通过一些对树的形态的改变来保证操作的复杂度,且保持树中序遍历的形态。

随机数据还是很强势的。

在理解了二叉搜索树之后,我们来看非旋\((fhq)Treap\)。

既然二叉搜索树在刻意构造的数据中会被卡成一条链,那么我们可以考虑对每个结点多维护一个信息,来避免这些个情况\(w\)。

对每个结点,除了它本身的权值\(val\)之外,再附加一个权值\(pri\),要求\(pri_k \geq pri_{son_k}\),即父亲的pri值要大于等于它的儿子的\(pri\)值,而这个\(pri\)值怎么确定呢?它是随机的,也就是rand()!

用非旋\(Treap\)实现普通平衡树,我们只需要5个函数。

一些必要的交代:

\(val[\ ]\) 节点的权值(题目给出

\(pri[ \ ]\) 节点的附加权值(\(rand()\)

\(siz[\ ]\) 节点的大小(子树+自己

\(ch[\ ][0/1]\) 节点的左/右儿子编号(动态开点

首先是非旋\(Treap\)的核心函数:

\(\mathbb{A}.{Split}\) 分裂:

\(split\)的主要思想是将原本的这棵树按照某个值\(x\)分裂成两棵,其中\(\leq x\)的为一棵,\(>x\)的为另一棵

void split(int nd,int a,int &x,int &y) {
if(!nd) x=y=0;
else {
if(val[nd]>a) {
y=nd;
split(ch[nd][0],a,x,ch[nd][0]);
} else {
x=nd;
split(ch[nd][1],a,ch[nd][1],y);
}
update(nd);
}
}

其中\(nd\)表示当前节点,\(a\)表示上面所提到的某个值\(x\),而\(x,y\)表示分裂成的两棵树的根分别是\(x,y\)。

采取递归分裂的方式,如果当前节点的\(val>a\),根据二叉搜索树的性质,要去当前节点的左子树寻找,而可以确定另外一棵子树的根也就是当前节点\(nd\),然后我们递归的去左子树寻找,但是要注意传参的时候,传的是:\(split(ch[nd][0],a,x,ch[nd][0])\),因为我们把\(nd\)节点的左子树的一部分分裂到了另一棵树中,相应的需要改变原来的左儿子的值,让左子树中\(>x\)的部分接到\(nd\)节点的左儿子上。

如果\(val<=a\)同理。

最后我们需要更新\(nd\)节点的信息。

\(\mathbb{B}. merge\) 合并:

将两棵子树重新合并为一棵,要求\(a\)树的最大权值<=\(b\)树的最小权值(对有没有等于产生疑惑,暂且认为有吧

int merge(int a,int b) {
if(!a||!b) return a+b;
if(pri[a]<pri[b]) {
ch[b][0]=merge(a,ch[b][0]);
update(b);
return b;
} else {
ch[a][1]=merge(ch[a][1],b);
update(a);
return a;
}
}

如果\(a\)的随机权值要比\(b\)的小,为了满足上面“要求\(pri_k \geq pri_{son_k}\),即父亲的pri值要大于等于它的儿子的\(pri\)值”,应该将b节点作为子树的根,由于“\(a\)树的最大权值<=\(b\)树的最小权值”,所以将\(a\)与\(b\)的左儿子进行合并,合并之后得到的根作为\(b\)的左儿子,更新\(b\)。反之同理。

\(\mathbb{C}.\) 一些不是那么重要的函数:

​ \(\mathfrak{a}.\)基本上每个平衡树都有的找第a大:

int Kth(int a,int now) {
while(1) {
if(siz[ch[now][0]]>=a)
now=ch[now][0];
else {
if(siz[ch[now][0]]+1==a)
return val[now];
else {
a-=(siz[ch[now][0]]+1);
now=ch[now][1];
}
}
}
}

​ \(\mathfrak{b}.\) 新建节点

int New(int a) {
Tcnt++;
val[Tcnt]=a;
pri[Tcnt]=rand();
siz[Tcnt]=1;
return Tcnt;
}

​ \(\mathfrak{c}.\) 更新

void update(int nd) {
siz[nd]= siz[ch[nd][0]]+ siz[ch[nd][1]]+ 1;
}

这就是非旋\(Treap\)的基本函数,下面口胡一下如何完成普通平衡树的各项操作:

1.插入\(a\)数

将原树分裂成\(\leq a\)的树和\(>a\)的树,新建a节点,将新建节点与\(\leq a\)的树合并,再将两棵树合并

2.删除\(a\)数(若有多个相同的数,应只删除一个)

将树分裂成\(\leq a\)和\(>a\)的两棵,再将\(\leq a\)的分裂为\(\leq a-1\)与\(>a-1\)的(那么\(>a-1\)的树里就只有一堆a),合并\(>a-1\)树的左右子树(根就被吃掉了),然后按照分裂顺序再合并回去

3.查询\(a\)数的排名(排名定义为比当前数小的数的个数+1)

将树分裂成\(\leq a-1\)和\(>a-1\)的两棵,那么\(\leq a-1\)的树的\(siz+1\)就是a的排名

4.查询排名为\(a\)的数

直接Kth(a,root);

5.求\(a\)的前驱(前驱定义为小于\(a\),且最大的数)

将树分裂成\(\leq a-1\)和\(>a-1\)的两棵,查询\(\leq a-1\)树中第\(siz\)大的节点编号

6.求\(a\)的后继(后继定义为大于\(a\),且最小的数)

将树分裂成\(\leq a\)和\(>a\)的两棵,查询\(>a\)的树第一大的节点的编号

以上所有操作都一定要记得合并回去

#include<bits/stdc++.h>

using namespace std;

inline int read() {
int ans=0;
char last=' ',ch=getchar();
while(ch>'9'||ch<'0') last=ch,ch=getchar();
while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
if(last=='-') ans=-ans;
return ans;
}const int mxn=100010; int T;
int val[mxn],pri[mxn];
int ch[mxn][2],siz[mxn];
int Tcnt;
void update(int nd) {
siz[nd]= siz[ch[nd][0]]+ siz[ch[nd][1]]+ 1;
}
int New(int a) {
Tcnt++;
val[Tcnt]=a;
pri[Tcnt]=rand();
siz[Tcnt]=1;
return Tcnt;
} void split(int nd,int a,int &x,int &y) {
if(!nd) x=y=0;
else {
if(val[nd]>a) {
y=nd;
split(ch[nd][0],a,x,ch[nd][0]);
} else {
x=nd;
split(ch[nd][1],a,ch[nd][1],y);
}
update(nd);
}
} int merge(int a,int b) {
if(!a||!b) return a+b;
if(pri[a]<pri[b]) {
ch[b][0]=merge(a,ch[b][0]);
update(b);
return b;
} else {
ch[a][1]=merge(ch[a][1],b);
update(a);
return a;
}
} int Kth(int a,int now) {
while(1) {
if(siz[ch[now][0]]>=a)
now=ch[now][0];
else {
if(siz[ch[now][0]]+1==a)
return val[now];
else {
a-=(siz[ch[now][0]]+1);
now=ch[now][1];
}
}
}
} int main () {
T=read();
int opt,a,root=0,x,y,z,w;
while(T--) {
opt=read();
a=read();
if(opt==1) {
split(root,a,x,y);
x=merge(x,New(a));
root=merge(x,y);
}
if(opt==2) {
split(root,a,x,y);
split(x,a-1,z,w);
w=merge(ch[w][0],ch[w][1]);
z=merge(z,w);
root=merge(z,y);
}
if(opt==3) {
split(root,a-1,x,y);
printf("%d\n",siz[x]+1);
root=merge(x,y);
}
if(opt==4)
printf("%d\n",Kth(a,root));
if(opt==5) {
split(root,a-1,x,y);
printf("%d\n",Kth(siz[x],x));
root=merge(x,y);
}
if(opt==6) {
split(root,a,x,y);
printf("%d\n",Kth(1,y));
root=merge(x,y);
}
}
return 0;
}

非旋(fhq)Treap小记的更多相关文章

  1. 关于非旋FHQ Treap的复杂度证明

    非旋FHQ Treap复杂度证明(类比快排) a,b都是sort之后的排列(从小到大) 由一个排列a构造一颗BST,由于我们只确定了中序遍历=a,但这显然是不能确定一棵树的形态的. 由一个排列b构造一 ...

  2. 非旋treap (fhq treap) 指针版

    传送门 看了一圈,好像真的没什么用指针的呢.. 明明觉得指针很好看(什么??你说RE???听不见听不见) 其实我觉得用数组的话不RE直接WA调起来不是更困难嘛,毕竟通过gdb还可以知道哪里RE,WA就 ...

  3. [模板] 平衡树: Splay, 非旋Treap, 替罪羊树

    简介 二叉搜索树, 可以维护一个集合/序列, 同时维护节点的 \(size\), 因此可以支持 insert(v), delete(v), kth(p,k), rank(v)等操作. 另外, prev ...

  4. 非旋 treap 结构体数组版(无指针)详解,有图有真相

    非旋  $treap$ (FHQ treap)的简单入门 前置技能 建议在掌握普通 treap 以及 左偏堆(也就是可并堆)食用本blog 原理 以随机数维护平衡,使树高期望为logn级别, FHQ  ...

  5. 非旋Treap总结 : 快过Splay 好用过传统Treap

    非旋$Treap$ 其高级名字叫$Fhq\ Treap$,既然叫$Treap$,它一定满足了$Treap$的性质(虽然可能来看这篇的人一定知道$Treap$,但我还是多说几句:$Fhp\ Treap$ ...

  6. 平衡树简单教程及模板(splay, 替罪羊树, 非旋treap)

    原文链接https://www.cnblogs.com/zhouzhendong/p/Balanced-Binary-Tree.html 注意是简单教程,不是入门教程. splay 1. 旋转: 假设 ...

  7. 2827: 千山鸟飞绝 非旋treap

    国际惯例的题面:看起来很不可做的样子,我们先来整理一下题意吧.就是,维护每个点曾经拥有过的最大的两个属性值,支持把点的位置移动.我们用map对每个位置进行离散化,对每个位置建立一个平衡树.为了方便分离 ...

  8. 2081.09.22 Kuma(非旋treap)

    描述 有N张卡片,编号从0到n-1, 刚开始从0到n-1按顺序排好. 现有一个操作, 对于p. l,表示从第p张卡片之后的l张卡片拿到 最前面. 例如n=7的时候, 刚开始卡片序列为0 1 2 3 4 ...

  9. 2018.08.27 rollcall(非旋treap)

    描述 初始有一个空集,依次插入N个数Ai.有M次询问Bj,表示询问第Bj个数加入集合后的排名为j的数是多少 输入 第一行是两个整数N,M 接下来一行有N个整数,Ai 接下来一行有M个整数Bj,保证数据 ...

随机推荐

  1. AcWing:142. 前缀统计(字典树)

    给定N个字符串S1,S2…SNS1,S2…SN,接下来进行M次询问,每次询问给定一个字符串T,求S1S1-SNSN中有多少个字符串是T的前缀. 输入字符串的总长度不超过106106,仅包含小写字母. ...

  2. Linux网络编程三、 IO操作

    当从一个文件描述符进行读写操作时,accept.read.write这些函数会阻塞I/O.在这种会阻塞I/O的操作好处是不会占用cpu宝贵的时间片,但是如果需要对多个描述符操作时,阻塞会使同一时刻只能 ...

  3. scrapy_redis的使用

    配置Scrapy-Redis 配置Scrapy-Redis非常简单,只需要修改一下settings.py配置文件即可. 1. 核心配置 首先最主要的是,需要将调度器的类和去重的类替换为Scrapy-R ...

  4. python3笔记十六:python匿名函数和高阶函数

    一:学习内容 lambda函数 map函数与reduce函数 filter函数 sorted函数 二:匿名函数-lambda 1.概念:不使用def这样的语句去定义函数,使用lambda来创建匿名函数 ...

  5. Windows上配置Mask R-CNN及运行示例demo.ipynb

    最近做项目需要用到Mask R-CNN,于是花了几天时间配置.简单跑通代码,踩了很多坑,写下来分享给大家. 首先贴上官方Mask R-CNN的Github地址:https://github.com/m ...

  6. [go]new和make开辟内存

    var申明取址和new效果一样 值类型 引用类型 make和new的区别 内置函数new按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式. 而引用类型则必须使用make函数创建, ...

  7. LC 650. 2 Keys Keyboard

    Initially on a notepad only one character 'A' is present. You can perform two operations on this not ...

  8. dyld: Library not loaded: /usr/local/opt/readline/lib/libreadline.7.dylib

    dyld: Library not loaded: /usr/local/opt/readline/lib/libreadline.7.dylib Table of Contents 1. 启动时报错 ...

  9. Dialog 对话框

    在保留当前页面状态的情况下,告知用户并承载相关操作. 基本用法 Dialog 弹出一个对话框,适合需要定制性更大的场景. 需要设置visible属性,它接收Boolean,当为true时显示 Dial ...

  10. 如何在GitHub上下载一部分文件(单个文件夹)

    Preface Github下的项目可能很大,里面有很多的子文件夹,我们可能只需要使用某个子目录下的资源,可以不用下载完整的repo就能使用. 例如,我想下载这个repo中的mnist_gan文件:h ...