bzoj3224: Tyvj 1728 普通平衡树(平衡树)

总结

a. cout<<(x=3)<<endl;这句话输出的值是3,那么对应的,在splay操作中,当父亲不为0的时候,就一直向上旋转

3224: Tyvj 1728 普通平衡树

Time Limit: 10 Sec  Memory Limit: 128 MB
Submit: 18071  Solved: 7953
[Submit][Status][Discuss]

Description

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1. 插入x数
2. 删除x数(若有多个相同的数,因只删除一个)
3. 查询x数的排名(若有多个相同的数,因输出最小的排名)
4. 查询排名为x的数
5. 求x的前驱(前驱定义为小于x,且最大的数)
6. 求x的后继(后继定义为大于x,且最小的数)

Input

第一行为n,表示操作的个数,下面n行每行有两个数opt和x,opt表示操作的序号(1<=opt<=6)

Output

对于操作3,4,5,6每行输出一个数,表示对应答案

Sample Input

10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598

Sample Output

106465
84185
492737

HINT

1.n的数据范围:n<=100000
2.每个数的数据范围:[-2e9,2e9]

Source

变量声明:f[i]表示i的父结点,ch[i][0]表示i的左儿子,ch[i][1]表示i的右儿子,key[i]表示i的关键字(即结点i代表的那个数字),cnt[i]表示i结点的关键字出现的次数(相当于权值),size[i]表示包括i的这个子树的大小;sz为整棵树的大小,root为整棵树的根。

再介绍几个基本操作:

【clear操作】:将当前点的各项值都清0(用于删除之后)

  1. inline void clear(int x){
  2. ch[x][0]=ch[x][1]=f[x]=cnt[x]=key[x]=size[x]=0;
  3. }

【get操作】:判断当前点是它父结点的左儿子还是右儿子

  1. inline int get(int x){
  2. return ch[f[x]][1]==x;
  3. }

【update操作】:更新当前点的size值(用于发生修改之后)

  1. inline void update(int x){
  2. if (x){
  3. size[x]=cnt[x];
  4. if (ch[x][0]) size[x]+=size[ch[x][0]];
  5. if (ch[x][1]) size[x]+=size[ch[x][1]];
  6. }
  7. }

下面boss来了:

【rotate操作图文详解】

这是原来的树,假设我们现在要将D结点rotate到它的父亲的位置。

step 1:

找出D的父亲结点(B)以及父亲的父亲(A)并记录。判断D是B的左结点还是右结点。

step 2:

我们知道要将Drotate到B的位置,二叉树的大小关系不变的话,B就要成为D的右结点了没错吧?

咦?可是D已经有右结点了,这样不就冲突了吗?怎么解决这个冲突呢?

我们知道,D原来是B的左结点,那么rotate过后B就一定没有左结点了对吧,那么正好,我们把G接到B的左结点去,并且这样大小关系依然是不变的,就完美的解决了这个冲突。

这样我们就完成了一次rotate,如果是右儿子的话同理。step 2的具体操作:

我们已经判断了D是B的左儿子还是右儿子,设这个关系为K;将D与K关系相反的儿子的父亲记为B与K关系相同的儿子(这里即为D的右儿子的父亲记为B的左儿子);将D与K关系相反的儿子的父亲即为B(这里即为把G的父亲记为B);将B的父亲即为D;将D与K关系相反的儿子记为B(这里即为把D的右儿子记为B);将D的父亲记为A。

最后要判断,如果A存在(即rotate到的位置不是根的话),要把A的儿子即为D。

显而易见,rotate之后所有牵涉到变化的父子关系都要改变。以上的树需要改变四对父子关系,BG DG BD AB,需要三个操作(BG BD AB)。

step 3:update一下当前点和各个父结点的各个值

【代码】

  1. inline void rotate(int x){
  2. int old=f[x],oldf=f[old],which=get(x);
  3. ch[old][which]=ch[x][which^1];f[ch[old][which]]=old;
  4. f[old]=x;ch[x][which^1]=old;
  5. f[x]=oldf;
  6. if (oldf)
  7. ch[oldf][ch[oldf][1]==old]=x;
  8. update(old);update(x);
  9. }

【splay操作】

其实splay只是rotate的发展。伸展操作只是在不停的rotate,一直到达到目标状态。如果有一个确定的目标状态,也可以传两个参。此代码直接splay到根。

splay的过程中需要分类讨论,如果是三点一线的话(x,x的父亲,x的祖父)需要先rotate x的父亲,否则需要先rotate x本身(否则会形成单旋使平衡树失衡)

  1. inline void splay(int x){
  2. for (int fa;(fa=f[x]);rotate(x))
  3. if (f[fa])
  4. rotate((get(x)==get(fa)?fa:x));
  5. root=x;
  6. }

【insert操作】

其实插入操作是比较简单的,和普通的二叉查找树基本一样。

step 1:如果root=0,即树为空的话,做一些特殊的处理,直接返回即可。

step 2:按照二叉查找树的方法一直向下找,其中:

如果遇到一个结点的关键字等于当前要插入的点的话,我们就等于把这个结点加了一个权值。因为在二叉搜索树中是不可能出现两个相同的点的。并且要将当前点和它父亲结点的各项值更新一下。做一下splay。

如果已经到了最底下了,那么就可以直接插入。整个树的大小要+1,新结点的左儿子右儿子(虽然是空)父亲还有各项值要一一对应。并且最后要做一下他父亲的update(做他自己的没有必要)。做一下splay。

  1. inline void insert(int v){
  2. if (root==0) {sz++;ch[sz][0]=ch[sz][1]=f[sz]=0;key[sz]=v;cnt[sz]=1;size[sz]=1;root=sz;return;}
  3. int now=root,fa=0;
  4. while (1){
  5. if (key[now]==v){
  6. cnt[now]++;update(now);update(fa);splay(now);break;
  7. }
  8. fa=now;
  9. now=ch[now][key[now]<v];
  10. if (now==0){
  11. sz++;
  12. ch[sz][0]=ch[sz][1]=0;key[sz]=v;size[sz]=1;
  13. cnt[sz]=1;f[sz]=fa;ch[fa][key[fa]<v]=sz;
  14. update(fa);
  15. splay(sz);
  16. break;
  17. }
  18. }
  19. }

【find操作】查询x的排名

初始化:ans=0,当前点=root

和其它二叉搜索树的操作基本一样。但是区别是:

如果x比当前结点小,即应该向左子树寻找,ans不用改变(设想一下,走到整棵树的最左端最底端排名不就是1吗)。

如果x比当前结点大,即应该向右子树寻找,ans需要加上左子树的大小以及根的大小(这里的大小指的是权值)。

不要忘记了再splay一下

  1. inline int find(int v){
  2. int ans=0,now=root;
  3. while (1){
  4. if (v<key[now])
  5. now=ch[now][0];
  6. else{
  7. ans+=(ch[now][0]?size[ch[now][0]]:0);
  8. if (v==key[now]) {splay(now);return ans+1;}
  9. ans+=cnt[now];
  10. now=ch[now][1];
  11. }
  12. }
  13. }

【findx操作】找到排名为x的点

初始化:当前点=root

和上面的思路基本相同:

如果当前点有左子树,并且x比左子树的大小小的话,即向左子树寻找;

否则,向右子树寻找:先判断是否有右子树,然后记录右子树的大小以及当前点的大小(都为权值),用于判断是否需要继续向右子树寻找。

  1. inline int findx(int x){
  2. int now=root;
  3. while (1){
  4. if (ch[now][0]&&x<=size[ch[now][0]])
  5. now=ch[now][0];
  6. else{
  7. int temp=(ch[now][0]?size[ch[now][0]]:0)+cnt[now];
  8. if (x<=temp)
  9. return key[now];
  10. x-=temp;now=ch[now][1];
  11. }
  12. }
  13. }

【求x的前驱(后继),前驱(后继)定义为小于(大于)x,且最大(最小)的数】

这类问题可以转化为将x插入,求出树上的前驱(后继),再将x删除的问题。

其中insert操作上文已经提到。

【pre/next操作】

这个操作十分的简单,只需要理解一点:在我们做insert操作之后做了一遍splay。这就意味着我们把x已经splay到根了。求x的前驱其实就是求x的左子树的最右边的一个结点,后继是求x的右子树的左边一个结点(想一想为什么?)

  1. inline int pre(){
  2. int now=ch[root][0];
  3. while (ch[now][1]) now=ch[now][1];
  4. return now;
  5. }
  6. inline int next(){
  7. int now=ch[root][1];
  8. while (ch[now][0]) now=ch[now][0];
  9. return now;
  10. }

【del操作】

删除操作是最后一个稍微有点麻烦的操作。

step 1:随便find一下x。目的是:将x旋转到根。

step 2:那么现在x就是根了。如果cnt[root]>1,即不只有一个x的话,直接-1返回。

step 3:如果root并没有孩子,就说名树上只有一个x而已,直接clear返回。

step 4:如果root只有左儿子或者右儿子,那么直接clear root,然后把唯一的儿子当作根就可以了(f赋0,root赋为唯一的儿子)

剩下的就是它有两个儿子的情况。

step 5:我们找到新根,也就是x的前驱(x左子树最大的一个点),将它旋转到根。然后将原来x的右子树接到新根的右子树上(注意这个操作需要改变父子关系)。这实际上就把x删除了。不要忘了update新根。

  1. inline void del(int x){
  2. int whatever=find(x);
  3. if (cnt[root]>1) {cnt[root]--;return;}
  4. //Only One Point
  5. if (!ch[root][0]&&!ch[root][1]) {clear(root);root=0;return;}
  6. //Only One Child
  7. if (!ch[root][0]){
  8. int oldroot=root;root=ch[root][1];f[root]=0;clear(oldroot);return;
  9. }
  10. else if (!ch[root][1]){
  11. int oldroot=root;root=ch[root][0];f[root]=0;clear(oldroot);return;
  12. }
  13. //Two Children
  14. int leftbig=pre(),oldroot=root;
  15. splay(leftbig);
  16. f[ch[oldroot][1]]=root;
  17. ch[root][1]=ch[oldroot][1];
  18. clear(oldroot);
  19. update(root);
  20. return;
  21. }

【总结】

平衡树的本质其实是二叉搜索树,所以很多操作是基于二叉搜索树的操作。

splay的本质是rotate,旋转其实只是为了保证二叉搜索树的平衡性。

所有的操作一定都满足二叉搜索树的性质,所有改变父子关系的操作一定要update。

关键是理解rotate,splay的原理以及每一个操作的原理。

所有的操作均来自bzoj3224 普通平衡树  附链接:http://www.lydsy.com/JudgeOnline/problem.php?id=3224

完整代码:http://blog.csdn.net/clove_unique/article/details/50636361

 #include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
#define MAXN 1000000
int ch[MAXN][],f[MAXN],size[MAXN],cnt[MAXN],key[MAXN];
int sz,root;
inline void clear(int x){
ch[x][]=ch[x][]=f[x]=size[x]=cnt[x]=key[x]=;
}
//输入的x是查询的节点,输出1的话表示右孩子,0的话表示左孩子
//f[x]是x节点的父亲 ,ch[f[x]][1]是x父亲节点的右孩子
//ch[f[x]][1]==x,如果父亲的右孩子等于自己,自己就是右孩子
inline bool get(int x){
return ch[f[x]][]==x;
}
inline void update(int x){
if (x){
size[x]=cnt[x];
if (ch[x][]) size[x]+=size[ch[x][]];
if (ch[x][]) size[x]+=size[ch[x][]];
}
}
//这个旋转操作是将左旋和右旋结合起来了 ,将孩子旋转到父亲的位置上面去
inline void rotate(int x){
//图中x是D,old是B,oldf是A,whichx是0,表示左孩子
int old=f[x],oldf=f[old],whichx=get(x);
//相当于图中把G赋值给B
ch[old][whichx]=ch[x][whichx^]; //^是异或,相同为0,不同为1
//设置修改后G的父亲是B
f[ch[old][whichx]]=old;
//想当予图中将B作为D的孩子
ch[x][whichx^]=old;
//设置B的父亲为D
f[old]=x;
//将D的父亲设置为A
f[x]=oldf;
//
if (oldf)
ch[oldf][ch[oldf][]==old]=x;//设置D是A的左孩子还是右孩子
//更新B节点的大小和D节点的大小(节点个数)
update(old); update(x);
} inline void splay(int x){
//fa是x的父亲 ,旋转两次
for (int fa;fa=f[x];rotate(x))
//如果fa的父亲节点存在
if (f[fa])
//如果x和fa同是左孩子或者又孩子(同为链),就旋转fa,否则旋转x
rotate((get(x)==get(fa))?fa:x);
//一直旋转到x为根节点
root=x;
}
inline void insert(int x){
//如果之前没有节点,初始化根
if (root==){sz++; ch[sz][]=ch[sz][]=f[sz]=; root=sz; size[sz]=cnt[sz]=; key[sz]=x; return;}
//先找到位置然后再插入,从根节点开始
int now=root,fa=;
while(){
if (x==key[now]){//找到
cnt[now]++; update(now); update(fa); splay(now); break;
}
//从根节点开始向下找
fa=now;
//这一步确定是找左孩子还是右孩子, key[now]<x用来确定找哪个孩子
now=ch[now][key[now]<x];
//直到没找到,也就是找到插入的位置
if (now==){
//找到插入的位置后就是一系列的赋值修改
sz++;
ch[sz][]=ch[sz][]=;
f[sz]=fa;
size[sz]=cnt[sz]=;
//确定是左孩子还是右孩子
ch[fa][key[fa]<x]=sz;
key[sz]=x;
update(fa);
//伸展操作
splay(sz);
break;
}
}
}
inline int find(int x){
//从根节点开始找
int now=root,ans=;
while(){
if (x<key[now])
now=ch[now][];
else{
//加上左孩子
ans+=(ch[now][]?size[ch[now][]]:);
//找到
if (x==key[now]){
splay(now); return ans+;
}
//加上根
ans+=cnt[now];
//继续向右孩子循环
now=ch[now][];
}
}
}
inline int findx(int x){
int now=root;
while(){
if (ch[now][]&&x<=size[ch[now][]])
now=ch[now][];
else{
int temp=(ch[now][]?size[ch[now][]]:)+cnt[now];
if (x<=temp) return key[now];
x-=temp; now=ch[now][];
}
}
}
//因为有伸展操作,所以求前驱后继很方便
inline int pre(){//左子树中最大
int now=ch[root][];
while (ch[now][]) now=ch[now][];
return now;
}
inline int next(){//右子树中最小
int now=ch[root][];
while (ch[now][]) now=ch[now][];
return now;
}
inline void del(int x){
int whatever=find(x);
if (cnt[root]>){cnt[root]--; update(root); return;}
if (!ch[root][]&&!ch[root][]) {clear(root); root=; return;}
if (!ch[root][]){
int oldroot=root; root=ch[root][]; f[root]=; clear(oldroot); return;
}
else if (!ch[root][]){
int oldroot=root; root=ch[root][]; f[root]=; clear(oldroot); return;
}
//因为我们要操作的节点已经旋转到根,所以,求左右子树的话就是直接求根的,所以都不需要传参数
int leftbig=pre(),oldroot=root;
splay(leftbig);
//因为我们找的前驱节点,右子树没有变化,直接给就好
ch[root][]=ch[oldroot][];
//设置父亲
f[ch[oldroot][]]=root;
clear(oldroot);//清除老根节点
update(root);
}
int main(){
int n,opt,x;
scanf("%d",&n);
for (int i=;i<=n;++i){
scanf("%d%d",&opt,&x);
switch(opt){
case : insert(x); break;
case : del(x); break;
case : printf("%d\n",find(x)); break;
case : printf("%d\n",findx(x)); break;
case : insert(x); printf("%d\n",key[pre()]); del(x); break;
case : insert(x); printf("%d\n",key[next()]); del(x); break;
}
}
}

bzoj3224: Tyvj 1728 普通平衡树(平衡树)的更多相关文章

  1. [BZOJ3224]Tyvj 1728 普通平衡树

    [BZOJ3224]Tyvj 1728 普通平衡树 试题描述 您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:1. 插入x数2. 删除x数(若有多个相同的数,因只删除一个) ...

  2. bzoj3224 Tyvj 1728 普通平衡树(名次树+处理相同)

    3224: Tyvj 1728 普通平衡树 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 5354  Solved: 2196[Submit][Sta ...

  3. bzoj3224: Tyvj 1728 普通平衡树(splay)

    3224: Tyvj 1728 普通平衡树 题目:传送门 题解: 啦啦啦啦又来敲个模版水经验啦~ 代码: #include<cstdio> #include<cstring> ...

  4. [BZOJ3224] [Tyvj 1728] 普通平衡树 (treap)

    Description 您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作: 1. 插入x数 2. 删除x数(若有多个相同的数,因只删除一个) 3. 查询x数的排名(若有多个相 ...

  5. 【bzoj3224】【Tyvj 1728】 普通平衡树 树状数组

    您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:1. 插入$x$数2. 删除$x$数(若有多个相同的数,因只删除一个)3. 查询$x$数的排名(若有多个相同的数,因输出最小 ...

  6. 【权值分块】bzoj3224 Tyvj 1728 普通平衡树

    权值分块和权值线段树的思想一致,离散化之后可以代替平衡树的部分功能. 部分操作的时间复杂度: 插入 删除 全局排名 全局K大 前驱 后继 全局最值 按值域删除元素 O(1) O(1) O(sqrt(n ...

  7. 绝对是全网最好的Splay 入门详解——洛谷P3369&BZOJ3224: Tyvj 1728 普通平衡树 包教包会

    平衡树是什么东西想必我就不用说太多了吧. 百度百科: 一个月之前的某天晚上,yuli巨佬为我们初步讲解了Splay,当时接触到了平衡树里的旋转等各种骚操作,感觉非常厉害.而第二天我调Splay的模板竟 ...

  8. 【权值线段树】bzoj3224 Tyvj 1728 普通平衡树

    一个板子. #include<cstdio> #include<algorithm> using namespace std; #define N 100001 struct ...

  9. BZOJ3224 Tyvj 1728 普通平衡树(Treap)

    本文版权归ljh2000和博客园共有,欢迎转载,但须保留此声明,并给出原文链接,谢谢合作. 本文作者:ljh2000 作者博客:http://www.cnblogs.com/ljh2000-jump/ ...

随机推荐

  1. 服务器端 CentOS 下配置 JDK 和 Tonmcat 踩坑合集

    一.配置 JDK 时,在 /etc/profile 文件下配置环境变量,添加   #java environment export JAVA_HOME=/usr/java/jdk- export CL ...

  2. SpringBoot 搭建

    1.使用Eclipse 建立Maven项目(webapp OR quickstart) 2.配置Maven,如下: <parent> <groupId>org.springfr ...

  3. IIS添加映射配置

    这种问题主要出现在使用应用程序级别的地址重写.如果你将一个动态的地址重写成虚拟的其它扩展名或者不带扩展名的地址,通常在IIS5.1和II6.0中,访问这样一个实际不存在的地址,首先会被Web服务器返回 ...

  4. SSL协议提供的服务

    SSL协议提供的服务主要有: 1)认证用户和服务器,确保数据发送到正确的客户机和服务器: 2)加密数据以防止数据中途被窃取: 3)维护数据的完整性,确保数据在传输过程中不被改变.

  5. iOS UIWebView 访问https绕过证书验证的方法

    @implementation NSURLRequest (NSURLRequestWithIgnoreSSL) + (BOOL)allowsAnyHTTPSCertificateForHost:(N ...

  6. TCP:三次握手,URG、ACK、PSH、RST、SYN、FIN 含义

    http://blog.csdn.net/wudiyi815/article/details/8505726 TCP:SYN ACK FIN RST PSH URG简析   三次握手Three-way ...

  7. python round()模块

    Python3的round()函数四舍五入取整时,采用最近偶数原则 >>> round(1.5)2>>> round(2.5)2>>> round ...

  8. forcedirectories和CreateDirectory

    forcedirectories和CreateDirectory都能创建文件ForceDirectories可以创建多层目录. 如果你创建一个目录为c:\mymusic\music 如果你的C盘不存在 ...

  9. [ZJOJ] 5794 2018.08.10【2018提高组】模拟A组&省选 旅行

    Description 悠悠岁月,不知不觉,距那传说中的pppfish晋级泡泡帝已是过 去数十年.数十年 中,这颗泡泡树上,也是再度变得精彩,各种泡泡 天才辈出,惊艳世人,然而,似乎 不论后人如何的出 ...

  10. 【codeforces 508A】Pasha and Pixels

    [题目链接]:http://codeforces.com/contest/508/problem/A [题意] 让你在一个n*m的方格上给方格染色; 顺序给出染色的k个格子 如果在某一时刻 有一个2* ...