平衡树·从BST到treap到fhq-treap

1.BST

1.1 BST是啥

要说平衡树必须要从BST( B i n a r y   S e a r c h   T r e e Binary\ Search\ Tree Binary Search Tree)说起

我们定义一棵BST满足一下性质

1.是一棵二叉树,既每个点最多拥有两个儿子
2.对于每个节点,满足他的左儿子 < < < < < <他的右儿子

是不是很简单呢?

1.2 BST的基本操作

我们发现,利用BST的这个特殊的性质,可以解决很多东西
虽然大家肯定也不会写BST,但是这一部分建议大家还是读一读,因为后面的一些过程和这里是差不多的

1.2.1 插入x

我们发现,对于给定的一棵BST,我们是可以定位出下一个东西是在哪里的
具体流程是:
1.从根开始查找
2.对于经过的每一个点 i i i,如果 x < v a l i x<val_i x<vali,根据定义,我们应该查询他的左子树,反之则右子树,当 i = 0 i=0 i=0的时候就是我们要插入的地方

当然如果 x = v a l i x=val_i x=vali的时候可能还需要出现特判的情况,这就需要在题目中具体情况具体分析。当然你以后肯定也不会写这玩意

1.2.2 删除x

同样的,我们按照插入的时候的方法,找到我们要删除的位置,然后直接断掉一条边就可以

1.2.3 查询x的排名

这个东西怎么做呢?
我们还是从根节点开始查找,记录一个 r e s res res表示答案
对于我们查到到的一个点 i i i
如果 x < v a l i x<val_i x<vali i = l c i=lc i=lc
如果 x = v a l i x=val_i x=vali,那么我们就要查的是 i i i的排名,答案应该是 r e s res res加上左子树的大小再加上一对吧,也就是 r e s = r e s + s i z [ l c ] + 1 res=res+siz[lc]+1 res=res+siz[lc]+1
如果 x > v a l i x>val_i x>vali,说明 i i i在右子树,那么所有的左子树的部分和 i i i都比 x x x小,所以 r e s = r e s + s i z [ l c ] + 1 , u = r c res=res+siz[lc]+1,u=rc res=res+siz[lc]+1,u=rc

很好理解,下面给出简要代码,为了后面写的方便,我们用 s o n [ u ] [ 0 ] son[u][0] son[u][0]表示 u u u的左子树, 1 1 1表示右子树

int rnk(int x){
    int u=rt,res=0;
    while(1){
        if(val[u]>x)u=lc;
        else if(val[u]==x)return res+siz[son[u][0]]+1;
        else res+=siz[son[u][0]]+1,u=son[u][1];
    }
}

1.2.4 查询排名为x的数

这个东西根上面就是非常类似的了
就是把上面的过程反过来了
如果 x ≤ s i z [ l c ] x\leq siz[lc] xsiz[lc],则 i = l c i=lc i=lc
否则如果 x ≤ s i z [ l c ] + 1 x\leq siz[lc]+1 xsiz[lc]+1,则返回 v a l i val_i vali
否则 x = x − s i z [ l c ] − 1 , i = r c x=x-siz[lc]-1,i=rc x=xsiz[lc]1,i=rc

代码:

int kth(int x){
    int u=rt;
    while(1){
        if(siz[son[u][0]]>=x)u=son[u][0];
        else if(siz[son[u][0]]+1>=x)return val[u];
        else x-=siz[son[u][0]]+1,u=son[u][1];
    }
}

1.2.5 查询x在数中的编号
这个不难,一直往下走就可以,根据权值决定走做左子树还是右子树

int find(int x){
    int u=rt;
    while(u&&son[u][x>val[u]]!=x)u=son[u][x>val[u]];
    return u;
}

这个地方我们就看出来为什么要写 s o n [ u ] [ 0 ] , s o n [ u ] [ 1 ] son[u][0],son[u][1] son[u][0],son[u][1]

1.2.6 查询x的前驱
这里 x x x的前驱定义为小于 x x x的最大的数

这个其实很简单,首先要找到 x x x,然后考虑BST的定义就可以,首先他的前驱一定在他的左子树里面。然后我们要找最大,所以不停的往右子树走,直到没有右子树,他就是最大的

代码:

int pre(int x){
    int u=find(x);
    u=son[u][0];
    while(son[u][1])u=son[u][1];
    return u;
}

1.2.7 查询x的后继
和上一个非常像,不过是先往右子树走再往左子树走

1.3 时间复杂度

这个东西因为是一个二叉树,那么每次查询的复杂度最坏的时候就是树的深度,但是因为每个节点的插入顺序不同会导致不同形态的BST,那么很容易被退化成一条链,那么这个时候查询一次的复杂度就是 Θ ( n ) \Theta(n) Θ(n),就是平方级别的了

下面给出几种退化的情况(其实很好卡对吧)

在这里插入图片描述

那么 Θ ( n 2 ) \Theta(n^2) Θ(n2)的效率我们显然是无法接受的(和暴力都一样了要他有啥用),所以我们考虑如何优化——于是,平衡树诞生了!

2.从BST到平衡树

我们发现,我们让复杂度尽量的低的一种处理方法就是,让左右两颗子树大小差尽量的小,那么深度就会尽量小。我们把通过一些奇奇怪怪的处理方法使得期望深度为 log ⁡ n \log n logn级别的BST的优化叫做平衡树

平衡树的实现方法有很多,最常见的有

s p l a y splay splay系,通过旋转使得每次操作均摊 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn) 优点是代码短,理解难度低,可以套LCT,是万能的一种平衡树,但是缺点是常数太大,被各种吊打

t r e a p treap treap系,通过旋转+玄学使得每次操作均摊 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn)(我们接下来会讲到) 优点是 f h q   t r e a p fhq\ treap fhq treap是代码最短的平衡树了吧,同时能够基本实现 s p l a y splay splay的各项操作并且快。但是原 t r e a p treap treap的局限性很大,不能维护序列上的问题。包括 f h q   t r e a p fhq\ treap fhq treap在内的 t r e a p treap treap系平衡树都有一个问题就是有点看脸(虽然一般不会被卡),另外 f h q   t r e a p fhq\ treap fhq treap维护LCT会比 s p l a y splay splay维护多一个 log ⁡ \log log

其他快速平衡树,比如SGT,SBT,AVL,RBT等等,都是利用一些奇奇怪怪的技巧让他尽量平衡。优点是常数小,速度快,但是功能都没有 s p l a y splay splay齐全,而且码量都很长(比如SBT,RBT板子码量都在200+左右,但是 s p l a y , t r e a p splay,treap splay,treap写的好看一点都只用80左右)

那么下面我们会着重讲解 t r e a p treap treap系的平衡树,如果想学 s p l a y splay splay也可以点这里

3.treap

3.1 treap是啥

这个词啥意思啊?你翻遍字典也找不着(学术界的或许有),因为这个词本来就是人造出来的,他从这两个词合成的

t r e a p = t r e e + h e a p treap=tree+heap treap=tree+heap

所以一般翻译出来就叫树堆,当然一般没人读中文叫法

t r e e tree tree好理解,二叉查找树嘛,那 h e a p heap heap是怎么来的呢?这就是 t r e a p treap treap的精髓所在

t r e a p treap treap的核心思想是

  • v a l val val满足二叉查找树性质,既 t r e e tree tree
  • 在新储存一个键值,使得这整颗 t r e a p treap treap的键值满足堆性质(我习惯小根堆)

那么键值怎么来呢?题目里又没给。 方法很简单,没有就随机一个嘛,所以这个键值我们就把它命名成 r n d rnd rnd好了(在实际写程序中我习惯写成 t r e a p treap treap

可能光说不太好理解,配张图

在这里插入图片描述

比如在棵 t r e a p treap treap中, v a l val val满足全部BST性质,并且 r n d rnd rnd满足堆性质,既每个点 r n d ≤ rnd\leq rnd他的两个子树的 r n d rnd rnd

然后我们再看几个基本操作

3.2 treap的基本操作

3.2.1 插入x

首先,我们先创建处新的节点,也就是将 r n d rnd rnd随机出来,这里以插入为例

在这里插入图片描述

然后按照权值找到这个点应该加在哪里(也就是BST的插入操作)

在这里插入图片描述

那么这个时候我们的权值是满足条件了,但是 r n d rnd rnd又不满足堆性质了啊
所以我们需要进行上调
等会别着急,这个上调不能直接把它和他爸爸交换啊,要不然 v a l val val的性质又不对了啊

这怎么办啊…
学过 s p l a y splay splay的同学们一定知道,我们需要通过左旋右旋的方式来进行调整

在这里插入图片描述

这里就是左右旋,左旋之后再右旋又回去了,这两个是相对的

大家观察之后会发现,这样旋转之后,新的树仍然满足性质,这个思想也就是 s p l a y splay splay的核心思想

然后我们进行分部上调

回到刚才的过程中,这里我们需要左旋

在这里插入图片描述

变成

在这里插入图片描述

继续右旋

在这里插入图片描述

然后旋转完了之后满足条件还需要把相应的信息更新了,比如说 s i z siz siz什么的

那么这个时候我们就成功的在 t r e a p treap treap中插入的一个数(真累啊)

3.2.2 删除x
一个比较常见的操作是找到该点,然后把那个点的值调为 + ∞ +\infty +,然后再维护堆性质,这个时候这个点就会下去了,具体看演示

在这里插入图片描述

我们要删除27,先找到该节点,然后将随机值改成inf,然后比较左右子树的 r n d rnd rnd大小,把根转到叶子,这个时候和插入就不太一样了

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

以此类推,等他转到最下面的时候,直接断掉他和他爸爸之间的边就可以,这样我们就完成了删除节点的操作

3.2.3 其他操作

和BST基本一样,详见上面说的1.2.31.2.7部分

3.3 代码

相信经过上面的讲解最关键的问题已经都解决了,接下来就是写代码了

但是因为我从来没写过treap所以这里放一个简单写的核心代码,既插入和删除,其他部分上面我们都有讲到,应该不难写

void rotate(int &u, int d){//x代表的是旋转时作为父节点的节点,d代表的是旋转的方向
//d==0时是左儿子旋上来, d==1是右儿子旋上来.
    int c=son[u][d];
    son[u][d]=son[c][d^1];
    son[c][d^1]=u; 
    update(u),update(u=c);//更新相应信息
}
void insert(int &u, int val){
    if(!u){//找到对应位置就新建节点
        u=++tot;//节点数
        cnt[u]=siz[u]=1;
       val[u]=val,rnd[u]=rand();
        return;
    }
    siz[u]++;//因为插入了数,所以在路径上每个节点的size都会加1
    if(val[u]==val){cnt[u]; return;}//找到了直接返回
    int d=val[u]<val;
    insert(son[u][d],val);//否则递归查找插入位置
    if(rnd[u]>rnd[son[u][d]])rotate(u,d);
}
void delet(int &u, int val){
    if(!u) return;//防止越界
    if(val[u]==val){
        if(cnt[u]>1){cnt[u]--,siz[u]--;return;}//有相同的就直接cnt--
        bool d=rnd[son[u][0]]>rnd[son[u][1]];
        if(!son[u][0]||!son[u][1])u=son[u][0]|son[u][1]//只有一个儿子就直接把那个儿子放到这个位置
        else rotate(u,d), delet(u,val);//否则将x旋下去,找一个随机值小的替代,直到回到1,2种情况
    }
    else siz[u]--, delet(son[u][val>val[u]], val);//递归找到要删除的节点.
}

3.4 treap一些问题

3.4.1 时间复杂度
t r e a p treap treap的时间其实算是靠脸拿分,如果你脸比较黑生成的随机数刚好让你的 t r e a p treap treap退化成一条链,那也没辙。

但是这种情况一般不会出现,只要你没有忘掉srand(time(0)),当然虽然大部分题目不写这一行也能AC,但是很容易会退化,甚至比 s p l a y splay splay还要慢

3.4.2 功能
我们发现这些单点修改之类的问题 t r e a p treap treap都是可以支持的,但是我们发现,我们无法对于一段区间进行求和之类的操作,因此功能比较局限

而且转来转去好麻烦啊…

3.4.3 优化
有没有一种 t r e a p treap treap既可以维护一段区间上的问题,又可以不用旋转呢?
没错,是有的,范浩强大佬(%)发明了这样的一种 t r e a p treap treap,因此这样的 t r e a p treap treap就叫做 f h q   t r e a p fhq\ treap fhq treap

同时根据是否旋转,最本身的 t r e a p treap treap叫做双旋 t r e a p treap treap,而 f h q   t r e a p fhq\ treap fhq treap叫做无旋 t r e a p treap treap

f h q   t r e a p fhq\ treap fhq treap的优点有很多很多,比如说可以算是码量最小的平衡树了,同时比 s p l a y splay splay快,虽然稍微比 s p l a y splay splay难理解,但是结合图形还是不难理解的

那么接下来,我们就来讲解一下 f h q   t r e a p fhq\ treap fhq treap,他的根本思想是通过分裂和合并提取出我们想要的部分

t r e a p treap treap?被 f h q fhq fhq碾压好不好

4.fhq-treap

4.1 分裂

我们要把 f h q   t r e a p fhq\ treap fhq treap分裂成两棵平衡树,但是还需要有个条件吧,我们把所有权值 ≤ k \leq k k的分到左树,剩下的分到右树

为了方便,我们用 x x x表示左树根, y y y表示右树根

这个不太好理解,上图

在这里插入图片描述

我们从根节点向下找,我们发现 22 22 22是比 27 27 27小的,所以 22 22 22这个点应该在左树中,既 x = 22 所 在 节 点 编 号 x=22所在节点编号 x=22,而且根据BST性质,他的左子树也应该在左树里面,所以我们拿右子树进行分裂

在这里插入图片描述

然后我们看右子树, 31 31 31 27 27 27大,所以我们拿这个点的左子树进行分裂,而 31 31 31和他的右子树就应该属于分裂之后的右树

在这里插入图片描述

分裂左子树, 27 = 27 27=27 27=27,应该划到左树里,相同的

在这里插入图片描述

继续操作,直到分裂完成

在这里插入图片描述

最后就变成了这个样子的两棵树

在这里插入图片描述

但是这个东西看上去不太好写啊?

s p l i t split split其实有两种分裂方法,一种是按照权值,把权值 ≤ k \leq k k的分裂,当然另一种是按照排名分裂,根据不同的题目各有不同

void split_by_val(int o,int &u,int &v,int k)//o表示当前访问的原平衡树节点,u表示如果分裂到左子树要接到哪里(所以要引用),v表示如果分裂到右子树要接到哪里,k表示按多少分裂
{
    if(!o){u=v=0;return;}//边界条件
    if(val[o]<=k)split_by_val(son[u=o][1],son[o][1],v,k);//应当放到左子树,所以u=o,同时需要分裂o的右子树,所以要分裂的就是son[o][1],v不动
    else split_by_val(son[v=o][0],u,son[o][0],k);//同理,需要往右子树分裂
    update(o);//因为他的子树发生了变化,所以要更新o的信息
}

另一个是按照排名分裂

void split_by_rank(int o,int &u,int &v,int k){
    if(!o){u=v=0;return;}//边界条件
    int rank=siz[son[o][0]]+1;//计算排名
    if(rank<=k)split_by_rank(son[u=o][1],son[o][1],v,k-rank);//应该往右子树进行分裂,但是这个时候我们分裂的排名就应该是k-rank了
    else split_by_rank(son[v=o][0],u,son[o][1],k);//往左子树分裂不用懂
    update(o);
}

然后分裂就没了,程序也很短呢>_<

4.2 合并

要分裂还要合并回去啊,怎么合并呢?注意这里合并要满足我们合并的两棵树 u , v , max ⁡ ( v a l u ) ≤ min ⁡ ( v a l v ) u,v,\max(val_u)\leq\min(val_v) u,v,max(valu)min(valv)才能合并,否则只能启发式合并。

那么怎么合并呢?我们发现因为 r a n k rank rank满足相应的关系,所以我们只需要考虑合并之后的 r n d rnd rnd满足堆性质就可以

比如我们要把这两个东西合并上来,我们比较两个根的 r n k rnk rnk,发现 22 22 22那个点的 r n d rnd rnd比较小,所以我们为了维护小根堆的性质,应该把 22 22 22作为合并之后的新的根。

在这里插入图片描述

然后他的左儿子是不用变的,我们需要用右儿子 25 25 25 31 31 31进行合并

在这里插入图片描述

然后我们再比较这个时候的两个根 25 , 31 25,31 25,31发现 31 31 31 r n d rnd rnd比较小,所以把它拎上来

在这里插入图片描述

那么我们就需要拿 31 31 31的右儿子和 25 25 25进行合并

在这里插入图片描述

再经过比较,我们就得到了

在这里插入图片描述

这样我们就合并完了,我觉得合并比分裂更好理解一点吧

下面是程序

int merge(int u,int v){
    if(!u||!v)return u|v;//边界情况,如果有一个待合并子树空了就返回另一个
    int rt;//合并之后的根
    if(treap[u]<treap[v])son[rt=u][1]=merge(son[u][1],v);//合并右子树(treap就是rnd)
    else son[rt=v][0]=merge(u,son[v][0]);//合并左子树
    return update(rt),rt;
}

4.3 其他操作

4.3.1 插入x

这个东西怎么做呢?显然不能按照之前的遍历的方法了,为什么呢?因为 r n d rnd rnd不满足小根堆了,于是,旋转…

那怎么做呢?之前说的 m e r g e merge merge s p l i t split split是干啥的?

所以插入一个数 k k k的流程就是

1. s p l i t ( r t , x , y , k ) split(rt,x,y,k) split(rt,x,y,k),注意这里是按照权值 s p l i t , x , y split,x,y split,x,y是我们单独开的两个变量表示分裂之后的两个根

2. r t = m e r g e ( m e r g e ( x , n e w n o d e ( k ) ) , y ) rt=merge(merge(x,newnode(k)),y) rt=merge(merge(x,newnode(k)),y)

也就是说我们只要在这两个子树之间添加一个点就可以了,然后合并回去,大功告成

代码在下面

4.3.2 删除x

这个东西怎么做呢?可以先把 ≤ x − 1 \leq x-1 x1的拆出来,再把 > x >x >x的拆出来,但是这样真的对吗?有可能有很多个 v a l = x val=x val=x的对吧,但是我们只要删掉一个,怎么办呢?

删除 k k k的流程

1. s p l i t ( r t , x , z , k ) split(rt,x,z,k) split(rt,x,z,k)
2. s p l i t ( x , x , y , k − 1 ) split(x,x,y,k-1) split(x,x,y,k1)
3. y = m e r g e ( s o n [ y ] [ 0 ] , s o n [ y ] [ 1 ] ) y=merge(son[y][0],son[y][1]) y=merge(son[y][0],son[y][1])这里我们把他的两棵子树合并起来相当于只删掉了一个点了
4. r t = m e r g e ( m e r g e ( x , y ) , z ) rt=merge(merge(x,y),z) rt=merge(merge(x,y),z)注意顺序

4.3.3 查询x的排名

这个怎么办呢?当然我们可以按照BST的方法,但是我们比较懒怎么办呢?

查询 k k k的排名的流程

1. s p l i t ( r t , x , y , k − 1 ) split(rt,x,y,k-1) split(rt,x,y,k1)这里必须是 k − 1 k-1 k1
2. p r i n t   s i z x + 1 print\ siz_x+1 print sizx+1分裂后左树都比他小,再 + 1 +1 +1
3. r t = m e r g e ( x , y ) rt=merge(x,y) rt=merge(x,y)还原

4.3.4 查询排名为x的数
这个东西当然可以仿照上面的写一个按照 r a n k rank rank s p l i t split split
但是太麻烦了,我们还要再写一个 s p l i t split split,所以这里我们一般使用BST的查询方式

4.3.5 查询x的前驱

思路就是先裂出来,然后再找左边最大的

查询 k k k的前驱的流程

1. s p l i t ( r t , x , y , k − 1 ) split(rt,x,y,k-1) split(rt,x,y,k1)
2.查找 x x x中最大的就是前驱
3. r t = m e r g e ( x , y ) rt=merge(x,y) rt=merge(x,y)

4.3.6 查询x的后继

和查询前驱没有什么本质上的区别

4.3.7 查询区间情况

这个也不难,我们可以利用两次 s p l i t split split拎出来需要查询的那一段,让后获得答案之后再合并回去就好了

4.4 代码

这里是洛谷模板题的代码,当然不是加强版

为了清晰一点我把每次操作都写成了函数

#include <bits/stdc++.h>
using namespace std;

# define Rep(i,a,b) for(int i=a;i<=b;i++)
# define _Rep(i,a,b) for(int i=a;i>=b;i--)
# define RepG(i,u) for(int i=head[u];~i;i=e[i].next)

typedef long long ll;

const int N=1e5+5;

template<typename T> void read(T &x){
   x=0;int f=1;
   char c=getchar();
   for(;!isdigit(c);c=getchar())if(c=='-')f=-1;
   for(;isdigit(c);c=getchar())x=(x<<1)+(x<<3)+c-'0';
    x*=f;
}

int n;
int tot;
int son[N][2],val[N],siz[N],treap[N];
int rt,x,y,z;

int newnode(int x){
    int u=++tot;
    son[u][0]=son[u][1]=0;
    val[u]=x,siz[u]=1;
    treap[u]=rand();
    return u;
}

void update(int x){
    siz[x]=siz[son[x][0]]+siz[son[x][1]]+1;
}

int merge(int u,int v){
    if(!u||!v)return u|v;
    int rt;
    if(treap[u]<treap[v])son[rt=u][1]=merge(son[u][1],v);
    else son[rt=v][0]=merge(u,son[v][0]);
    return update(rt),rt;
}

void split(int o,int &u,int &v,int k){
    if(!o){u=v=0;return;}
    if(val[o]<=k)split(son[u=o][1],son[o][1],v,k);
    else split(son[v=o][0],u,son[o][0],k);
    update(o);
}

void ins(int k){
    split(rt,x,y,k);
    rt=merge(merge(x,newnode(k)),y);
}

void del(int k){
    split(rt,x,z,k);
    split(x,x,y,k-1);
    y=merge(son[y][0],son[y][1]);
    rt=merge(merge(x,y),z);
}

void rnk(int k){
    split(rt,x,y,k-1);
    printf("%d\n",siz[x]+1);
    rt=merge(x,y);
}

int kth(int k){
    int u=rt;
    while(1){
        if(siz[son[u][0]]>=k)u=son[u][0];
        else if(siz[son[u][0]]+1>=k)return val[u];
        else k-=siz[son[u][0]]+1,u=son[u][1];
    }
}

int pre(int k){
    split(rt,x,y,k-1);
    int u=x;
    while(son[u][1])u=son[u][1];
    rt=merge(x,y);
    return u;
}

int nxt(int k){
    split(rt,x,y,k);
    int u=y;
    while(son[u][0])u=son[u][0];
    rt=merge(x,y);
    return u;
}

int main()
{
    srand(19260817);
    read(n);
    Rep(i,1,n){
        int opt,x;
        read(opt),read(x);
        switch(opt){
            case 1:ins(x);break;
            case 2:del(x);break;
            case 3:rnk(x);break;
            case 4:printf("%d\n",kth(x));break;
            case 5:printf("%d\n",val[pre(x)]);break;
            case 6:printf("%d\n",val[nxt(x)]);break;
        }
    }
    return 0;
}

4.5 fhq-treap的一些问题

4.5.1 时间复杂度

通常来说只要不脸黑应该是比 s p l a y splay splay和普通 t r e a p treap treap快的,因为旋转操作常数太大

当然理论复杂度还是 Θ ( n log ⁡ n ) \Theta(n\log n) Θ(nlogn)

4.5.2 srand

t r e a p treap treap千万别忘了写 s r a n d srand srand,虽然能对,但是说不定就被卡没了

关于 s r a n d srand srand里面的数,srand(time(0))是可以的,但是不太好调试,可能会写出一些锅有的时候随机的数是对的,有的时候又错了,所以在调试的时候建议大家写srand(一个比较大的数),比如说 19260817 , 1919810 19260817,1919810 19260817,1919810之类的,让随机种子固定。或者当然手写随机数也是可以的

5.写在最后

这篇博客我大概写了快三个小时?
里面的图都是从我老师那里盗的(bushi

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值