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


    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.   
    7. inline int next(){  
    8.      int now=ch[root][1];  
    9.      while (ch[now][0]) now=ch[now][0];  
    10.      return now;  
    11. }  


    【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

      1 #include<iostream>
      2 #include<cstring>
      3 #include<cstdio>
      4 using namespace std;
      5 #define MAXN 1000000
      6 int ch[MAXN][2],f[MAXN],size[MAXN],cnt[MAXN],key[MAXN];
      7 int sz,root;
      8 inline void clear(int x){
      9     ch[x][0]=ch[x][1]=f[x]=size[x]=cnt[x]=key[x]=0;
     10 }
     11 //输入的x是查询的节点,输出1的话表示右孩子,0的话表示左孩子 
     12 //f[x]是x节点的父亲 ,ch[f[x]][1]是x父亲节点的右孩子 
     13 //ch[f[x]][1]==x,如果父亲的右孩子等于自己,自己就是右孩子 
     14 inline bool get(int x){
     15     return ch[f[x]][1]==x;
     16 }
     17 inline void update(int x){
     18     if (x){
     19         size[x]=cnt[x];
     20         if (ch[x][0]) size[x]+=size[ch[x][0]];
     21         if (ch[x][1]) size[x]+=size[ch[x][1]];
     22     }
     23 }
     24 //这个旋转操作是将左旋和右旋结合起来了 ,将孩子旋转到父亲的位置上面去 
     25 inline void rotate(int x){
     26     //图中x是D,old是B,oldf是A,whichx是0,表示左孩子 
     27     int old=f[x],oldf=f[old],whichx=get(x);
     28     //相当于图中把G赋值给B 
     29     ch[old][whichx]=ch[x][whichx^1]; //^是异或,相同为0,不同为1 
     30     //设置修改后G的父亲是B 
     31     f[ch[old][whichx]]=old;
     32     //想当予图中将B作为D的孩子 
     33     ch[x][whichx^1]=old; 
     34     //设置B的父亲为D 
     35     f[old]=x;
     36     //将D的父亲设置为A 
     37     f[x]=oldf;
     38     //
     39     if (oldf)
     40         ch[oldf][ch[oldf][1]==old]=x;//设置D是A的左孩子还是右孩子 
     41     //更新B节点的大小和D节点的大小(节点个数)    
     42     update(old); update(x);
     43 }
     44 
     45 inline void splay(int x){
     46     //fa是x的父亲 ,旋转两次 
     47     for (int fa;fa=f[x];rotate(x))
     48     //如果fa的父亲节点存在 
     49     if (f[fa])
     50     //如果x和fa同是左孩子或者又孩子(同为链),就旋转fa,否则旋转x 
     51         rotate((get(x)==get(fa))?fa:x);
     52     //一直旋转到x为根节点 
     53     root=x;
     54 }
     55 inline void insert(int x){
     56     //如果之前没有节点,初始化根 
     57     if (root==0){sz++; ch[sz][0]=ch[sz][1]=f[sz]=0; root=sz; size[sz]=cnt[sz]=1; key[sz]=x; return;}
     58     //先找到位置然后再插入,从根节点开始 
     59     int now=root,fa=0;
     60     while(1){
     61         if (x==key[now]){//找到 
     62             cnt[now]++; update(now); update(fa); splay(now); break;
     63         }
     64         //从根节点开始向下找 
     65         fa=now;
     66         //这一步确定是找左孩子还是右孩子, key[now]<x用来确定找哪个孩子 
     67         now=ch[now][key[now]<x];
     68         //直到没找到,也就是找到插入的位置 
     69         if (now==0){
     70             //找到插入的位置后就是一系列的赋值修改 
     71             sz++;
     72             ch[sz][0]=ch[sz][1]=0;
     73             f[sz]=fa;
     74             size[sz]=cnt[sz]=1;
     75             //确定是左孩子还是右孩子 
     76             ch[fa][key[fa]<x]=sz;
     77             key[sz]=x;
     78             update(fa);
     79             //伸展操作 
     80             splay(sz);
     81             break;
     82         }
     83     }
     84 }
     85 inline int find(int x){
     86     //从根节点开始找 
     87     int now=root,ans=0;
     88     while(1){
     89         if (x<key[now])
     90           now=ch[now][0];
     91         else{
     92             //加上左孩子 
     93             ans+=(ch[now][0]?size[ch[now][0]]:0);
     94             //找到 
     95             if (x==key[now]){
     96                 splay(now); return ans+1;
     97             }
     98             //加上根 
     99             ans+=cnt[now];
    100             //继续向右孩子循环 
    101             now=ch[now][1];
    102         }
    103     }
    104 }
    105 inline int findx(int x){
    106     int now=root;
    107     while(1){
    108         if (ch[now][0]&&x<=size[ch[now][0]])
    109           now=ch[now][0];
    110         else{
    111             int temp=(ch[now][0]?size[ch[now][0]]:0)+cnt[now];
    112             if (x<=temp) return key[now];
    113             x-=temp; now=ch[now][1];
    114         }
    115     }
    116 }
    117 //因为有伸展操作,所以求前驱后继很方便 
    118 inline int pre(){//左子树中最大 
    119     int now=ch[root][0];
    120     while (ch[now][1]) now=ch[now][1];
    121     return now;
    122 }
    123 inline int next(){//右子树中最小 
    124     int now=ch[root][1];
    125     while (ch[now][0]) now=ch[now][0];
    126     return now;
    127 }
    128 inline void del(int x){
    129     int whatever=find(x);
    130     if (cnt[root]>1){cnt[root]--; update(root); return;}
    131     if (!ch[root][0]&&!ch[root][1]) {clear(root); root=0; return;}
    132     if (!ch[root][0]){
    133         int oldroot=root; root=ch[root][1]; f[root]=0; clear(oldroot); return;
    134     }
    135     else if (!ch[root][1]){
    136         int oldroot=root; root=ch[root][0]; f[root]=0; clear(oldroot); return;
    137     }
    138     //因为我们要操作的节点已经旋转到根,所以,求左右子树的话就是直接求根的,所以都不需要传参数 
    139     int leftbig=pre(),oldroot=root;
    140     splay(leftbig);
    141     //因为我们找的前驱节点,右子树没有变化,直接给就好 
    142     ch[root][1]=ch[oldroot][1];
    143     //设置父亲 
    144     f[ch[oldroot][1]]=root;
    145     clear(oldroot);//清除老根节点 
    146     update(root); 
    147 }
    148 int main(){
    149     int n,opt,x;
    150     scanf("%d",&n);
    151     for (int i=1;i<=n;++i){
    152         scanf("%d%d",&opt,&x);
    153         switch(opt){
    154             case 1: insert(x); break;
    155             case 2: del(x); break;
    156             case 3: printf("%d
    ",find(x)); break;
    157             case 4: printf("%d
    ",findx(x)); break;
    158             case 5: insert(x); printf("%d
    ",key[pre()]); del(x); break;
    159             case 6: insert(x); printf("%d
    ",key[next()]); del(x); break;
    160         }
    161     }
    162 }
  • 相关阅读:
    微软职位内部推荐-Software Engineer II
    微软职位内部推荐-Senior Software Engineer
    Linux日期时间显示输出
    Redis性能优化
    Can't use Subversion command line client: svn
    redis常用性能分析命令
    Linux下配置tomcat + apr + native应对高并发
    Tomcat7并发和线程数
    mongodb常用命令小结
    Spring MVC @PathVariable被截断
  • 原文地址:https://www.cnblogs.com/Renyi-Fan/p/8231863.html
Copyright © 2020-2023  润新知