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


    非旋  $treap$ (FHQ treap)的简单入门

    前置技能

    建议在掌握普通 treap 以及 左偏堆(也就是可并堆)食用本blog

    原理

    以随机数维护平衡,使树高期望为logn级别, FHQ 不依靠旋转,只有两个核心操作merge(合并)和split(拆分)

    所谓随机数维护平衡就是给每个节点一个随机值 key (下文中没有加随机的就代表是真实权值),

    然后整棵树中 key 值要满足小(大)根堆的性质(也就是heap),

    同时也要满足平衡树(tree)的性质(也就是每个节点左子树内节点真实权值小于它,右子树相反)

    然后这个玩意儿就有了一个草率的名字:treap (tree 和 heap 的结合体)

    结构体变量介绍

     1 int Rand() {  //伪随机函数,能让代码稍微变快
     2     static int seed=703;
     3     return seed=int(seed*48271LL%(~0u>>1));
     4 }
     5 struct Node {
     6     int val,key,siz,ch[2];
     7 // val 真实权值,key 随机权值,siz 子树大小 , ch 左右子节点
     8     void clear() {  //清空操作
     9         ch[0]=ch[1]=siz=val=key=0;
    10     }
    11 } t[M];
    12 int update(int now){ //更新操作
    13     t[now].siz=t[t[now].ch[0]].siz+t[t[now].ch[1]].siz+1;
    14 }
    veiw code

    核心操作

    merge操作

    模型实现

    假设有两颗子树x,y,且 x 的所有节点的值都小于 y 的所有节点的值,随机权值 key 都以小根堆的形式存储。

    此时要合并 x , y 。我们先比较它们的根的随机权值,发现1<3,因为要满足小根堆性质,于是 x 的左子树全部不变,让它的右子树继续和 y 合并。

    这时我们发现,随机权值 key 5>3,所以 y 接到 rot 的下方,成为 rot 的右儿子,y的右子树全部不变,让y的左子树继续和x合并(以满足平衡树的性质)。

    由于5>4,所以y和y的右子树作为rot的左儿子,y的左子树继续和x合并。

    5<7,所以接入x和它的左子树作为rot的左儿子。

    至此,我们发现 x 为 0 ,所以直接返回 y ,合并结束。

    代码实现

     1 int merge(int u,int v) { // 此时 u 中节点权值均小于 v 中节点权值 
     2     if(!u || !v) return u|v;  //某节点为空,直接返回另一节点 
     3     if(t[u].key<t[v].key) { //以此满足 heap 性质 
     4         t[u].ch[1]=merge(t[u].ch[1],v); // u 右子节点与 v 合并, 以满足平衡树性质 
     5         update(u); return u;
     6     } else {
     7         t[v].ch[0]=merge(u,t[v].ch[0]); // u 与 v 左子节点合并, 以满足平衡树性质 
     8         update(v); return v;
     9     }
    10 }
    view code

    split操作

    split有两种拆分方式:

      1. 按权值大小拆分

      2. 按排名大小拆分。

    模型实现

    1.按权值split

    首先得有个基准值 a ,即权值小于等于 a 的节点全部进入左树(下图中会将此类节点染红),大于a的节点全部进入右树(下图中会将此类节点染蓝)。这里以a=25为例。

    首先,发现rot的权值=15<25,由平衡树的性质可知,rot的左子树所有节点权值一定小于25,所以rot和它的的左子树全部进入左树,继续拆分rot的右子树。

    32>25,所以 rot 和它的右子树全部进入右树,继续拆分 rot 的左子树。

    29>25,同上。

    24<25,所以拆分右子树。

    27>25,所以拆分左子树。

    发现此时rot为0,所以拆分完毕,返回。

    2.按排名split

    就是把前 k 个节点拆入左树,其它节点拆入右树。这里以k=5为例。

    rot的左子树的siz+1=3<5,所以rot和它的左子树进入左树,其他节点拆分5-3=2个节点进入左树。

    4+1>2,所以rot和右子树进入右树,其它节点继续拆分出2个节点进入左树。

    3+1>2,同上。

    1+1=2,所以rot和左子树进入左树,其它节点继续拆分2-2=0个节点进入左树。

    1+0>0,所以rot和右子树进入右树,其它节点继续拆分0个节点进入左树。

    rot为0,拆分结束。

    代码实现

    1.按权值split

    1 void split_val(int now,int k,int& x,int& y) {
    2     if(!now) return (void)(x=y=0); //节点为空, return 
    3     if(t[now].val<=k) //当前节点和它的左子树都满足进入左树的条件 
    4         x=now,split_val(t[now].ch[1],k,t[now].ch[1],y);
    5     else //当前节点和它的右子树都满足进入右树的条件 
    6         y=now,split_val(t[now].ch[0],k,x,t[now].ch[0]);
    7     update(now);
    8 }
    view code

    2.按排名split

    1 void split_k(int now,int k,int& x,int& y) { //与按权值 split 类似 
    2     if(!now) return (void)(x=y=0);
    3     update(now);
    4     if(t[t[now].ch[0]].siz<k)
    5         x=now,split_k(t[now].ch[1],k-t[t[now].ch[0]].siz-1,t[now].ch[1],y);
    6     else
    7         y=now,split_k(t[now].ch[0],k,x,t[now].ch[0]);
    8     update(now);
    9 }
    view code

    其他操作

    FHQ treap 的核心操作只有 merge 和 split 两个,其他操作都是基于这两个操作实现的。

    插入

    插入权值为 x 的节点时,先新建一个节点,再以 x 为界按权值 split 整棵树为a,b,再按顺序 merge a,x,b。

    代码实现

    1 void ins(int x) {
    2     int u,a,b;
    3     t[u=++cnt].key=Rand();
    4     t[u].val=x,t[u].siz=1;
    5     split_val(root,x,a,b);
    6     root=merge(merge(a,u),b);
    7 }
    view code

    删除

    要删除x,先将整棵树以 x-1 为界按权值split 成a和b,再将 b 以 1 为界 按排名split 成c和d,则 c 就是要删除的节点。最后按顺序merge a,b,d。

    (当然,这是在要删除节点必定存在的情况下才能进行的操作,不存在的情况请自行脑补) 

    代码实现

    1 void del(int x) {
    2     int a,b,c,d;
    3     split_val(root,x-1,a,b);
    4     split_k(b,1,c,d);
    5     t[c].clear(),root=merge(a,d);
    6 }
    view code

    查询 x 的排名

    先将整棵树以x-1按权值split成a和b,则a的siz+1即为x的排名。

    代码实现

    1 int get_rank(int x) {
    2     int a,b,c;
    3     split_val(root,x-1,a,b);
    4     c=t[a].siz+1;
    5     root=merge(a,b);
    6     return c;
    7 }
    view code

    查询排名为 k 的值

    先split出整棵树前k-1小节点,则右树最小节点即为所求节点,再次split 即可。

    代码实现

    1 int get_val(int& now,int x) {
    2     int a,b,c,d,e;
    3     split_k(now,x-1,a,b);
    4     split_k(b,1,c,d);
    5     e=t[c].val;
    6     now=merge(a,merge(c,d));
    7     return e;
    8 }
    view code

    查x前驱

    将整棵树以x-1按权值split,左树中最大节点即为所求节点,转入第x小值问题。

    代码实现

    1 int pre(int x) {
    2     int a,b,c;
    3     split_val(root,x-1,a,b);
    4     c=get_val(a,t[a].siz);
    5     root=merge(a,b);
    6     return c;
    7 }
    view code

    查x后继

    将整棵树以x按权值split,右树中最小节点即为所求节点,转入第x小值问题。

    代码实现

    1 int sub(int x) {
    2     int a,b,c;
    3     split_val(root,x,a,b);
    4     c=get_val(b,1);
    5     root=merge(a,b);
    6     return c;
    7 }
    view code

    非旋 Treap的其他作用

    非旋 trap 是支持区间操作的,具体其实就是你把原来的一棵树 split 成 3 棵树($1~l-1,l~r,r+1~n$),然后 我们对中间那棵树进行操作即可,具体代码不附上了

    例题

    洛谷P3369【模板】普通平衡树

    代码

      1 //by Judge
      2 #include<iostream>
      3 #include<cstdio>
      4 using namespace std;
      5 const int M=1e5+111;
      6 #define getc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
      7 char buf[1<<21],*p1=buf,*p2=buf;
      8 inline int read(){
      9     #define num ch-'0'
     10     char ch;bool flag=0;int res;
     11     while(!isdigit(ch=getc()))
     12     (ch=='-')&&(flag=true);
     13     for(res=num;isdigit(ch=getc());res=res*10+num);
     14     (flag)&&(res=-res);
     15     #undef num
     16     return res;
     17 }
     18 char sr[1<<21],z[20];int C=-1,Z;
     19 inline void Ot(){fwrite(sr,1,C+1,stdout),C=-1;}
     20 inline void print(int x){
     21     if(C>1<<20)Ot();if(x<0)sr[++C]=45,x=-x;
     22     while(z[++Z]=x%10+48,x/=10);
     23     while(sr[++C]=z[Z],--Z);sr[++C]='
    ';
     24 }
     25 int n,cnt,root;
     26 int Rand() {
     27     static int seed=703;
     28     return seed=int(seed*48271LL%(~0u>>1));
     29 }
     30 struct Node {
     31     int val,key,siz,ch[2];
     32     void clear() {
     33         ch[0]=ch[1]=siz=val=key=0;
     34     }
     35 } t[M];
     36 int update(int now){
     37     t[now].siz=t[t[now].ch[0]].siz+t[t[now].ch[1]].siz+1;
     38 }
     39 int merge(int u,int v) { 
     40     if(!u || !v) return u|v; 
     41     if(t[u].key<t[v].key) {
     42         t[u].ch[1]=merge(t[u].ch[1],v); 
     43         update(u); return u;
     44     } else {
     45         t[v].ch[0]=merge(u,t[v].ch[0]); 
     46         update(v); return v;
     47     }
     48 }
     49 void split_val(int now,int k,int& x,int& y) {
     50     if(!now) return (void)(x=y=0); 
     51     if(t[now].val<=k) 
     52         x=now,split_val(t[now].ch[1],k,t[now].ch[1],y);
     53     else 
     54         y=now,split_val(t[now].ch[0],k,x,t[now].ch[0]);
     55     update(now);
     56 }
     57 void split_k(int now,int k,int& x,int& y) {
     58     if(!now) return (void)(x=y=0);
     59     update(now);
     60     if(t[t[now].ch[0]].siz<k)
     61         x=now,split_k(t[now].ch[1],k-t[t[now].ch[0]].siz-1,t[now].ch[1],y);
     62     else
     63         y=now,split_k(t[now].ch[0],k,x,t[now].ch[0]);
     64     update(now);
     65 }
     66 void ins(int x) {
     67     int u,a,b;
     68     t[u=++cnt].key=Rand();
     69     t[u].val=x,t[u].siz=1;
     70     split_val(root,x,a,b);
     71     root=merge(merge(a,u),b);
     72 }
     73 void del(int x) {
     74     int a,b,c,d;
     75     split_val(root,x-1,a,b);
     76     split_k(b,1,c,d);
     77     t[c].clear(),root=merge(a,d);
     78 }
     79 int get_rank(int x) {
     80     int a,b,c;
     81     split_val(root,x-1,a,b);
     82     c=t[a].siz+1;
     83     root=merge(a,b);
     84     return c;
     85 }
     86 int get_val(int& now,int x) {
     87     int a,b,c,d,e;
     88     split_k(now,x-1,a,b);
     89     split_k(b,1,c,d);
     90     e=t[c].val;
     91     now=merge(a,merge(c,d));
     92     return e;
     93 }
     94 int pre(int x) {
     95     int a,b,c;
     96     split_val(root,x-1,a,b);
     97     c=get_val(a,t[a].siz);
     98     root=merge(a,b);
     99     return c;
    100 }
    101 int sub(int x) {
    102     int a,b,c;
    103     split_val(root,x,a,b);
    104     c=get_val(b,1);
    105     root=merge(a,b);
    106     return c;
    107 }
    108 int main() {
    109     n=read(); int opt,x;
    110     while(n--){
    111         opt=read(),x=read();
    112         switch(opt){
    113             case 1: ins(x); break;
    114             case 2: del(x); break;
    115             case 3: print(get_rank(x)); break;
    116             case 4: print(get_val(root,x)); break;
    117             case 5: print(pre(x)); break;
    118             case 6: print(sub(x)); break;
    119         }
    120     } Ot(); return 0;
    121 }
    view code

     然后这是压过行了的:

     1 //by Judge
     2 #include<iostream>
     3 #include<cstdio>
     4 using namespace std;
     5 const int M=1e5+111;
     6 #define getc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
     7 char buf[1<<21],*p1=buf,*p2=buf;
     8 inline int read(){
     9     #define num ch-'0'
    10     char ch;bool flag=0;int res;
    11     while(!isdigit(ch=getc()))
    12     (ch=='-')&&(flag=true);
    13     for(res=num;isdigit(ch=getc());res=res*10+num);
    14     (flag)&&(res=-res);
    15     #undef num
    16     return res;
    17 }
    18 char sr[1<<21],z[20];int C=-1,Z;
    19 inline void Ot(){fwrite(sr,1,C+1,stdout),C=-1;}
    20 inline void print(int x){
    21     if(C>1<<20)Ot();if(x<0)sr[++C]=45,x=-x;
    22     while(z[++Z]=x%10+48,x/=10);
    23     while(sr[++C]=z[Z],--Z);sr[++C]='
    ';
    24 }
    25 int n,cnt,root;
    26 struct Node {
    27     int val,key,siz,ch[2];
    28     void clear() { ch[0]=ch[1]=siz=val=key=0; }
    29 } t[M];
    30 int Rand() { static int seed=703; return seed=int(seed*48271LL%(~0u>>1)); }
    31 int update(int now){ t[now].siz=t[t[now].ch[0]].siz+t[t[now].ch[1]].siz+1; }
    32 int merge(int u,int v) {
    33     if(!u || !v) return u|v;
    34     if(t[u].key<t[v].key) { t[u].ch[1]=merge(t[u].ch[1],v),update(u); return u; }
    35     else { t[v].ch[0]=merge(u,t[v].ch[0]),update(v); return v; }
    36 }
    37 void split_val(int now,int k,int& x,int& y) {
    38     if(!now) return (void)(x=y=0);
    39     if(t[now].val<=k) split_val(t[x=now].ch[1],k,t[now].ch[1],y);
    40     else split_val(t[y=now].ch[0],k,x,t[now].ch[0]);
    41     update(now);
    42 }
    43 void split_k(int now,int k,int& x,int& y) { 
    44     if(!now) return (void)(x=y=0);
    45     if(t[t[now].ch[0]].siz>=k) split_k(t[y=now].ch[0],k,x,t[now].ch[0]);
    46     else split_k(t[x=now].ch[1],k-t[t[now].ch[0]].siz-1,t[now].ch[1],y);
    47     update(now);
    48 }
    49 void ins(int x) { int u,a,b; t[u=++cnt].key=Rand(),t[u].val=x,t[u].siz=1,split_val(root,x,a,b),root=merge(merge(a,u),b); }
    50 void del(int x) { int a,b,c,d; split_val(root,x-1,a,b),split_k(b,1,c,d),t[c].clear(),root=merge(a,d); }
    51 int get_rank(int x) { int a,b,c; split_val(root,x-1,a,b),c=t[a].siz+1,root=merge(a,b); return c; }
    52 int get_val(int& now,int x) { int a,b,c,d,e; split_k(now,x-1,a,b),split_k(b,1,c,d),e=t[c].val,now=merge(a,merge(c,d)); return e; }
    53 int pre(int x) { int a,b,c; split_val(root,x-1,a,b),c=get_val(a,t[a].siz),root=merge(a,b); return c; }
    54 int sub(int x) { int a,b,c; split_val(root,x,a,b),c=get_val(b,1),root=merge(a,b); return c; }
    55 int main() {
    56     n=read(); int opt,x;
    57     while(n--){
    58         opt=read(),x=read();
    59         switch(opt){
    60             case 1: ins(x); break;
    61             case 2: del(x); break;
    62             case 3: print(get_rank(x)); break;
    63             case 4: print(get_val(root,x)); break;
    64             case 5: print(pre(x)); break;
    65             case 6: print(sub(x)); break;
    66         }
    67     } Ot(); return 0;
    68 }
    view code

    推荐题目

    题目

    洛谷 ——  列队

    分析

    首先我们分析一下这个列队的性质;

    首先每次操作时,先让一个人 (a,b) 出队  。

    然后右边的人跟上来,空缺的位置变成了 (a,m)   (也就是在一棵平衡树中删除了一个节点)

    最后其实也就是最后一列的跟到上面,空缺的位置变成 (n,m) ,然后出队的人到这个位置上

    于是非常显然的,题目就是要你维护 n 棵平衡树,

    但是看看数据范围:3e5  ...  于是发现这题不可做

    那么我萌就要用到一个巧妙的思路—— 缩点与拆点  了。

    什么意思? 你再看眼数据范围:3e5  ...  没毛病?

    对啊,询问也是  3e5  啊...  所以说我们是不是维护了许多用都用不到的点呢?

    于是我们把一大段区间的没用的点缩成一个点,让这个点记录区间左右信息就好了咯。

    emmm...你说的没错,我们怎么知道哪些点该缩哪些点不该缩呢?

    简单,我们点全都缩起来,要用的时候再拆出来不就好了?

    然后还要注意的就是最后一列的特殊性,要单独维护(也就 3e5 个点嘛,开得下),

    这点从图中应该也看得出来(何况这些点并不属于一个区间!)

    于是愉快地上代码...

    代码

     1 //by Judge
     2 #include<iostream>
     3 #include<cstdio>
     4 #define ll long long
     5 #define int long long
     6 using namespace std;
     7 const int M=3e5+111;
     8 #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)    // 手动输入需要去掉这句话 
     9 char buf[1<<21],*p1=buf,*p2=buf;
    10 inline int read(){ 
    11     int x=0,f=1; char c=getchar();
    12     for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
    13     for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f;
    14 }
    15 char sr[1<<21],z[20];int C=-1,Z;
    16 inline void Ot(){fwrite(sr,1,C+1,stdout),C=-1;}
    17 inline void print(int x){
    18     if(C>1<<20)Ot();if(x<0)sr[++C]=45,x=-x;
    19     while(z[++Z]=x%10+48,x/=10);
    20     while(sr[++C]=z[Z],--Z);sr[++C]='
    ';
    21 }
    22 ll n,m,q,cnt,root,rt[M];
    23 struct Node { int key,ch[2]; ll l,r,siz; } t[M<<4];
    24 inline int Rand() { static int seed=703; return seed=(ll)(seed*48271LL%(~0u>>1)); }
    25 inline int update(int now){ t[now].siz=t[t[now].ch[0]].siz+t[t[now].ch[1]].siz+t[now].r-t[now].l+1; }
    26 inline int newnode(ll x,ll y) { int u=++cnt; t[u].l=x,t[u].r=y,t[u].siz=y-x+1,t[u].key=Rand(); return u; }
    27 int merge(int u,int v) {
    28     if(!u || !v) return u|v;
    29     if(t[u].key<t[v].key) { t[u].ch[1]=merge(t[u].ch[1],v),update(u); return u; }
    30     else { t[v].ch[0]=merge(u,t[v].ch[0]),update(v); return v; }
    31 }
    32 void split_new(ll now,ll k){  //把一个节点拆成两个节点 
    33     if(k>=t[now].r-t[now].l+1) return ; int nw=newnode(t[now].l+k,t[now].r);
    34     t[now].r=t[now].l+k-1,t[now].ch[1]=merge(nw,t[now].ch[1]),update(now);
    35 }
    36 void split(int now,int k,int& x,int& y) {  //常规操作 
    37     if(!now) return (void)(x=y=0);
    38     if(t[t[now].ch[0]].siz>=k) split(t[y=now].ch[0],k,x,t[now].ch[0]);
    39     else{
    40         split_new(now,k-t[t[now].ch[0]].siz),k-=t[t[now].ch[0]].siz,
    41         split(t[x=now].ch[1],k-(t[now].r-t[now].l+1),t[now].ch[1],y);
    42     } update(now);
    43 }
    44 signed main(){
    45     n=read(),m=read(),q=read();
    46     for(ll i=1;i<=n;++i)
    47         rt[i]=newnode((i-1)*m+1,i*m-1);
    48     for(ll i=1;i<=n;++i)
    49         rt[n+1]=merge(rt[n+1],newnode(i*m,i*m));
    50     while(q--){
    51         ll a=read(),b=read(),x,y,z;
    52         if(b^m){
    53             ll xx,yy,zz; split(rt[a],b,x,y),
    54             split(x,b-1,x,z),print(t[z].l),
    55             split(rt[n+1],a,xx,yy),split(xx,a-1,xx,zz);
    56             rt[a]=merge(merge(x,y),zz),rt[n+1]=merge(merge(xx,yy),z);  //拆完合并 
    57         }
    58         else{
    59             split(rt[n+1],a,x,y),split(x,a-1,x,z);
    60             print(t[z].l),rt[n+1]=merge(merge(x,y),z);
    61         }
    62     } Ot(); return 0;
    63 }
    View Code

    最后感谢 axjcy 大佬的 blog 

  • 相关阅读:
    转 mysql 数据结构详解
    转单元测试之道C#版
    转 告诉你如何用C#写出iOS与Android应用
    转 MySQL索引背后的数据结构及算法原理
    转单元测试基础知识
    转C#冒泡排序
    如何让web页面鼠标右键单击之后不出现菜单选项
    开博文
    jquery ui 1.7 ui.tabs 动态添加与关闭(按钮关闭+双击关闭)
    jquery ui tabs详解(中文)
  • 原文地址:https://www.cnblogs.com/Judge/p/9506980.html
Copyright © 2020-2023  润新知