常用数据结构小结

应当说这段时间学习了很多的数据结构,也到了一个总结的时候。fotile96的这篇Blog非常值得推荐,我达不到这个高度,只能给自己和队友做些简单的归纳。

  树状数组

  非常简单的数据结构,只需要一个数组,一切操作基于如下的函数。需要注意的是,树状数组的下标必须从1开始(从0开始会死循环)。它可以做到在O(logn)时间内完成下述的操作。

  1. int bit[MAXN],n;  
  2. inline int lowbit(int x) {  
  3.     return x&-x;  
  4. }  

  最常见的是增减单点的值,询问前缀和。

  1. void add(int x,int val) {  
  2.     for(int i=x; i<=n; i+=lowbit(i))  
  3.         bit[i]+=val;  
  4. }  
  5. int sum(int x) {  
  6.     int ret=0;  
  7.     for(int i=x; i>0; i-=lowbit(i))  
  8.         ret+=bit[i];  
  9.     return ret;  
  10. }  

  我们注意到,只要维护原序列的差分序列,就可以用上述方式实现对前缀的每一位增减同一个值,询问单点值。不过,其实我们不需要手动将原数组转化为差分序列,只需对函数做一个简单的转化。

  1. void add(int x,int val) {  
  2.     for(int i=x; i>0; i-=lowbit(i))  
  3.         bit[i]+=val;  
  4. }  
  5. int get(int x) {  
  6.     int ret=0;  
  7.     for(int i=x; i<=n; i+=lowbit(i))  
  8.         ret+=bit[i];  
  9.     return ret;  
  10. }  

  树状数组其实是可以实现区间增减,区间求和的。但这个一般还是用线段树来实现。

  1. //修改区间:add(r,val);  
  2. //          if(l>1) add(l-1,-val);  
  3. //查询区间:sum(r)-sum(l-1);  
  4. int bit1[MAXN],bit2[MAXN],n;  
  5. void add(int x,int val) {  
  6.     for(int i=x; i>0; i-=lowbit(i))  
  7.         bit1[i]+=val;  
  8.     for(int i=x; i<=n; i+=lowbit(i))  
  9.         bit2[i]+=x*val;  
  10. }  
  11. int sum(int x) {  
  12.     if(!x)  
  13.         return 0;  
  14.     int ret1=0,ret2=0;  
  15.     for(int i=x; i<=n; i+=lowbit(i))  
  16.         ret1+=bit1[i];  
  17.     for(int i=x-1; i>0; i-=lowbit(i))  
  18.         ret2+=bit2[i];  
  19.     return ret1*x+ret2;  
  20. }  

  树状数组也可以维护最值,但复杂度上升一个log,所以不如用线段树来维护。

  1. void modify(int x,int val) {  
  2.     num[x]=val;  
  3.     for(int i=x; i<=n; i+=lowbit(i)) {  
  4.         bit[i]=max(bit[i],val);  
  5.         for(int j=1; j<lowbit(i); j<<=1)  
  6.             bit[i]=max(bit[i],bit[i-j]);  
  7.     }  
  8. }  
  9. int query(int L,int R) {  
  10.     int ret=num[R];  
  11.     while(true) {  
  12.         ret=max(ret,num[R]);  
  13.         if(L==R)  
  14.             break;  
  15.         for(R-=1; R-L>=lowbit(R); R-=lowbit(R))  
  16.             ret=max(ret,bit[R]);  
  17.     }  
  18.     return ret;  
  19. }  

  线段树

  现在最常见的线段树实现大概有两个版本,一个是NotOnlySuccess的风格,也是我最常使用的风格;它的特点是利用二叉树的数学关系来维护根节点信息。这种写法有一点不足是必须开4倍内存。支持在O(1)时间内将子树信息合并的序列信息理论上都可以用线段树来维护(如最大子段和等),区间维护依靠lazy标记完成以维持O(logn)的单次操作复杂度。

  1. #define lson l,m,rt<<1  
  2. #define rson m+1,r,rt<<1|1  
  3. int sum[MAXN<<2],add[MAXN<<2];  
  4. void push_up(int rt) {  
  5.     sum[rt]=sum[rt<<1]+sum[rt<<1|1];  
  6. }  
  7. void push_down(int rt,int len) {  
  8.     if(add[rt]) {  
  9.         add[rt<<1]+=add[rt];  
  10.         add[rt<<1|1]+=add[rt];  
  11.         sum[rt<<1]+=add[rt]*(len-(len>>1));  
  12.         sum[rt<<1|1]+=add[rt]*(len>>1);  
  13.         add[rt]=0;  
  14.     }  
  15. }  
  16. void build(int l,int r,int rt) {  
  17.     add[rt]=0;  
  18.     if(l==r) {  
  19.         scanf("%d",&sum[rt]);  
  20.         return;  
  21.     }  
  22.     int m=l+r>>1;  
  23.     build(lson);  
  24.     build(rson);  
  25.     push_up(rt);  
  26. }  
  27. void update(int p,int val,int l,int r,int rt) {  
  28.     if(l==r) {  
  29.         sum[rt]+=val;  
  30.         return;  
  31.     }  
  32.     push_down(rt,r-l+1);  
  33.     int m=l+r>>1;  
  34.     if(p<=m)  
  35.         update(p,val,lson);  
  36.     else  
  37.         update(p,val,rson);  
  38.     push_up(rt);  
  39. }  
  40. void update(int L,int R,int val,int l,int r,int rt) {  
  41.     if(L<=l&&r<=R) {  
  42.         add[rt]+=val;  
  43.         sum[rt]+=val*(r-l+1);  
  44.         return;  
  45.     }  
  46.     push_down(rt,r-l+1);  
  47.     int m=l+r>>1;  
  48.     if(L<=m)  
  49.         update(L,R,val,lson);  
  50.     if(m<R)  
  51.         update(L,R,val,rson);  
  52.     push_up(rt);  
  53. }  
  54. int query(int L,int R,int l,int r,int rt) {  
  55.     if(L<=l&&r<=R)  
  56.         return sum[rt];  
  57.     push_down(rt,r-l+1);  
  58.     int m=l+r>>1,ret=0;  
  59.     if(L<=m)  
  60.         ret+=query(L,R,lson);  
  61.     if(m<R)  
  62.         ret+=query(L,R,rson);  
  63.     return ret;  
  64. }  

  另外一种有些类似,与上面不同的是,它使用下面这个神奇的ID函数组织每个节点在数组中的位置。具体实现不再赘述。

  1. inline int ID(int l,int r) {  
  2.     return l+r|l!=r;  
  3. }  

  可持久化线段树

  如果我们想在用数据结构维护信息时,留下所有操作的历史记录,以便回退和查询,我们就需要对数据结构进行可持久化。可持久化的理念非常简单,就是把每次修改节点的操作变成新建新节点的操作,留下了原节点就留下了历史记录。

  线段树是天生利于可持久化的数据结构,原因在于形态的一致性。具体地说,对于以固定方式建立的固定大小的线段树,其形态是完全一致的,且不会因修改而产生变动。为了节约空间,采用了自顶向下的函数式风格,保证了每次更新节点个数在O(logn)的级别,这个做法由黄嘉泰(fotile96)首创,故又称主席树。更具体的介绍可以看我以前的Blog,里面有很多例题,这里不再赘述。

  实时开节点的线段树

  我们注意到可持久化线段树常常被用来维护集合信息而非序列信息,换言之,经常以权值线段树的形式存在,其大小不由数据量决定,而是由数据的范围决定。这样我们便遇到一个困境,对于可持久化线段树这一在线维护信息的利器,却常常需要先离散化才可以使用,从实际上变成了离线做法,面对强制在线的题目非常尴尬。对此,陈立杰(WJMZBMR)在13年国家集训队论文中提到实时开节点的权值线段树;陈立杰的解释有一点抽象,我们可以这样理解,可持久化线段树是先建立初始版本的完整的树,在创建新版本时对修改的节点实时开新节点;而我们现在变成一棵初始状态为一棵空树,从一开始就实时开新节点。

  即使这样说,还是显得不够具体,难以据此实现出实时开节点的线段树,所以这里以BZOJ1901为例,给出一个没有离散化,而是采用实时开节点的可持久化线段树的实现,与这里的代码做一个对比;可以发现实现起来并不麻烦。唯一美中不足是线段树部分单次操作复杂度由O(logn)升为O(logV),其中n是数据量,V是数据范围上界。

  1. #include<cstdio>  
  2. #include<cstring>  
  3. using namespace std;  
  4. #define lson l,m,ls[rt]  
  5. #define rson m+1,r,rs[rt]  
  6. const int MAXN=60005;  
  7. const int MAXM=2500005;  
  8. const int INF=0x3f3f3f3f;  
  9. int ls[MAXM],rs[MAXM],cnt[MAXM],root[MAXN],tot;  
  10. int new_node() {  
  11.     ++tot;  
  12.     ls[tot]=rs[tot]=cnt[tot]=0;  
  13.     return tot;  
  14. }  
  15. void update(int p,int val,int l,int r,int &rt) {  
  16.     if(!rt)  
  17.         rt=new_node();  
  18.     if(l==r) {  
  19.         cnt[rt]+=val;  
  20.         return;  
  21.     }  
  22.     int m=l+r>>1;  
  23.     if(p<=m)  
  24.         update(p,val,lson);  
  25.     else  
  26.         update(p,val,rson);  
  27.     cnt[rt]=cnt[ls[rt]]+cnt[rs[rt]];  
  28. }  
  29. int use[MAXN],n;  
  30. inline int lowbit(int x) {  
  31.     return x&-x;  
  32. }  
  33. void modify(int x,int p,int val) {  
  34.     for(int i=x; i<=n; i+=lowbit(i))  
  35.         update(p,val,0,INF,root[i]);  
  36. }  
  37. int sum(int x) {  
  38.     int ret=0;  
  39.     for(int i=x; i>0; i-=lowbit(i))  
  40.         ret+=cnt[ls[use[i]]];  
  41.     return ret;  
  42. }  
  43. int query(int ss,int tt,int l,int r,int k) {  
  44.     for(int i=ss; i>0; i-=lowbit(i))  
  45.         use[i]=root[i];  
  46.     for(int i=tt; i>0; i-=lowbit(i))  
  47.         use[i]=root[i];  
  48.     while(l<r) {  
  49.         int m=l+r>>1,tmp=sum(tt)-sum(ss);  
  50.         if(k<=tmp) {  
  51.             r=m;  
  52.             for(int i=ss; i>0; i-=lowbit(i))  
  53.                 use[i]=ls[use[i]];  
  54.             for(int i=tt; i>0; i-=lowbit(i))  
  55.                 use[i]=ls[use[i]];  
  56.         } else {  
  57.             l=m+1;  
  58.             k-=tmp;  
  59.             for(int i=ss; i>0; i-=lowbit(i))  
  60.                 use[i]=rs[use[i]];  
  61.             for(int i=tt; i>0; i-=lowbit(i))  
  62.                 use[i]=rs[use[i]];  
  63.         }  
  64.     }  
  65.     return l;  
  66. }  
  67. int a[MAXN];  
  68. int main() {  
  69.     int m,l,r,k;  
  70.     char op;  
  71.     while(~scanf("%d%d",&n,&m)) {  
  72.         tot=0;  
  73.         memset(root,0,sizeof(root));  
  74.         for(int i=1; i<=n; ++i) {  
  75.             scanf("%d",&a[i]);  
  76.             modify(i,a[i],1);  
  77.         }  
  78.         while(m--) {  
  79.             scanf(" %c%d%d",&op,&l,&r);  
  80.             switch(op) {  
  81.             case 'Q':  
  82.                 scanf("%d",&k);  
  83.                 printf("%d\n",query(l-1,r,0,INF,k));  
  84.                 break;  
  85.             case 'C':  
  86.                 modify(l,a[l],-1);  
  87.                 a[l]=r;  
  88.                 modify(l,a[l],1);  
  89.                 break;  
  90.             }  
  91.         }  
  92.     }  
  93. }  

  SkipList(跳表)

  没什么用,这是我对它的评价;它能实现的一切操作用平衡二叉树都可以实现。这里放一个我测过可用,但没花心思修改的版本。

  1. const int MAX_LEVEL=18;  
  2. struct SkipList {  
  3.     struct node {  
  4.         int key,val;  
  5.         node *next[1];  
  6.     };  
  7.     int level;  
  8.     node *head;  
  9.     SkipList() {  
  10.         level=0;  
  11.         head=NewNode(MAX_LEVEL-1,0,0);  
  12.         for(int i=0; i<MAX_LEVEL; ++i)  
  13.             head->next[i]=NULL;  
  14.     }  
  15.     node* NewNode(int level,int key,int val) {  
  16.   
  17.         node *ns=(node *)malloc(sizeof(node)+level*sizeof(node*));  
  18.         ns->key=key;  
  19.         ns->val=val;  
  20.         return ns;  
  21.     }  
  22.     int randomLevel() {  
  23.         int k=1;  
  24.         while(rand()&1)  
  25.             ++k;  
  26.         return k<MAX_LEVEL?k:MAX_LEVEL;  
  27.     }  
  28.     int find(int key) {  
  29.         node *p=head,*q=NULL;  
  30.         for(int i=level-1; i>=0; --i)  
  31.             while((q=p->next[i])&&q->key<=key) {  
  32.                 if(q->key==key)  
  33.                     return q->val;  
  34.                 p=q;  
  35.             }  
  36.         return -INF;  
  37.     }  
  38.     bool insert(int key,int val) {  
  39.         node *update[MAX_LEVEL],*p=head,*q=NULL;  
  40.         for(int i=level-1; i>=0; --i) {  
  41.             while((q=p->next[i])&&q->key<key)  
  42.                 p=q;  
  43.             update[i]=p;  
  44.         }  
  45.         if(q&&q->key==key)  
  46.             return false;  
  47.         int k=randomLevel();  
  48.         if(k>level) {  
  49.             for(int i=level; i<k; ++i)  
  50.                 update[i]=head;  
  51.             level=k;  
  52.         }  
  53.         q=NewNode(k,key,val);  
  54.         for(int i=0; i<k; ++i) {  
  55.             q->next[i]=update[i]->next[i];  
  56.             update[i]->next[i]=q;  
  57.         }  
  58.         return true;  
  59.     }  
  60.     bool erase(int key) {  
  61.         node *update[MAX_LEVEL],*p=head,*q=NULL;  
  62.         for(int i=level-1; i>=0; --i) {  
  63.             while((q=p->next[i])&&q->key<key)  
  64.                 p=q;  
  65.             update[i]=p;  
  66.         }  
  67.         if(q&&q->key==key) {  
  68.             for(int i=0; i<level; ++i)  
  69.                 if(update[i]->next[i]==q)  
  70.                     update[i]->next[i]=q->next[i];  
  71.             free(q);  
  72.             for(int i=level-1; i>=0; --i)  
  73.                 if(head->next[i]==NULL)  
  74.                     --level;  
  75.             return true;  
  76.         }  
  77.         return false;  
  78.     }  
  79.     node* getMin() {  
  80.         return head->next[0];  
  81.     }  
  82.     node* getMax() {  
  83.         node *p=head,*q=NULL;  
  84.         for(int i=level-1; i>=0; --i)  
  85.             while((q=p->next[i])&&q)  
  86.                 p=q;  
  87.         return p==head?NULL:p;  
  88.     }  
  89. };  

  平衡二叉树

  平衡二叉树是维护集合有序性的常用数据结构。我会的平衡二叉树并不多,基本以实用为原则,有Treap、Size Balanced Tree(以下简称SBT)和Splay。它们的很多操作的实现方式完全一致或大同小异,这里为节省篇幅一并放出;注意这些操作适用于不允许重复值的平衡二叉树(set而非multiset),对于允许重复值(拥有cnt域)的实现,只要在一些+1的地方稍作修改(改成cnt[x])即可。一些使用的例题可以在这篇Blog里找到,不再赘述。

  1. bool find(int v) {  
  2.     for(int x=root; x; x=ch[x][key[x]<v])  
  3.         if(key[x]==v)  
  4.             return true;  
  5.     return false;  
  6. }  
  7. int get_kth(int k) {  
  8.     int x=root;  
  9.     while(size[ch[x][0]]+1!=k)  
  10.         if(k<size[ch[x][0]]+1)  
  11.             x=ch[x][0];  
  12.         else {  
  13.             k-=size[ch[x][0]]+1;  
  14.             x=ch[x][1];  
  15.         }  
  16.     return key[x];  
  17. }  
  18. int get_rank(int v) {  
  19.     int ret=0,x=root;  
  20.     while(x)  
  21.         if(v<key[x])  
  22.             x=ch[x][0];  
  23.         else {  
  24.             ret+=size[ch[x][0]]+1;  
  25.             x=ch[x][1];  
  26.         }  
  27.     return ret;  
  28. }  
  29. int get_pre(int v) {  
  30.     int x=root,y=0;  
  31.     while(x)  
  32.         if(v<key[x])  
  33.             x=ch[x][0];  
  34.         else {  
  35.             y=x;  
  36.             x=ch[x][1];  
  37.         }  
  38.     return y;  
  39. }  
  40. int get_next(int v) {  
  41.     int x=root,y=0;  
  42.     while(x)  
  43.         if(v>key[x])  
  44.             x=ch[x][1];  
  45.         else {  
  46.             y=x;  
  47.             x=ch[x][0];  
  48.         }  
  49.     return y;  
  50. }  
  51. int get_min() {  
  52.     if(size[root]==0)  
  53.         return -1;  
  54.     int x=root;  
  55.     while(ch[x][0])  
  56.         x=ch[x][0];  
  57.     return x;  
  58. }  
  59. int get_max() {  
  60.     if(size[root]==0)  
  61.         return -1;  
  62.     int x=root;  
  63.     while(ch[x][1])  
  64.         x=ch[x][1];  
  65.     return x;  
  66. }  
  67. void Treaval(int x) {  
  68.     if(x) {  
  69.         Treaval(ch[x][0]);  
  70.         printf("结点%2d:左儿子 %2d 右儿子 %2d size = %2d ,val = %2d\n",x,ch[x][0],ch[x][1],size[x],key[x]);  
  71.         Treaval(ch[x][1]);  
  72.     }  
  73. }  
  74. void debug() {  
  75.     printf("root:%d\n",root);  
  76.     Treaval(root);  
  77.     putchar('\n');  
  78. }  

  基于旋转的Treap

  Treap大概是赛场上最常见的平衡二叉树,它同时维护序列的有序性和堆的性质,依靠堆值的随机化,将树的高度维护在期望下平衡的程度,从而实现了各种操作期望O(logn)的复杂度。它的性价比高在只有两种旋转(而且可以合并地写),比红黑树和AVL短小;又因为复杂度基于期望而非均摊,在各种数据下都有良好的表现。这里只给出允许重复值的实现。

  1. struct Treap {  
  2.     int tot,root;  
  3.     int ch[MAXN][2],key[MAXN],pt[MAXN],cnt[MAXN],size[MAXN];  
  4.     void init() {  
  5.         tot=root=0;  
  6.         pt[0]=INF;  
  7.     }  
  8.     void push_up(int x) {  
  9.         size[x]=size[ch[x][0]]+size[ch[x][1]]+cnt[x];  
  10.     }  
  11.     void new_node(int &x,int v) {  
  12.         x=++tot;  
  13.         ch[x][0]=ch[x][1]=0;  
  14.         size[x]=cnt[x]=1;  
  15.         pt[x]=rand();  
  16.         key[x]=v;  
  17.     }  
  18.     void rotate(int &x,int f) {  
  19.         int y=ch[x][f];  
  20.         ch[x][f]=ch[y][f^1];  
  21.         ch[y][f^1]=x;  
  22.         push_up(x);  
  23.         push_up(y);  
  24.         x=y;  
  25.     }  
  26.     void insert(int &x,int v) {  
  27.         if(!x) {  
  28.             new_node(x,v);  
  29.             return;  
  30.         }  
  31.         if(key[x]==v)  
  32.             ++cnt[x];  
  33.         else {  
  34.             int f=key[x]<v;  
  35.             insert(ch[x][f],v);  
  36.             if(pt[ch[x][f]]<pt[x])  
  37.                 rotate(x,f);  
  38.         }  
  39.         push_up(x);  
  40.     }  
  41.     void erase(int &x,int v) {  
  42.         if(!x)  
  43.             return;  
  44.         if(key[x]==v) {  
  45.             if(cnt[x]>1)  
  46.                 --cnt[x];  
  47.             else {  
  48.                 if(!ch[x][0]&&!ch[x][1])  
  49.                     x=0;  
  50.                 else {  
  51.                     rotate(x,pt[ch[x][0]]>pt[ch[x][1]]);  
  52.                     erase(x,v);  
  53.                 }  
  54.             }  
  55.         } else  
  56.             erase(ch[x][key[x]<v],v);  
  57.         push_up(x);  
  58.     }  
  59.     void insert(int v) {  
  60.         insert(root,v);  
  61.     }  
  62.     void erase(int v) {  
  63.         erase(root,v);  
  64.     }  
  65. };  

  Size Balanced Tree

  这个由陈启峰发明的数据结构依靠其独特的平摊时间O(1)的Maintain操作,具有仅次于红黑树的优秀的时间效率,但由于赛场上Treap已经够用,也因为有人指出陈启峰在复杂度的证明上有漏洞(有人声称某些数据可以使SBT退化成人字形,可以想象一下成串的鞭炮的样子),使用的人并不多。我也只是大致学习了一下,用的次数并不多。这里给出不允许重复值的实现。

  1. struct SBT {  
  2.     int root,tot;  
  3.     int ch[MAXN][2],key[MAXN],size[MAXN];  
  4.     void init() {  
  5.         tot=root=0;  
  6.         size[0]=0;  
  7.     }  
  8.     void rotate(int &x,int f) {  
  9.         int y=ch[x][f];  
  10.         ch[x][f]=ch[y][f^1];  
  11.         ch[y][f^1]=x;  
  12.         size[y]=size[x];  
  13.         size[x]=size[ch[x][0]]+size[ch[x][1]]+1;  
  14.         x=y;  
  15.     }  
  16.     void maintain(int &x,int f) {  
  17.         if(size[ch[ch[x][f]][f]]>size[ch[x][f^1]])  
  18.             rotate(x,f);  
  19.         else if(size[ch[ch[x][f]][f^1]]>size[ch[x][f^1]]) {  
  20.             rotate(ch[x][f],f^1);  
  21.             rotate(x,f);  
  22.         } else  
  23.             return;  
  24.         maintain(ch[x][0],0);  
  25.         maintain(ch[x][1],1);  
  26.         maintain(x,0);  
  27.         maintain(x,1);  
  28.     }  
  29.     void insert(int &x,int v) {  
  30.         if(!x) {  
  31.             x=++tot;  
  32.             ch[x][0]=ch[x][1]=0;  
  33.             size[x]=1;  
  34.             key[x]=v;  
  35.         } else {  
  36.             ++size[x];  
  37.             insert(ch[x][key[x]<v],v);  
  38.             maintain(x,key[x]<v);  
  39.         }  
  40.     }  
  41.     int erase(int &x,int v) {  
  42.         if(!x)  
  43.             return 0;  
  44.         --size[x];  
  45.         if(key[x]==v||(key[x]>v&&!ch[x][0])||(key[x]<v&&!ch[x][1])) {  
  46.             int ret=key[x];  
  47.             if(ch[x][0]&&ch[x][1])  
  48.                 key[x]=erase(ch[x][0],v+1);  
  49.             else  
  50.                 x=ch[x][0]+ch[x][1];  
  51.             return ret;  
  52.         }  
  53.         return erase(ch[x][key[x]<v],v);  
  54.     }  
  55.     void insert(int v) {  
  56.         insert(root,v);  
  57.     }  
  58.     void erase(int v) {  
  59.         erase(root,v);  
  60.     }  
  61. };  

  Splay(伸展树)

  Splay因其独特性的splay操作(将某一节点旋转到另一节点下)而得名,是一种非常灵活的,在现实生活中应用也非常广泛的二叉查找树;称之为平衡二叉树是不太严谨的,因为它从来不保证高度平衡,而是每次将访问过的节点旋转到根。也因这一操作,Splay变得异常强大,可以实现很多其它平衡树无法实现的操作(如区间翻转),它更像平衡树和线段树的杂糅,既可以维护集合信息,也可以维护序列信息,可以用它来做Treap的题,也可以用它来做线段树的题。更重要的是,Splay可以实现split(将某棵子树从原树中分离)和merge操作(将某棵子树插入另一棵树),这也使得区间插入删除成为可能。它的美中不足是常数稍大,约是Treap的1.5~3倍,线段树的2~5倍。

  Splay有单旋和双旋两种实现,其中只有双旋保证了均摊O(logn)的单次操作复杂度,但因为很多人认为zigzag太长不好敲(大多是OI选手有此困扰),选择了单旋。其实完全可以稍微损失一点常数,合并成一个rotate函数来完成双旋。此外一个良好的实现通常要在序列一首一尾增加两个哨兵节点,这样可以减少很多边界特判。

  有必要进行的扩展性说明是,对于一棵树,如果想要维护子树信息,我们可以用Splay维护这棵树的括号序列(dfs序),这样便可以轻易split出任意子树所属的区间;而用Splay维护dfs序的结构,就是Euler-Tour Tree。同样的,如果想要维护链上信息,可以先树链剖分然后用Splay维护每条重链,根据杨哲在07年国家集训队作业的计算,因其势能分析得到的复杂度依然是单次操作均摊O(logn)复杂度;而类似的思想做些转化,就变成了后面会提到的Link-Cut Tree(以下简称LCT)。

  这里给出了POJ3580的Splay实现。注意我这次erase函数写了内存回收,这一做法完全可以照搬到其它平衡树中。

  1. #include<cstdio>  
  2. #include<algorithm>  
  3. using namespace std;  
  4. #define keyTree (ch[ch[root][1]][0])  
  5. const int MAXN=200005;  
  6. const int INF=0x3f3f3f3f;  
  7. int num[MAXN];  
  8. struct SplayTree {  
  9.     int root,tot1,tot2;  
  10.     int ch[MAXN][2],pre[MAXN],size[MAXN];  
  11.     int gc[MAXN],que[MAXN];  
  12.     int key[MAXN],vmin[MAXN],add[MAXN],rev[MAXN];  
  13.     void rotate(int x,int f) {  
  14.         int y=pre[x];  
  15.         ch[y][f^1]=ch[x][f];  
  16.         pre[ch[x][f]]=y;  
  17.         pre[x]=pre[y];  
  18.         if(pre[x])  
  19.             ch[pre[y]][ch[pre[y]][1]==y]=x;  
  20.         ch[x][f]=y;  
  21.         pre[y]=x;  
  22.         push_up(y);  
  23.     }  
  24.     void splay(int x,int goal) {  
  25.         push_down(x);  
  26.         while(pre[x]!=goal) {  
  27.             int y=pre[x],z=pre[y];  
  28.             if(z==goal) {  
  29.                 push_down(y);  
  30.                 push_down(x);  
  31.                 rotate(x,ch[y][0]==x);  
  32.             } else {  
  33.                 push_down(z);  
  34.                 push_down(y);  
  35.                 push_down(x);  
  36.                 int f=ch[z][0]==y;  
  37.                 if(ch[y][f]==x)  
  38.                     rotate(x,f^1);  
  39.                 else  
  40.                     rotate(y,f);  
  41.                 rotate(x,f);  
  42.             }  
  43.         }  
  44.         push_up(x);  
  45.         if(goal==0)  
  46.             root=x;  
  47.     }  
  48.     void rotate_to(int k,int goal) {  
  49.         int x=root;  
  50.         push_down(x);  
  51.         while(size[ch[x][0]]!=k) {  
  52.             if(k<size[ch[x][0]])  
  53.                 x=ch[x][0];  
  54.             else {  
  55.                 k-=size[ch[x][0]]+1;  
  56.                 x=ch[x][1];  
  57.             }  
  58.             push_down(x);  
  59.         }  
  60.         splay(x,goal);  
  61.     }  
  62.     void erase(int x) {  
  63.         int fa=pre[x],head=0,tail=0;  
  64.         for(que[tail++]=x; head<tail; ++head) {  
  65.             gc[tot2++]=que[head];  
  66.             if(ch[que[head]][0])  
  67.                 que[tail++]=ch[que[head]][0];  
  68.             if(ch[que[head]][1])  
  69.                 que[tail++]=ch[que[head]][1];  
  70.         }  
  71.         ch[fa][ch[fa][1]==x]=0;  
  72.         push_up(fa);  
  73.     }  
  74.     void new_node(int &x,int v,int fa) {  
  75.         if(tot2)  
  76.             x=gc[--tot2];  
  77.         else  
  78.             x=++tot1;  
  79.         ch[x][0]=ch[x][1]=0;  
  80.         pre[x]=fa;  
  81.         size[x]=1;  
  82.         key[x]=vmin[x]=v;  
  83.         add[x]=rev[x]=0;  
  84.     }  
  85.     void update_add(int x,int d) {  
  86.         if(x) {  
  87.             key[x]+=d;  
  88.             add[x]+=d;  
  89.             vmin[x]+=d;  
  90.         }  
  91.     }  
  92.     void update_rev(int x) {  
  93.         if(x) {  
  94.             swap(ch[x][0],ch[x][1]);  
  95.             rev[x]^=1;  
  96.         }  
  97.     }  
  98.     void push_up(int x) {  
  99.         size[x]=size[ch[x][0]]+size[ch[x][1]]+1;  
  100.         vmin[x]=min(key[x],min(vmin[ch[x][0]],vmin[ch[x][1]]));  
  101.     }  
  102.     void push_down(int x) {  
  103.         if(add[x]) {  
  104.             update_add(ch[x][0],add[x]);  
  105.             update_add(ch[x][1],add[x]);  
  106.             add[x]=0;  
  107.         }  
  108.         if(rev[x]) {  
  109.             update_rev(ch[x][0]);  
  110.             update_rev(ch[x][1]);  
  111.             rev[x]=0;  
  112.         }  
  113.     }  
  114.     void build(int &x,int l,int r,int f) {  
  115.         int m=l+r>>1;  
  116.         new_node(x,num[m],f);  
  117.         if(l<m)  
  118.             build(ch[x][0],l,m-1,x);  
  119.         if(r>m)  
  120.             build(ch[x][1],m+1,r,x);  
  121.         push_up(x);  
  122.     }  
  123.     void init(int n) {  
  124.         root=tot1=tot2=0;  
  125.         ch[0][0]=ch[0][1]=pre[0]=size[0]=0;  
  126.         add[0]=rev[0]=0;  
  127.         key[0]=vmin[0]=INF;  
  128.         new_node(root,-1,0);  
  129.         new_node(ch[root][1],-1,root);  
  130.         size[root]=2;  
  131.         for(int i=1; i<=n; ++i)  
  132.             scanf("%d",&num[i]);  
  133.         build(keyTree,1,n,ch[root][1]);  
  134.         push_up(ch[root][1]);  
  135.         push_up(root);  
  136.     }  
  137.     void plus(int l,int r,int v) {  
  138.         rotate_to(l-1,0);  
  139.         rotate_to(r+1,root);  
  140.         update_add(keyTree,v);  
  141.     }  
  142.     void reverse(int l,int r) {  
  143.         rotate_to(l-1,0);  
  144.         rotate_to(r+1,root);  
  145.         update_rev(keyTree);  
  146.     }  
  147.     void revolve(int l,int r,int k) {  
  148.         k%=r-l+1;  
  149.         if(!k)  
  150.             return;  
  151.         rotate_to(r-k,0);  
  152.         rotate_to(r+1,root);  
  153.         int tmp=keyTree;  
  154.         keyTree=0;  
  155.         push_up(ch[root][1]);  
  156.         push_up(root);  
  157.         rotate_to(l-1,0);  
  158.         rotate_to(l,root);  
  159.         keyTree=tmp;  
  160.         pre[tmp]=ch[root][1];  
  161.         push_up(ch[root][1]);  
  162.         push_up(root);  
  163.     }  
  164.     void insert(int k,int v) {  
  165.         rotate_to(k,0);  
  166.         rotate_to(k+1,root);  
  167.         new_node(keyTree,v,ch[root][1]);  
  168.         push_up(ch[root][1]);  
  169.         push_up(root);  
  170.     }  
  171.     void del(int k) {  
  172.         rotate_to(k-1,0);  
  173.         rotate_to(k+1,root);  
  174.         erase(keyTree);  
  175.         push_up(ch[root][1]);  
  176.         push_up(root);  
  177.     }  
  178.     int query(int l,int r) {  
  179.         rotate_to(l-1,0);  
  180.         rotate_to(r+1,root);  
  181.         return vmin[keyTree];  
  182.     }  
  183. } splay;  
  184. int main() {  
  185.     int n,m,x,y,v;  
  186.     char op[10];  
  187.     while(~scanf("%d",&n)) {  
  188.         splay.init(n);  
  189.         scanf("%d",&m);  
  190.         while(m--) {  
  191.             scanf("%s",op);  
  192.             switch(op[0]) {  
  193.             case 'A':  
  194.                 scanf("%d%d%d",&x,&y,&v);  
  195.                 splay.plus(x,y,v);  
  196.                 break;  
  197.             case 'R':  
  198.                 scanf("%d%d",&x,&y);  
  199.                 if(op[3]=='E')  
  200.                     splay.reverse(x,y);  
  201.                 else {  
  202.                     scanf("%d",&v);  
  203.                     splay.revolve(x,y,v);  
  204.                 }  
  205.                 break;  
  206.             case 'I':  
  207.                 scanf("%d%d",&x,&v);  
  208.                 splay.insert(x,v);  
  209.                 break;  
  210.             case 'D':  
  211.                 scanf("%d",&x);  
  212.                 splay.del(x);  
  213.                 break;  
  214.             case 'M':  
  215.                 scanf("%d%d",&x,&y);  
  216.                 printf("%d\n",splay.query(x,y));  
  217.                 break;  
  218.             }  
  219.         }  
  220.     }  
  221. }  

  额外的更新。哨兵节点的存在有利有弊,在对树进行split和merge的时候有时会出现一些调试上的不便,在某些时候显得代码不够优雅。由此,我参照交大板,结合LCT的一些函数写了新的实现。以下是HDU4453的Splay实现,当然用上面的做法和后面提到的不基于旋转的Treap同样可做。

  1. #include<cstdio>  
  2. #include<algorithm>  
  3. using namespace std;  
  4. const int MAXN=200005;  
  5. int k1,k2,num[MAXN];  
  6. struct Splay {  
  7.     int root,tot,point;  
  8.     int ch[MAXN][2],pre[MAXN],size[MAXN];  
  9.     int key[MAXN],add[MAXN],rev[MAXN];  
  10.     bool isroot(int x) {  
  11.         return !pre[x]||ch[pre[x]][0]!=x&&ch[pre[x]][1]!=x;  
  12.     }  
  13.     void rotate(int x) {  
  14.         int y=pre[x],f=ch[y][1]==x;  
  15.         ch[y][f]=ch[x][f^1];  
  16.         pre[ch[y][f]]=y;  
  17.         if(!isroot(y))  
  18.             ch[pre[y]][ch[pre[y]][1]==y]=x;  
  19.         pre[x]=pre[y];  
  20.         ch[x][f^1]=y;  
  21.         pre[y]=x;  
  22.         push_up(y);  
  23.     }  
  24.     void splay(int x) {  
  25.         push_down(x);  
  26.         while(!isroot(x)) {  
  27.             int y=pre[x],z=pre[y];  
  28.             if(isroot(y)) {  
  29.                 push_down(y);  
  30.                 push_down(x);  
  31.                 rotate(x);  
  32.             } else {  
  33.                 push_down(z);  
  34.                 push_down(y);  
  35.                 push_down(x);  
  36.                 rotate((ch[z][1]==y)==(ch[y][1]==x)?y:x);  
  37.                 rotate(x);  
  38.             }  
  39.         }  
  40.         push_up(x);  
  41.     }  
  42.     void new_node(int &x,int v,int fa) {  
  43.         x=++tot;  
  44.         ch[x][0]=ch[x][1]=0;  
  45.         pre[x]=fa;  
  46.         size[x]=1;  
  47.         key[x]=v;  
  48.         add[x]=rev[x]=0;  
  49.     }  
  50.     void update_add(int x,int v) {  
  51.         if(x) {  
  52.             key[x]+=v;  
  53.             add[x]+=v;  
  54.         }  
  55.     }  
  56.     void update_rev(int x) {  
  57.         if(x) {  
  58.             rev[x]^=1;  
  59.             swap(ch[x][0],ch[x][1]);  
  60.         }  
  61.     }  
  62.     void push_down(int x) {  
  63.         if(add[x]) {  
  64.             update_add(ch[x][0],add[x]);  
  65.             update_add(ch[x][1],add[x]);  
  66.             add[x]=0;  
  67.         }  
  68.         if(rev[x]) {  
  69.             update_rev(ch[x][0]);  
  70.             update_rev(ch[x][1]);  
  71.             rev[x]=0;  
  72.         }  
  73.     }  
  74.     void push_up(int x) {  
  75.         size[x]=size[ch[x][0]]+size[ch[x][1]]+1;  
  76.     }  
  77.     void build(int &x,int l,int r,int fa) {  
  78.         int m=l+r>>1;  
  79.         new_node(x,num[m],fa);  
  80.         if(l<m)  
  81.             build(ch[x][0],l,m-1,x);  
  82.         if(r>m)  
  83.             build(ch[x][1],m+1,r,x);  
  84.         push_up(x);  
  85.     }  
  86.     void init(int n) {  
  87.         root=tot=size[0]=0;  
  88.         for(int i=1; i<=n; ++i)  
  89.             scanf("%d",&num[i]);  
  90.         build(root,1,n,0);  
  91.         point=1;  
  92.     }  
  93.     int find(int rt,int k) {  
  94.         int x=rt;  
  95.         while(size[ch[x][0]]+1!=k) {  
  96.             push_down(x);  
  97.             if(k<=size[ch[x][0]])  
  98.                 x=ch[x][0];  
  99.             else {  
  100.                 k-=size[ch[x][0]]+1;  
  101.                 x=ch[x][1];  
  102.             }  
  103.         }  
  104.         return x;  
  105.     }  
  106.     void split(int &x,int &y,int sz) {  
  107.         if(!sz) {  
  108.             y=x;  
  109.             x=0;  
  110.             return;  
  111.         }  
  112.         y=find(x,sz+1);  
  113.         splay(y);  
  114.         x=ch[y][0];  
  115.         ch[y][0]=0;  
  116.         push_up(y);  
  117.     }  
  118.     void split3(int &x,int &y,int &z,int l,int r) {  
  119.         split(x,z,r);  
  120.         split(x,y,l-1);  
  121.     }  
  122.     void join(int &x,int &y) {  
  123.         if(!x||!y) {  
  124.             x|=y;  
  125.             return;  
  126.         }  
  127.         x=find(x,size[x]);  
  128.         splay(x);  
  129.         ch[x][1]=y;  
  130.         pre[y]=x;  
  131.         push_up(x);  
  132.     }  
  133.     void join3(int &x,int y,int z) {  
  134.         join(y,z);  
  135.         join(x,y);  
  136.     }  
  137.     void evert() {  
  138.         if(point>1) {  
  139.             int x;  
  140.             split(root,x,point-1);  
  141.             swap(root,x);  
  142.             join(root,x);  
  143.             point=1;  
  144.         }  
  145.     }  
  146.     void plus(int v) {  
  147.         evert();  
  148.         int x,y;  
  149.         split3(root,x,y,point,point+k2-1);  
  150.         update_add(x,v);  
  151.         join3(root,x,y);  
  152.     }  
  153.     void reverse() {  
  154.         evert();  
  155.         int x,y;  
  156.         split3(root,x,y,point,point+k1-1);  
  157.         update_rev(x);  
  158.         join3(root,x,y);  
  159.     }  
  160.     void insert(int v) {  
  161.         evert();  
  162.         int x,y;  
  163.         split(root,x,point);  
  164.         new_node(y,v,0);  
  165.         join3(root,y,x);  
  166.     }  
  167.     void erase() {  
  168.         evert();  
  169.         int x,y;  
  170.         split3(root,x,y,point,point);  
  171.         join(root,y);  
  172.     }  
  173.     void move(int tag) {  
  174.         switch(tag) {  
  175.         case 1:  
  176.             if(--point==0)  
  177.                 point=size[root];  
  178.             break;  
  179.         case 2:  
  180.             if(++point==size[root]+1)  
  181.                 point=1;  
  182.             break;  
  183.         }  
  184.     }  
  185.     void query() {  
  186.         evert();  
  187.         int x,y;  
  188.         split3(root,x,y,point,point);  
  189.         printf("%d\n",key[x]);  
  190.         join3(root,x,y);  
  191.     }  
  192. } splay;  
  193. int main() {  
  194.     int n,m,v,cas=0;  
  195.     char op[10];  
  196.     while(~scanf("%d%d%d%d",&n,&m,&k1,&k2)&&(n||m||k1||k2)) {  
  197.         splay.init(n);  
  198.         printf("Case #%d:\n",++cas);  
  199.         while(m--) {  
  200.             scanf("%s",op);  
  201.             switch(op[0]) {  
  202.             case 'a':  
  203.                 scanf("%d",&v);  
  204.                 splay.plus(v);  
  205.                 break;  
  206.             case 'r':  
  207.                 splay.reverse();  
  208.                 break;  
  209.             case 'i':  
  210.                 scanf("%d",&v);  
  211.                 splay.insert(v);  
  212.                 break;  
  213.             case 'd':  
  214.                 splay.erase();  
  215.                 break;  
  216.             case 'm':  
  217.                 scanf("%d",&v);  
  218.                 splay.move(v);  
  219.                 break;  
  220.             case 'q':  
  221.                 splay.query();  
  222.                 break;  
  223.             }  
  224.         }  
  225.     }  
  226. }  

  Link-Cut Tree

  动态树是维护森林信息的一类问题的总称。最常见的也是我唯一会的就是LCT;此外还有自适应Top Tree、全局平衡二叉树等。LCT可以维护多棵树(森林)的形态,并在O(logn)的时间复杂度内维护链上信息;但LCT处理子树信息将会非常麻烦。它的核心操作是access函数,可以把某个节点到根的路径上所有点按照深度用Splay维护起来,从而结合evert函数(换跟操作)和splay操作可以实现对链的信息维护。LCT几乎可以实现除维护子树信息外以上的所有操作,同时有着优越的理论复杂度,但实际常数较大,很多不改变树形态的题用O(logn)的LCT并不比O(log^2n)的树链剖分套线段树更优越。以下是HDU4010的LCT实现。

  1. #include<cstdio>  
  2. #include<cstring>  
  3. #include<algorithm>  
  4. using namespace std;  
  5. const int MAXN=300005;  
  6. struct LCT {  
  7.     int ch[MAXN][2],pre[MAXN],key[MAXN],rev[MAXN];  
  8.     int add[MAXN],vmax[MAXN];  
  9.     bool isroot(int x) {  
  10.         return !pre[x]||ch[pre[x]][0]!=x&&ch[pre[x]][1]!=x;  
  11.     }  
  12.     void rotate(int x) {  
  13.         int y=pre[x],f=ch[y][1]==x;  
  14.         ch[y][f]=ch[x][f^1];  
  15.         pre[ch[y][f]]=y;  
  16.         if(!isroot(y))  
  17.             ch[pre[y]][ch[pre[y]][1]==y]=x;  
  18.         pre[x]=pre[y];  
  19.         ch[x][f^1]=y;  
  20.         pre[y]=x;  
  21.         push_up(y);  
  22.     }  
  23.     void splay(int x) {  
  24.         push_down(x);  
  25.         while(!isroot(x)) {  
  26.             int y=pre[x],z=pre[y];  
  27.             if(isroot(y)) {  
  28.                 push_down(y);  
  29.                 push_down(x);  
  30.                 rotate(x);  
  31.             } else {  
  32.                 push_down(z);  
  33.                 push_down(y);  
  34.                 push_down(x);  
  35.                 rotate((ch[z][1]==y)==(ch[y][1]==x)?y:x);  
  36.                 rotate(x);  
  37.             }  
  38.         }  
  39.         push_up(x);  
  40.     }  
  41.     int access(int x) {  
  42.         int y=0;  
  43.         for(; x; x=pre[x]) {  
  44.             splay(x);  
  45.             ch[x][1]=y;  
  46.             push_up(x);  
  47.             y=x;  
  48.         }  
  49.         return y;  
  50.     }  
  51.     void evert(int x) {  
  52.         rev[access(x)]^=1;  
  53.         splay(x);  
  54.     }  
  55.     void push_up(int x) {  
  56.         vmax[x]=max(max(vmax[ch[x][0]],vmax[ch[x][1]]),key[x]);  
  57.     }  
  58.     void push_down(int x) {  
  59.         if(add[x]) {  
  60.             key[x]+=add[x];  
  61.             if(ch[x][0]) {  
  62.                 add[ch[x][0]]+=add[x];  
  63.                 vmax[ch[x][0]]+=add[x];  
  64.             }  
  65.             if(ch[x][1]) {  
  66.                 add[ch[x][1]]+=add[x];  
  67.                 vmax[ch[x][1]]+=add[x];  
  68.             }  
  69.             add[x]=0;  
  70.         }  
  71.         if(rev[x]) {  
  72.             if(ch[x][0])  
  73.                 rev[ch[x][0]]^=1;  
  74.             if(ch[x][1])  
  75.                 rev[ch[x][1]]^=1;  
  76.             swap(ch[x][0],ch[x][1]);  
  77.             rev[x]=0;  
  78.         }  
  79.     }  
  80.     int find_root(int x) {  
  81.         while(pre[x])  
  82.             x=pre[x];  
  83.         return x;  
  84.     }  
  85.     void link(int u,int v) {  
  86.         if(find_root(u)==find_root(v)) {  
  87.             puts("-1");  
  88.             return;  
  89.         }  
  90.         evert(u);  
  91.         pre[u]=v;  
  92.     }  
  93.     void cut(int u,int v) {  
  94.         if(u==v||find_root(u)!=find_root(v)) {  
  95.             puts("-1");  
  96.             return;  
  97.         }  
  98.         evert(u);  
  99.         access(v);  
  100.         splay(v);  
  101.         pre[ch[v][0]]=0;  
  102.         ch[v][0]=0;  
  103.         push_up(v);  
  104.     }  
  105.     void update(int u,int v,int w) {  
  106.         if(find_root(u)!=find_root(v)) {  
  107.             puts("-1");  
  108.             return;  
  109.         }  
  110.         evert(u);  
  111.         access(v);  
  112.         splay(v);  
  113.         add[v]+=w;  
  114.         vmax[v]+=w;  
  115.         push_down(v);  
  116.     }  
  117.     void query(int u,int v) {  
  118.         if(find_root(u)!=find_root(v)) {  
  119.             puts("-1");  
  120.             return;  
  121.         }  
  122.         evert(u);  
  123.         access(v);  
  124.         splay(v);  
  125.         printf("%d\n",vmax[v]);  
  126.     }  
  127.     struct graph {  
  128.         int head[MAXN],to[MAXN<<1],next[MAXN<<1];  
  129.         int tot;  
  130.         void init() {  
  131.             tot=0;  
  132.             memset(head,0xff,sizeof(head));  
  133.         }  
  134.         void add(int u,int v) {  
  135.             to[tot]=v;  
  136.             next[tot]=head[u];  
  137.             head[u]=tot++;  
  138.         }  
  139.     } g;  
  140.     void dfs(int u,int fa) {  
  141.         for(int i=g.head[u]; ~i; i=g.next[i]) {  
  142.             int v=g.to[i];  
  143.             if(v!=fa) {  
  144.                 dfs(v,u);  
  145.                 pre[v]=u;  
  146.             }  
  147.         }  
  148.     }  
  149.     void init(int n) {  
  150.         int m,x,y;  
  151.         g.init();  
  152.         for(int i=1; i<n; ++i) {  
  153.             scanf("%d%d",&x,&y);  
  154.             g.add(x,y);  
  155.             g.add(y,x);  
  156.         }  
  157.         memset(ch,0,sizeof(ch));  
  158.         memset(pre,0,sizeof(pre));  
  159.         memset(rev,0,sizeof(rev));  
  160.         memset(add,0,sizeof(add));  
  161.         vmax[0]=0;  
  162.         for(int i=1; i<=n; ++i) {  
  163.             scanf("%d",&key[i]);  
  164.             vmax[i]=key[i];  
  165.         }  
  166.         dfs(1,0);  
  167.     }  
  168. } lct;  
  169. int main() {  
  170.     int n,q,op,x,y,w;  
  171.     while(~scanf("%d",&n)) {  
  172.         lct.init(n);  
  173.         scanf("%d",&q);  
  174.         while(q--) {  
  175.             scanf("%d",&op);  
  176.             switch(op) {  
  177.             case 1:  
  178.                 scanf("%d%d",&x,&y);  
  179.                 lct.link(x,y);  
  180.                 break;  
  181.             case 2:  
  182.                 scanf("%d%d",&x,&y);  
  183.                 lct.cut(x,y);  
  184.                 break;  
  185.             case 3:  
  186.                 scanf("%d%d%d",&w,&x,&y);  
  187.                 lct.update(x,y,w);  
  188.                 break;  
  189.             case 4:  
  190.                 scanf("%d%d",&x,&y);  
  191.                 lct.query(x,y);  
  192.                 break;  
  193.             }  
  194.         }  
  195.         putchar('\n');  
  196.     }  }
  197. }  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值