splay + 垃圾回收 知识点与例题的简要讲解
splay 简要讲解
前置芝士:普通二叉树
splay tree是一个越处理越灵活的数据结构,通过splay(伸展)操作,使整棵树的单次查询时间复杂度接近于O(log n),整棵树的高度也接近于log n
根据上面的这句话,很明显能看出splay与普通二叉树的区别
普通二叉树经过多次处理后,很容易退化成链,单次查询的复杂度直升O(n),对于处理大型数据来说,这是绝对不能允许的
OI和ACM界也经常会有数据能使一个普通二叉树快速退化成链
例: 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9
很明显,对于大型数据的处理来说,普通二叉树已经满足不了了
下面,我将会以例图来讲解中序遍历
这张图是我偷的,哈哈
在这组数据中,数的根节点为6,整棵树的中序遍历为1,2,3,4,5,6,7,10,15,一个树的中序遍历为:左子树 + 根 + 右子树
很明显,这是递增的,当前这个splay树维护的是一个递增的序列
________________________________
操作
_______________
splay 有一个核心操作,就叫splay(伸展)
splay操作是依靠rotate去实现的
rotate(x) 点x向上翻转,分左旋和右旋,使其中序遍历不变,详情见代码
(关键是将这玩意得要图,要不不好讲清,如果代码看不懂的话,点这里,至OI wiki)
splay(x,k) 将点x翻转到点k的下方,每次添加一个数,就将其splay到根节点的下方(每次翻转的复杂度为log n),这样的话就能使整棵树的高度接近于log n
-------------------------------
在树中每个点的前继(succ)就是其在数组中的上一个点,后继(pred)就是下一个点,
kth 查询第k大的数,在树中每个点的前继就是其在数组中的上一个点,后继就是下一个点,所以在其中序遍历有序的前提下,递归判断每个子树的大小就行了
当然,p2042这道题的kth是第k个数,并不是第k大的数,因为这道题(p2042)维护的是一个区间
---------
build(or insert) 创建子树(或插入一个数)
如果我们要创建一段数字,插入到下标x的后面,想一下,是不是可以先在这组将要插入的数中建树,然后直接将这个树插到下标为x的右子树下,是不是就可以了?
同样,insert(x)也是同理,只不过是其子树的大小只有1
---------
detele操作,垃圾回收
每一次delete(x)后,tree[x]这个点为空,是不是浪费了?这很不符合我们环保的心理 (=_=) ,所以在每次我们delete操作后,将其节点下标保存下来,到build(or insert)的时候再使用,这样是不是就做到回收再利用了?
---------------
当我们想要处理区间 [x,y] 的时候,记住无论我们怎么splay,其中序遍历一定不变,再想想,在splay中一个点的前继和后继是什么
所以我们就可以现将x splay到根节点的下方,将y splay到x的下方,其 [x,y] 是不是就是点x及其右子树 和 点y及其左子树,然后就可以区间处理了
___________________________________________
记住,splay所维护的可以不是一个数组的val值,他可以是数组的下标
___________________________________________
一切尽在注释之中,这道题所维护的是每一个点的下标,并不是其点的val
splay+垃圾回收 #include"bits/stdc++.h"
using namespace std;
const int N = 500010,INF = 1e9;
#define inl inline
#define reg register
//#define ll long long
int n,m;
struct node
{
int s[2],p,v;
int rev,same,size,sum,ms,ls,rs;
//翻转标记,相同标记,子树大小,区间和,最大子段和,最大前缀和,最大后缀和
inl void init(int _v,int _p) //预处理
{
s[0] = s[1] = 0,p = _p,v = _v;
rev = same = 0;
size = 1,sum = ms =v;
ls = rs = max(v,0);
}
}tr[N];
int root;
int nodes[N],tt;//垃圾回收数组,tt为其大小
int w[N];
inl void pushup(int x) //向上传递
{
auto &u = tr[x],&l = tr[u.s[0]],&r = tr[u.s[1]];
u.size = l.size + r.size + 1;
u.sum = l.sum + r.sum + u.v;
u.ls = max(l.ls,l.sum + u.v + r.ls);
u.rs = max(r.rs,r.sum + u.v + l.rs);
u.ms = max(max(l.ms,r.ms),l.rs + u.v + r.ls);
//处理各个数据
}
inl void pushdown(int x) //向下传递
{
auto &u = tr[x],&l = tr[u.s[0]], &r = tr[u.s[1]];
if(u.same) //若有相同标记,则翻转标记就并不需要
{
u.same = u.rev = 0;
if(u.s[0]) l.same = 1,l.v = u.v,l.sum = l.v * l.size;
if(u.s[1]) r.same = 1,r.v = u.v,r.sum = r.v * r.size;
if(u.v > 0)
{
if(u.s[0]) l.ms = l.ls = l.rs = l.sum;
if(u.s[1]) r.ms = r.ls = r.rs = r.sum;
}else
{
if(u.s[0]) l.ms = l.v,l.ls = l.rs = 0;
if(u.s[1]) r.ms = r.v,r.ls = r.rs = 0;
}
//左右子树数据处理
}else
if(u.rev)
{
u.rev = 0,l.rev ^= 1,r.rev ^= 1;
swap(l.ls,l.rs),swap(r.ls,r.rs);
swap(l.s[0],l.s[1]),swap(r.s[0],r.s[1]);
//翻转
}
}
inl void rotate(int x)
{
int y = tr[x].p,z = tr[y].p;
int k = tr[y].s[1] == x;
tr[z].s[tr[z].s[1] == y] = x,tr[x].p = z;
tr[y].s[k] = tr[x].s[k ^ 1],tr[tr[x].s[k ^ 1]].p = y;
tr[x].s[k ^ 1] = y,tr[y].p = x;
pushup(y),pushup(x);
//向上旋转
}
inl void splay(int x,int k)
{
while (tr[x].p != k)
{
int y = tr[x].p, z = tr[y].p;
if (z != k)
if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
else rotate(y);
rotate(x);
}
if (!k) root = x;
}
//伸展
inl int get_k(int k) //查询第k个数
{
int u = root;
while (u)
{
pushdown(u);
if (tr[tr[u].s[0]].size >= k) u = tr[u].s[0];
else if (tr[tr[u].s[0]].size + 1 == k) return u;
else k -= tr[tr[u].s[0]].size + 1, u = tr[u].s[1];
}
}
inl int build(int l,int r,int p) //建造子树
{
int mid = l + r >> 1;
int u = nodes[tt -- ];
tr[u].init(w[mid], p);
if (l < mid) tr[u].s[0] = build(l, mid - 1, u);
if (mid < r) tr[u].s[1] = build(mid + 1, r, u);
pushup(u);
return u;
}
inl void dfs(int u) //delete 操作
{
if(tr[u].s[0]) dfs(tr[u].s[0]);
if(tr[u].s[1]) dfs(tr[u].s[1]);
nodes[ ++ tt] = u; //垃圾回收
}
int main(void)
{
for(int i = 1;i < N;i ++) nodes[ ++ tt] = i;
//nit [1,n] -> 垃圾回收站
scanf("%d%d",&n,&m);
tr[0].ms = -INF;
w[0] = w[n+1] = -INF;
for(int i = 1;i <= n;i ++) scanf("%d",&w[i]);
root = build(0,n + 1,0);
char op[20]; while (m -- )
{ scanf("%s", op);
if (!strcmp(op, "INSERT"))
{
int posi, tot;
scanf("%d%d", &posi, &tot);
for (int i = 0; i < tot; i ++ ) scanf("%d", &w[i]);
int l = get_k(posi + 1), r = get_k(posi + 2);
splay(l, 0), splay(r, l);
int u = build(0, tot - 1, r);
tr[r].s[0] = u;
pushup(r), pushup(l);
}
else if (!strcmp(op, "DELETE"))
{
int posi, tot;
scanf("%d%d", &posi, &tot);
int l = get_k(posi), r = get_k(posi + tot + 1);
splay(l, 0), splay(r, l);
dfs(tr[r].s[0]);
tr[r].s[0] = 0;
pushup(r), pushup(l);
}
else if (!strcmp(op, "MAKE-SAME"))
{
int posi, tot, c;
scanf("%d%d%d", &posi, &tot, &c);
int l = get_k(posi), r = get_k(posi + tot + 1);
splay(l, 0), splay(r, l);
auto& son = tr[tr[r].s[0]];
son.same = 1, son.v = c, son.sum = c * son.size;
if (c > 0) son.ms = son.ls = son.rs = son.sum;
else son.ms = c, son.ls = son.rs = 0;
pushup(r), pushup(l);
}
else if (!strcmp(op, "REVERSE"))
{
int posi, tot;
scanf("%d%d", &posi, &tot);
int l = get_k(posi), r = get_k(posi + tot + 1);
splay(l, 0), splay(r, l);
auto& son = tr[tr[r].s[0]];
son.rev ^= 1;
swap(son.ls, son.rs);
swap(son.s[0], son.s[1]);
pushup(r), pushup(l);
}
else if (!strcmp(op, "GET-SUM"))
{
int posi, tot;
scanf("%d%d", &posi, &tot);
int l = get_k(posi), r = get_k(posi + tot + 1);
splay(l, 0), splay(r, l);
printf("%d\n", tr[tr[r].s[0]].sum);
}
else printf("%d\n", tr[root].ms); //MAX-SUM
} return 0;
}
/*
输入样例 1
9 8
2 -6 3 5 1 -5 -3 6 3
GET-SUM 5 4
MAX-SUM
INSERT 8 3 -5 7 2
DELETE 12 1
MAKE-SAME 3 3 2
REVERSE 3 6
GET-SUM 5 4
MAX-SUM
*/
可能对于刚刚学习splay的人来说,这道题明显有点难了
答案我放在这里了,这道题维护的是数字的val
这道题用了并查集的启发式合并,说白了就是在合并操作中将较小的集合合并到较大的集合中,以减少所浪费的时间
为什么要用启发式合并?合并操作不是O(1)吗?
不不不,这道题是并查集维护每个splay,splay的合并是O(log n),并不是O(1),需要启发式合并,来过毒瘤数据
#include"bits/stdc++.h"
using namespace std;
const int N = 500010;
#define inl inline
#define reg register
//#define ll long long
int n,m;
struct node
{
int s[2],p,v,id;
int size;
inl void init(int _v,int _id, int _p)
{
v = _v,id = _id,p = _p;
size = 1;
}
}tr[N];
int root[N],idx;
int p[N];
inl void read()
{
std::cin.tie(nullptr);
}
inl int Find(int x)
{
return p[x] != x ? p[x] = Find(p[x]) : p[x];
} inl void pushup(int x)
{
tr[x].size = tr[tr[x].s[0]].size + tr[tr[x].s[1]].size + 1; } inl void rotate(int x)
{ //y为x的父节点,z为x的爷节点
int y = tr[x].p,z = tr[y].p;
int k = tr[y].s[1] == x;//k为(x是否为y的右节点)//即k为(x节点的左右儿子)
tr[z].s[tr[z].s[1] == y] = x,tr[x].p = z;
//z节点的y儿子改为x儿子,x节点的父节点改为z tr[y].s[k] = tr[x].s[k ^ 1],tr[tr[x].s[k ^ 1]].p = y;
//处理原x的右儿子,以及原x右节点的parent
tr[x].s[k ^ 1] = y,tr[y].p = x;
//更改原x的非y儿子节点(另一个儿子),更改y的父节点为x
pushup(y),pushup(x);
//update size
//x上移1个单位
}
inl void splay(int x,int k,int b)//将集合b的splay中x节点转到k节点的下面while(tr[x].p != k)
{
int y = tr[x].p, z = tr[y].p;
if(z != k)
if((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
else rotate(y);
//双旋
rotate(x);
}
//k==0
if(!k) root[b] = x;//集合b的根节点为x
}
inl void insert(int v,int id,int b)//将编号为id,重要度为v 的节点加入集合b的splay中
{
reg int u = root[b],p = 0;
while(u) p = u,u = tr[u].s[v > tr[u].v];
u = ++idx;
if(p) tr[p].s[v > tr[p].v] = u;
tr[u].init(v,id,p);
splay(u,0,b);
}
inl void dfs(int u,int b)//将根节点u所在splay中的每一个点插入到集合b的splay中
{
if(tr[u].s[0]) dfs(tr[u].s[0],b);
if(tr[u].s[1]) dfs(tr[u].s[1],b);
insert(tr[u].v,tr[u].id,b);//查询集合b的splay中重要度第k小的节点编号
}
inl int get_k(int k,int b)//²éѯ¼¯ºÏbµÄSplayÖÐÖØÒª¶ÈµÚkСµÄ½Úµã±àºÅ
{
int u = root[b];
while(u)
{
if(tr[tr[u].s[0]].size >= k) u = tr[u].s[0];
else if(tr[tr[u].s[0]].size + 1 == k) return tr[u].id;
else k -= tr[tr[u].s[0]].size + 1,u = tr[u].s[1];
}
return -1;
}
int main(void)
{
read();
scanf("%d%d",&n,&m);
//init Find-Union and root
for(int i = 1;i <= n;i ++)
{
p[i] = root[i] = i;
int v;
scanf("%d",&v);
tr[i].init(v,i,0);/init 每个集合的root
}
idx = n;//前n个下标个n个splay的根节点用
while(m--)
{
int a,b;
scanf("%d%d",&a,&b);
a = Find(a),b = Find(b);
if(a != b)//如果两个岛不在一个集合中,需要合并
{
/*
tr[root[a]].size > tr[root[b]].size
保证集合a中元素的个数 <= 集合b中元素的个数
启发式合并
保证较小的集合加入较大的集合
*/
if(tr[root[a]].size > tr[root[b]].size)
swap(a,b);//保证集合a中的元素个数 <= 集合b中的元素个数
dfs(root[a],b);//将集合a的splay里的每一个点插入到集合b的splay里面
p[a] = b;//将集合a合并入集合b
}
}
reg int q;
scanf("%d",&q);
while(q --)
{
char op[2];
int a,b;
scanf("%s%d%d",op,&a,&b);
if(*op == 'B')//add edge
{
a = Find(a),b = Find(b);
if(a != b)//Union 如果两个岛不在一个集合里,需要合并
{
if(tr[root[a]].size > tr[root[b]].size)
swap(a,b);
dfs(root[a],b);
p[a] = b;
} }else
{
a = Find(a);//集合中不足b个数,输出"-1"
if(tr[root[a]].size < b) puts("-1");
else printf("%d\n",get_k(b,a));//否则查询第k小数
}
} return 0;
}
好了,就到这里了
数据结构不能单纯靠记忆,一定要理解,记住啊,一定要靠理解,记忆只不过是帮助你在考场上调试出来的东西,并不是其主导的因素
splay + 垃圾回收 知识点与例题的简要讲解的更多相关文章
- PHP 垃圾回收机制
PHP垃圾回收说到底是对变量及其所关联内存对象的操作, 所以在讨论PHP的垃圾回收机制之前,先简要介绍PHP中变量及其内存对象的内部表示(其C源代码中的表示). PHP官方文档中将PHP中的变量划分为 ...
- 浅谈PHP5中垃圾回收算法
原文链接:http://www.cnblogs.com/leoo2sk/archive/2011/02/27/php-gc.html PHP是一门托管型语言,在PHP编程中程序员不需要手工处理内存资源 ...
- PHP垃圾回收深入理解
转摘于http://www.cnblogs.com/lovehappying/p/3679356.html PHP是一门托管型语言,在PHP编程中程序员不需要手工处理内存资源的分配与释放(使用C编写P ...
- 牛客网Java刷题知识点之垃圾回收算法过程、哪些内存需要回收、被标记需要清除对象的自我救赎、对象将根据存活的时间被分为:年轻代、年老代(Old Generation)、永久代、垃圾回收器的分类
不多说,直接上干货! 首先,大家要搞清楚,java里的内存是怎么分配的.详细见 牛客网Java刷题知识点之内存的划分(寄存器.本地方法区.方法区.栈内存和堆内存) 哪些内存需要回收 其实,一般是对堆内 ...
- JVM G1垃圾回收算法简要介绍
JVM G1垃圾回收算法简要介绍 G1的特点 能够像CMS垃圾回收算法一样并发操作应用线程(潜台词:多核) 无需太长时间即可压缩空闲内存空间(潜台词:不会引起太多的GC停顿时间) 尽可能地让GC时长可 ...
- (编程语言+python+变量名+垃圾回收机制)*知识点
编程语言 从低级到高级的发展的过程 1.机器语言 计算机是基于电工作的.(基于高.低电平 1010010101011) 如果用机器语言表现一个字符的意思需要多段代码的行.但是计算机读取的快. 所以机器 ...
- 大数据基础篇----jvm的知识点归纳-5个区和垃圾回收机制
一直对jvm看了又忘,忘了又看的.今天做一个笔记整理存放在这里. 我们先看一下JVM的内存模型图: 上面有5个区,这5个区干嘛用的呢? 我们想象一个场景: 我们有一个class文件,里面有很多的类的定 ...
- Java GC机制简要总结(Java垃圾回收的基本工作原理)
第一次编辑 2019-05-07 01:09:39 垃圾回收的对象 程序中的不可用对象(不存活的对象,没有任何引用),或者无用的变量信息等,在程序中长期存在会逐渐占用较多的内存空间,导致没有足够的空间 ...
- python--基础知识点梳理(三)深浅拷贝、进线协程、os和sys、垃圾回收机制、读文件的三种方式
深拷贝与浅拷贝 import copy 浅拷贝:将一个对象的引用拷贝到另一个对象上,所以如果我们在拷贝中改动,会影响到原对象.copy.copy() 深拷贝:将一个对象拷贝到另一个对象中,新开辟了一个 ...
- 简要描述Python的垃圾回收机制(garbage collection)
这里能说的很多.你应该提到下面几个主要的点: Python在内存中存储了每个对象的引用计数(reference count).如果计数值变成0,那么相应的对象就会小时,分配给该对象的内存就会释放出来用 ...
随机推荐
- SpringBoot定义优雅全局统一Restful API 响应框架完结撒花篇封装starter组件
之前我们已经,出了一些列文章. 讲解如何封统一全局响应Restful API. 感兴趣的可以看我前面几篇文章 (整个starter项目发展史) SpringBoot定义优雅全局统一Restful AP ...
- 用googletest写cpp单测
框架概述 Google Test(也称为 googletest)是由 Google 开发的 C++ 单元测试框架.它的首个版本是在2004年发布的,作为 Google 内部的测试框架使用.随后,Goo ...
- PHP递归和循环的速度测试
本文于 2017-12-05 重新整理. 写了一个可以对 $_GET, $_POST 等输入进行过滤的函数,递归实现如下: function array_map_recursive($filters, ...
- .NET Core中关于阿拉伯语环境下的坑:Input string was not in a correct format.
结论 .NET Core项目(.NET Framework没出现)在阿拉伯语(即语言名称是ar-开头的语言)环境下,将负数字符串转成数字,即int.Parse("-1")或Conv ...
- NOI2023 题解
打的太 shaber 了,于是补补题. D1T1 扫描线. 首先我们可以容斥一下,答案为被一种操作覆盖到的减去被两种操作覆盖到的加上被三种操作覆盖到的. 首先考虑只被一种操作覆盖到的,这很简单,直接上 ...
- 使用 python 快速搭建http服务
python -m SimpleHTTPServer 8888 使用上面的命令可以把当前目录发布到8888端口. 直接浏览器访问 但是这条命令是当前运行的,不是后台运行的,也就是说如果Ctrl + C ...
- OpenLayers文档
https://openlayers.org/en/latest/apidoc/module-ol_interaction_DragZoom-DragZoom.html#changed
- Dirty-Pipe Linux内核提权漏洞(CVE-2022-0847)
前言: 划水一波,哈哈,以后复现漏洞不再直接傻瓜无脑的走流程了,首先码字写加构思比较麻烦且写的不多还效率不高,现在就是当做见到了一个漏洞,在此记录一下这个漏洞,包括其来源,简单的描述,适用范围,以及其 ...
- Mybatis框架的搭建和基本使用
本文总结最原始Mybatis框架的搭建和最基本使用(不涉及Spring框架体系). 1 依赖 首先,我们要引入Mybatis依赖: <dependency> <groupId> ...
- 高级SQL分析函数-窗口函数
摘要:本文由葡萄城技术团队于博客园原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言 SQL语句中,聚合函数在统计业务数据结果时起到了重要作用 ...