快速入门Treap(代码实现)
学习数据结构对我来说真的相当困难,网上讲\(Treap\)的我也看不太懂,前前后后花了大概六天才把\(Treap\)学会。为了避免再次忘记,这里我整理一下\(Treap\)的基础知识和模板。
阅读此文前,你需要知道:
\(Treap\)的基本原理
二叉查找树的性质
第一次接触\(Treap\)的同学请移步Treap的学习总结,本文着重强调代码实现和细节问题。
本文无指针,码风比较清新,请放心食用。
0.变量定义
\(:t:Treap\_node\){
\(rd\):随机产生的优先级
\(sz\):子树大小
\(ch\):\(0\)代表左子节点,\(1\)代表右子节点。
\(key\):键值(实际值大小)
\(cnt\):当前键值节点个数
}
\(tot\):结点个数
\(root\):根节点
1.操作类型
\(rotate\) - 旋转
分为左旋和右旋两种,可以传一个方向的参数,把它们合成一种。
流程:(这里以右旋为例)
节点:\(p\)点(传址引用), \(p\)的左子节点\(ls\)
把\(ls\)的右子树接在\(p\)点上
\(ls\)的右子节点变为\(p\)
更新两个节点大小
把连向\(p\)的边引向\(ls\)(\(p = ls\))
Q:为什么对\(p\)点传址引用?
A:在插入/删除点的时候同样采用了传址引用。节点\(Fa\)搜索过程中到达它的子节点\(p\)时,\(Fa\)对\(p\)点的指针\(t[Fa].ch[dir]\)会同时被传出,从而使\(rotate\)直接修改\(Fa\)和\(p\)的连接状况。
Q:为什么把\(ls\)的右子树接在\(p\)点上?
A:为了维护二叉搜索树的性质,在任意一棵子树中,你要保证左子树>根节点>右子树。\(ls\)本身所连接的右子树是合法的,根据旋转前的定义,\(ls\)的右子树键值<\(p\)点键值。为了维护旋转后\(Treap\)二叉的性质,我们选择把这一棵子树改接到\(p\)点上去。
关于传入的\(dir\)参数怎么玩,可以自己画图琢磨一下。
注意两个节点大小的更新顺序。
void rotate (int &p, int dir) {
//dir = 0 / 1 -> 右旋 / 左旋
int s = t[p].ch[dir ^ 0];
t[p].ch[dir ^ 0] = t[s].ch[dir ^ 1];
t[s].ch[dir ^ 1] = p;
push_up (p);
push_up (s);
p = s;
}
\(Insert\) - 插入
没有就加入,有就累加计数器。
流程:
判断与子节点的大小关系,大了向左,小了向右
一路上对每个经过节点增加子树大小
如果有键值相等的点,就直接累加计数器
如果没有(在向子树查找过程中走到编号为\(0\)的子树)
新建节点,初始化相关数据。
注意传址引用
程序里用了一点小\(Trick\)来记录方向,简化代码。
Q:这里传址引用还有什么作用?
A:改变连接情况的作用见上一个操作,还有一个用处是初始化根节点。每次插入都从根节点开始。如果还没有点的话,新建的节点会直接覆盖传入的根节点的地址而成为根节点。
void Insert (int &p, int key) {
if (p == 0) {
p = ++tot;
t[p].sz = 1;
t[p].cnt = 1;
t[p].key = key;
t[p].rd = rand ();
return;
}
++t[p].sz;
if (t[p].key == key) {
++t[p].cnt;
} else {
int dir = key > t[p].key;
Insert (t[p].ch[dir], key);
if (t[p].rd > t[t[p].ch[dir]].rd) {
rotate (p, dir);
}
}
}
\(Delete\) - 删除
流程:
判断与子节点的大小关系,大了向左,小了向右
一路上对每个经过节点减少子树大小
如果有键值相等的点
如果该点个数\(>1\)
直接减少点数
如果点的个数\(=1\)
把当前节点不断向下旋转
为了保持优先级,总是把优先级更高(\(rd\)更小)的转上来
如果这个点只剩\(<=1\)棵子树(\(t[p].ch[0] * t[p].ch[1] == 0\)),那么就可以把它删掉,把它还剩的那个子节点提上来。($p = t[p].ch[0] + t[p].ch[1]; $)
void Delete (int &p, int key) {
if (p == 0) {
return;
}
if (t[p].key == key) {
if (t[p].cnt > 1) {
--t[p].sz;
--t[p].cnt;
} else {
if (t[p].ch[0] * t[p].ch[1] == 0) {
//分支 < 2
p = t[p].ch[0] + t[p].ch[1];
} else {
if (t[p].ch[0] < t[p].ch[1]) {
rotate (p, 0);
} else {
rotate (p, 1);
}
Delete (p, key);
}
}
} else {
--t[p].sz;
Delete (t[p].ch[key > t[p].key], key);
}
}
\(nxt/pre\) - 查找前驱/后继
也就是第一个比查找键值大/小的数
这里以\(nxt\)为例
流程:
查找的键值小于当前所在节点的键值:
向左走,看还有没有更小一点的值
防止玩脱,先记答案
否则直接向右走(一定不可能是后继)
这里本蒻使用非递归版
int pre (int key) {
int p = root, ans = 0;
while (p != 0) {
if (t[p].key < key) {
ans = t[p].key;
p = t[p].ch[1];
} else {
p = t[p].ch[0];
}
}
return ans;
}
int nxt (int key) {
int p = root, ans = 0;
while (p != 0) {
if (t[p].key > key) {
ans = t[p].key;
p = t[p].ch[0];
} else {
p = t[p].ch[1];
}
}
return ans;
}
\(kth\) - 求\(k\)大值
流程:
\(k<=\)当前点左子树大小:去左子树找
\(k>\)当前节点左子树大小 + 当前节点的计数:去右子树找
否则\(k\)就恰好落在当前节点上,直接返回当前节点的答案
附加操作都相对简单,不作赘述。
int kth (int k) {
int p = root;
while (p != 0) {
if (k <= t[t[p].ch[0]].sz) {
p = t[p].ch[0];
} else if (k > t[t[p].ch[0]].sz + t[p].cnt) {
k -= t[t[p].ch[0]].sz + t[p].cnt;
p = t[p].ch[1];
} else {
return t[p].key;
}
}
return false;
}
\(get\_rnk\) - 求当前数的排名
流程:
键值比当前节点大:去右子树找 && 带上左子树和当前节点的贡献
键值比当前节点小:去左子树找
键值与当前节点相等:返回答案
在返回答案时记得加上当前节点左子树大小(未统计的贡献)\(+ 1\)(排名)
int get_rnk (int key) {
int p = root, ans = 0;
while (p != 0) {
if (key < t[p].key) {
p = t[p].ch[0];
} else if (key > t[p].key) {
ans += t[p].cnt;
ans += t[t[p].ch[0]].sz;
p = t[p].ch[1];
} else {
return ans + t[t[p].ch[0]].sz + 1;
}
}
return false;
}
2.完整板子(Luogu P3369 普通平衡树)
#include <bits/stdc++.h>
#define N 100010
using namespace std;
struct Treap {
int tot, root;
struct Treap_node {
int rd, sz, ch[2], cnt, key;
}t[N];
Treap () {
tot = root = 0;
memset (t, 0, sizeof (t));
}
//注意初始化
void push_up (int p) {
t[p].sz = t[p].cnt;
t[p].sz += t[t[p].ch[0]].sz;
t[p].sz += t[t[p].ch[1]].sz;
}
//更新节点大小
void rotate (int &p, int dir) {
//dir = 0 / 1 -> 右旋 / 左旋
int s = t[p].ch[dir ^ 0];
t[p].ch[dir ^ 0] = t[s].ch[dir ^ 1];
t[s].ch[dir ^ 1] = p;
push_up (p);
push_up (s);
p = s;
}
//旋转
void Insert (int &p, int key) {
if (p == 0) {
p = ++tot;
t[p].sz = 1;
t[p].cnt = 1;
t[p].key = key;
t[p].rd = rand ();
return;
}
++t[p].sz;
if (t[p].key == key) {
++t[p].cnt;
} else {
int dir = key > t[p].key;
Insert (t[p].ch[dir], key);
if (t[p].rd > t[t[p].ch[dir]].rd) {
rotate (p, dir);
}
}
}
//插入
void Delete (int &p, int key) {
if (p == 0) {
return;
}
if (t[p].key == key) {
if (t[p].cnt > 1) {
--t[p].sz;
--t[p].cnt;
} else {
if (t[p].ch[0] * t[p].ch[1] == 0) {
//分支 < 2
p = t[p].ch[0] + t[p].ch[1];
} else {
if (t[p].ch[0] < t[p].ch[1]) {
rotate (p, 0);
} else {
rotate (p, 1);
}
Delete (p, key);
}
}
} else {
--t[p].sz;
Delete (t[p].ch[key > t[p].key], key);
}
}
//删除
int pre (int key) {
int p = root, ans = 0;
while (p != 0) {
if (t[p].key < key) {
ans = t[p].key;
p = t[p].ch[1];
} else {
p = t[p].ch[0];
}
}
return ans;
}
//前驱
int nxt (int key) {
int p = root, ans = 0;
while (p != 0) {
if (t[p].key > key) {
ans = t[p].key;
p = t[p].ch[0];
} else {
p = t[p].ch[1];
}
}
return ans;
}
//后继
int kth (int k) {
int p = root;
while (p != 0) {
if (k <= t[t[p].ch[0]].sz) {
p = t[p].ch[0];
} else if (k > t[t[p].ch[0]].sz + t[p].cnt) {
k -= t[t[p].ch[0]].sz + t[p].cnt;
p = t[p].ch[1];
} else {
return t[p].key;
}
}
return false;
}
//k大(返回的是第k大的键值)
int get_rnk (int key) {
int p = root, ans = 0;
while (p != 0) {
if (key < t[p].key) {
p = t[p].ch[0];
} else if (key > t[p].key) {
ans += t[p].cnt;
ans += t[t[p].ch[0]].sz;
p = t[p].ch[1];
} else {
return ans + t[t[p].ch[0]].sz + 1;
}
}
return false;
}
//求排名
}tree;
int n, x, opt;
int main () {
// freopen ("Data.in", "r", stdin);
srand (time (NULL));
scanf ("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf ("%d %d", &opt, &x);
if (opt == 1) {//insert
tree.Insert (tree.root, x);
}
if (opt == 2) {//delete
tree.Delete (tree.root, x);
}
if (opt == 3) {//get_rank
printf ("%d\n", tree.get_rnk (x));
}
if (opt == 4) {//get_kth
printf ("%d\n", tree.kth (x));
}
if (opt == 5) {//get_pre
printf ("%d\n", tree.pre(x));
}
if (opt == 6) {//get_nxt
printf ("%d\n", tree.nxt(x));
}
}
}
快速入门Treap(代码实现)的更多相关文章
- CodeIgniter连接数据库及快速入门
原文:CodeIgniter连接数据库及快速入门 一.数据库配置 CodeIgniter 有一个配置文件让你存放数据库连接值(username:用户名,password:密码,database nam ...
- 【翻译】WPF应用程序模块化开发快速入门(使用Prism+MEF)
编译并运行快速入门 需要在VisualStudio 2010上运行此快速入门示例 代码下载:ModularityWithMef.zip 先重新生成解决方案 再按F5运行此示例 说明: 在此快速入门示例 ...
- AngularJS快速入门指南19:示例代码
本文给出的大部分示例都可以直接运行,通过点击运行按钮来查看结果,同时支持在线编辑代码. <div ng-app=""> <p>Name: <input ...
- .NET Core快速入门教程 5、使用VS Code进行C#代码调试的技巧
一.前言 为什么要调试代码?通过调试可以让我们了解代码运行过程中的代码执行信息,比如变量的值等等.通常调试代码是为了方便我们发现代码中的bug.ken.io觉得熟练代码调试技巧是成为合格程序员的基本要 ...
- [易学易懂系列|rustlang语言|零基础|快速入门|(16)|代码组织与模块化]
[易学易懂系列|rustlang语言|零基础|快速入门|(16)|代码组织与模块化] 实用知识 代码组织与模块化 我们知道,在现代软件开发的过程中,代码组织和模块化是应对复杂性的一种方式. 今天我们来 ...
- Slickflow.NET 开源工作流引擎快速入门之三: 简单或分支流程代码编写示例
前言:对于急切想了解引擎功能的开发人员,在下载版本后,就想尝试编写代码,完成一个流程的开发和测试.本文试图从请假流程,或分支模式来快速了解引擎代码的编写. 1. 创建或分支流程图形 或分支流程是常见的 ...
- Slickflow.NET 开源工作流引擎快速入门之二: 简单并行分支流程代码编写示例
前言:对于急切想了解引擎功能的开发人员,在下载版本后,就想尝试编写代码,完成一个流程的开发和测试.本文试图从一个最简单的并行分支流程来示例说明,如何快速了解引擎代码的编写. 版本:.NET Core2 ...
- Slickflow.NET 开源工作流引擎快速入门之一: 简单序列流程代码编写示例
前言:对于急切想了解引擎功能的开发人员,在下载版本后,就想尝试编写代码,完成一个流程的开发和测试.本文试图从一个最简单的流程来示例说明,如何快速了解引擎代码的编写. 版本: .NETCore 2.1 ...
- Web Api 入门实战 (快速入门+工具使用+不依赖IIS)
平台之大势何人能挡? 带着你的Net飞奔吧!:http://www.cnblogs.com/dunitian/p/4822808.html 屁话我也就不多说了,什么简介的也省了,直接简单概括+demo ...
随机推荐
- Hbase和Oracle的对比
转自:http://www.cnblogs.com/chay1227/archive/2013/03/17/2964020.html 转自:http://blog.csdn.net/allen879/ ...
- Python——glob模块
一.作用: 找到匹配上特定格式的所有文件和文件夹,跟windows的文件搜索功能差不多 二.三种匹配符 *代表0个或多个字符 ?代表一个字符 []匹配指定范围内的字符,如[0-9]匹配数 三.应用方法 ...
- Lodop提示安装或升级的注意事项
LODOP的LodopFuncs.js文件里,自动判断浏览器类型提示下载哪个,根据版本号比较判断提示升级.此文章是以前写的,图示可能过旧,新版提示不同,但是LodopFuncs.js里各个方面变动不大 ...
- Jenkins+PowerShell持续集成环境搭建(六)参数化构建
参数化构建可以应用于动态绑定源码地址等情况. 勾选“This build is parameterized”: 如果需要动态绑定源码地址,参考: 配置完成后构建项目变成:
- java json转换(一)
主要使用了2个类 JsonConvert.class 和 ConvertHelper.class 由于常规转json.只要model牵涉到复杂的关联实体对象.那么就会出现 深度循环的错误. 因此这里通 ...
- Codeforces1101F Trucks and Cities 【滑动窗口】【区间DP】
题目分析: 2500的题目为什么我想了这么久... 考虑答案是什么.对于一辆从$s$到$t$的车,它有$k$次加油的机会.可以发现实际上是将$s$到$t$的路径以城市为端点最多划分为最大长度最小的$k ...
- win10系统同时安装python2.7和python3.6
我是先在本机上安装的python3.6.5,因为要学习一个框架,但是这个框架只支持python2,所以我又安装了python2.7.15,并且配置到系统环境变量 环境变量配置了python3.6.5的 ...
- 【XSY2680】玩具谜题 NTT 牛顿迭代
题目描述 小南一共有\(n\)种不同的玩具小人,每种玩具小人的数量都可以被认为是无限大.每种玩具小人都有特定的血量,第\(i\)种玩具小人的血量就是整数\(i\).此外,每种玩具小人还有自己的攻击力, ...
- Android studio preview界面无法预览,报错render problem
1.查看报错信息,如果有报错,该叹号应为红色,点击查看报错,显示为render problem 2.打开res/styles.xml修改为如图,添加Base. 3.再打开preview界面
- MT【295】线段比的仿射变换
已知$F_1,F_2$分别为椭圆$\dfrac{x^2}{4}+\dfrac{y^2}{3}=1$的左右焦点.$A,B,C$是椭圆上$x$轴上方的三点,且$AF_1//BO//CF_2$,则$\dfr ...