支配树dominator tree学习笔记

说实话冬令营之前我都没见过支配树的题,也不知道这是个什么东西(但是学完之后感觉这个东西已经烂大街了qaq)
支配树主要能干这么一件事:
给定起点r,对于一个终点x,如果r到x的所有路径都必须经过y,我们称y支配x
设支配x的点集 S = p 1 , p 2 , p 3..... p k S=p1,p2,p3.....pk S=p1,p2,p3.....pk
其在r到x的任意一条路径上的排列为 r . . . p 1... p 2...... p k . . . x r...p1...p2......pk...x r...p1...p2......pk...x
支配树能够对每个点x ( x ≠ r ) (x \neq r) (x=r)求出 p k pk pk,简单的讲就是对每个点x,求出支配他的所有点中离他最近的点idom[x]
如果每个点x连出指向idom[x],那么就构成了一棵内向树,我们称他为支配树

支配树有很多食用方法,OI中常见的似乎是求出idom[x]后利用性质什么的做dp或者做一些判断性问题

然后关于支配树的建树,有一种比较简单的O((n+m)n)做法, 就是枚举删掉每个点p,然后从起点r遍历一次图G,求出不能到哪些点q,那么p就支配了q
但是tarjan发明了一种更高效的算法Lengauer-Tarjan,他能够在 O ( n α ( n ) ) O(n\alpha(n)) O(nα(n))(emmmmmm好像可以被卡到logn,但他好像又介绍了另一种不会被卡,能取到 α \alpha α的方法当然啦我不会qaq)

接下来讲一下这个算法
先遍历一遍图G,求出每个点的dfs序,下文为了方便默认每个点p的dfs序dfsi[p]=p,下文中节点编号的比较指的都是dfs序编号
为了求idom[x],我们要定义一个辅助的东西sdom[x] ( s e m i − d o m i n a t o r semi-dominator semidominator),他的定义是dfs序最小的,满足p到x存在一条路径 P = p , a 1 , a 2...... a k , x   其中 ∀ a i > x , k > = 0 P=p,a1,a2......ak,x\ \ 其中\forall a_i>x,k>=0 P=p,a1,a2......ak,x  其中ai>xk>=0 的点p,这个东西可以理解为绕了一条不那么直接的路,他往往形如这样
这里写图片描述
图中u是sdom[x],标红的路径是”直接的路径“也就是搜索树上直接从u走到x的路径,标绿的路径即上述的路径 P P P,也就是不那么直接的路径,他往往是像这样从u走了一些树边(或者前向边),走一条横叉边,再走一些返祖边(后向边)到达x,所经过的点dfs序都比x大(除起点终点外)

为什么要求这个东西?因为有一些关于这个的结论(可以自己yy或者画个图意会一下,严谨的证明我不太会)
(下文中点x指的都是除了起点r之外的点,祖先指的都是搜索树上的祖先)
结论1:sdom[x]一定是x的祖先
结论2:idom[x]是sdom[x]或者是sdom[x]的祖先
结论3:sdom[x]< x

这两个结论说明了sdom[x]有潜力成为idom[x]
然后下面的两个结论说明了怎么求sdom[x]和idom[x](结论5我连不严谨的证明都不太会qaq)
结论4:
s d o m [ x ] = m i n { { v ∣ v 是 x 的父亲 } ⋁ { s d o m [ p ]   ∣   p > x , v > x , ( v , x ) ∈ G , p 是 v 的祖先 } } sdom[x]=min\{ \{v|v是x的父亲\} \bigvee \{ sdom[p]\ |\ p>x,v>x,(v,x)\in G,p是v的祖先\} \} sdom[x]=min{{vvx的父亲}{sdom[p]  p>x,v>x,(v,x)Gpv的祖先}}
结论5:
令 𝑃 为搜索树上,𝑠𝑑𝑜𝑚[𝑥]到𝑥的路径上,除了𝑠𝑑𝑜𝑚[𝑥]之外的所有点的点集
找到𝑢∈𝑃, 且 𝑠𝑑𝑜𝑚[𝑢] 是 𝑃 中所有点𝑠𝑑𝑜𝑚 的最小值
s d o m [ u ] = s d o m [ x ] sdom[u]=sdom[x] sdom[u]=sdom[x],则 i d o m [ x ] = s d o m [ x ] idom[x]=sdom[x] idom[x]=sdom[x]
否则 i d o m [ x ] = i d o m [ u ] idom[x]=idom[u] idom[x]=idom[u]

有了这些结论我们就可以开始Lengauer-Tarjan算法了
要维护的东西
par[x],x的父亲
pred[x],有边连向x的点集
V[x],以x为sdom的点集
要资瓷2种操作
link(x,y),将y接到x的子树下
query(x),询问x到其所在树根路径上sdom的最小值,和拥有这个最小值的点
这两种操作可以用带权并查集做到复杂度均摊 O ( α ( n ) ) O(\alpha(n)) O(α(n))(这里不讨论出题人恶意卡并查集qaq)

具体实现:
(1)为了方便可以先将每个点x的idom和sdom初始化为x
(2)先dfs一遍原图,求出每个点的dfs序

然后按dfs序从大到小处理每个点x,
3)将x的pred里面的每个点y取出来,query(y),并尝试用y到根的sdom最小值去更新sdom[x](这样做在y=par[x]时还y< x,未访问y,y所在的树只有自己,返回的最小值就是y,如果y>x,y已经访问过,按照下文(5)的link方式,这2种情况就是结论4)
(4)接着取出x的V[x],因为按dfs序从大到小处理点,此时x的子树中所有点都已经处理完,对于以x为sdom的每个点y,按照(5)的link方式,y到x的树边路径上符合结论5的点v在并查集中都成为了y的祖先,query(y)之后我们能找到u,按照结论5,若sdom[u]=x,idom[y]=x,否则idom[y]=idom[u],此时我们并不知道idom[u]是几,实现上我们可以先令idom[y]=u,在(7)中求出idom[y]
(5)此时已经处理完了点x的信息,可以将x在并查集中与其孩子合并了,枚举x的出边(x,y),若par[y]=x,则这条是树边,link(x,y)
(6)把x加入到sdom[x]的集合V中

(7)按dfs序从大到小处理完后,我们再从小到大,对于每个点x,若idom[x]!=sdom[x],那么此时的idom[x]就是(4)中标记的u,就令idom[x]=idom[idom[x]]
然后我们就建出dominator tree啦

支配树的题要特别注意多组数据的清空和图不连通的情况qaq

然后是几道例题

hdu4694,建出支配树后做个简单的dp,注意图有不连通的情况
code:

#include<set>
#include<map>
#include<deque>
#include<queue>
#include<stack>
#include<cmath>
#include<ctime>
#include<bitset>
#include<string>
#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<complex>
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;

const int maxn = 51000;
const int maxm = 110000;

int n,m;
int sdom[maxn],idom[maxn];
struct edge{int y,nex;}a[maxm]; int len,fir[maxn];
inline void ins(const int x,const int y){a[++len]=(edge){y,fir[x]};fir[x]=len;}

int par[maxn],dfn[maxn],To[maxn],id;
void build(const int x)
{
    To[dfn[x]=++id]=x;
    for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(!dfn[y])
        par[y]=x,build(y);
}

int fa[maxn],fas[maxn];
void findfa(const int x)
{
    if(fa[x]==x) return;
    findfa(fa[x]);
    if(dfn[sdom[fas[fa[x]]]]<dfn[sdom[fas[x]]]) fas[x]=fas[fa[x]];
    fa[x]=fa[fa[x]];
}
vector<int>pred[maxn],V[maxn];

ll ans[maxn];

int main()
{
    //freopen("tmp.in","r",stdin);
    //freopen("tmp.out","w",stdout);
    
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        len=id=0; for(int i=1;i<=n;i++) fir[i]=0,dfn[i]=0,par[i]=0;
        for(int i=1;i<=n;i++) pred[i].clear(),V[i].clear();
        
        for(int i=1;i<=m;i++)
        {
            int x,y; scanf("%d%d",&x,&y);
            ins(x,y); pred[y].push_back(x);
        }
        build(n);
        for(int i=1;i<=n;i++) fa[i]=i,fas[i]=sdom[i]=i,idom[i]=i;
        for(int i=id;i>=1;i--)
        {
            int x=To[i],&semi=sdom[x];
            for(int j=0;j<pred[x].size();j++)
            {
                int y=pred[x][j]; if(!dfn[y]) continue;
                findfa(y);
                if(dfn[sdom[fas[y]]]<dfn[semi]) semi=sdom[fas[y]];
            }
            for(int j=0;j<V[x].size();j++)
            {
                int y=V[x][j]; findfa(y);
                if(dfn[sdom[fas[y]]]<i) idom[y]=fas[y];
                else idom[y]=x;
            }
            for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(par[y]==x)
                fa[y]=x;
            V[semi].push_back(x);
        }
        for(int i=1;i<=id;i++)
        {
            int x=To[i];
            if(idom[x]!=sdom[x]) idom[x]=idom[idom[x]];
        }
        
        for(int i=1;i<=id;i++) ans[To[i]]=ans[idom[To[i]]]+To[i];
        for(int i=1;i<=n;i++) if(!dfn[i]) ans[i]=0;
        for(int i=1;i<n;i++) printf("%lld ",ans[i]),ans[i]=0;
        printf("%lld\n",ans[n]),ans[n]=0;
    }
    
    return 0;
}

BZOJ2815: [ZJOI2012]灾难
其实就是求支配树的子树节点数
新建一个点作为起点,连向所有生产者,建支配树
这是个DAG,所以也可以不用这个算法直接跑每个点pred里面的点在支配树上的LCA

code:

#include<set>
#include<map>
#include<deque>
#include<queue>
#include<stack>
#include<cmath>
#include<ctime>
#include<bitset>
#include<string>
#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<complex>
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;
 
inline void read(int &x)
{
    char c; while(!((c=getchar())>='0'&&c<='9'));
    x=c-'0';
    while((c=getchar())>='0'&&c<='9') (x*=10)+=c-'0';
}
const int maxn = 110000;
const int maxm = 2100000;
 
int n;
int sdom[maxn],idom[maxn];
vector<int>V[maxn];
struct edge{int y,nex;}a[maxm]; int len,fir[maxn];
inline void ins(const int x,const int y){a[++len]=(edge){y,fir[x]};fir[x]=len;}
vector<int>pred[maxn];
 
int dfn[maxn],To[maxn],id,par[maxn];
void build(const int x)
{
    To[dfn[x]=++id]=x;
    for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(!dfn[y])
        par[y]=x,build(y);
}
 
int fa[maxn],fas[maxn];
void findfa(const int x)
{
    if(fa[x]==x) return;
    findfa(fa[x]);
    if(dfn[sdom[fas[fa[x]]]]<dfn[sdom[fas[x]]]) fas[x]=fas[fa[x]];
    fa[x]=fa[fa[x]];
}
int ans[maxn];
 
int main()
{
    //freopen("tmp.in","r",stdin);
    //freopen("tmp.out","w",stdout);
     
    read(n);
    for(int i=1;i<=n;i++)
    {
        int x,t=0;
        while(1)
        {
            read(x); if(!x) break;
            ++t; 
            ins(x,i),pred[i].push_back(x);
        }
        if(!t) ins(n+1,i),pred[i].push_back(n+1);
    }
    build(n+1);
    for(int i=1;i<=n+1;i++) fa[i]=i,fas[i]=i,sdom[i]=idom[i]=i;
    for(int i=id;i>=1;i--)
    {
        int x=To[i],&semi=sdom[x];
        for(int j=0;j<pred[x].size();j++)
        {
            int y=pred[x][j]; if(!dfn[y]) continue;
            findfa(y);
            if(dfn[semi]>dfn[sdom[fas[y]]]) semi=sdom[fas[y]];
        }
        for(int j=0;j<V[x].size();j++)
        {
            int y=V[x][j]; findfa(y);
            if(dfn[sdom[fas[y]]]<i) idom[y]=fas[y];
            else idom[y]=x;
        }
        V[semi].push_back(x);
        for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(par[y]==x) 
            fa[y]=x;
    }
    for(int i=1;i<=id;i++) 
    {
        int x=To[i];
        if(idom[x]!=sdom[x]) idom[x]=idom[idom[x]];
    }
    for(int i=id;i>1;i--)
    {
        int x=To[i];
        ans[idom[x]]+=ans[x]+1;
    }
    for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
     
    return 0;
}

BZOJ3281: 小P的烦恼
“为了方便”我把边看作成一个点,边权附到了点权上,然后必经边也可以看作支配点(事实证明我这样把这个问题变复杂了)
建出支配树后,把出发点到终点路径上的支配点取出来做个前缀后缀dp,因为必经点必经的性质,相邻必经点之间的最短距离可以直接从起点跑一边dijskral后用distance[b]-distance[a]算得,如果是直接做这个问题,dp部分应该挺简单的,但是我把边拆成点后还要处理一个点不完全被绳覆盖的情况,还有两条绳拼在一个点上的情况…完全变复杂了qwq
同样的注意图不连通的情况
(还有为啥我的程序跑的这么慢…)

code:

#include<set>
#include<map>
#include<deque>
#include<queue>
#include<stack>
#include<cmath>
#include<ctime>
#include<bitset>
#include<string>
#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<complex>
#include<iostream>
#include<algorithm>
#define ll long long
#define inf 1e9
using namespace std;
 
inline void read(int &x)
{
    char c; while(!((c=getchar())>='0'&&c<='9'));
    x=c-'0';
    while((c=getchar())>='0'&&c<='9') (x*=10)+=c-'0';
}
const int maxn = 310000;
const int maxm = 410000;
 
int n,m,N,S,T,l;
int val[maxn];
struct edge{int y,nex;}a[maxm]; int len,fir[maxn];
inline void ins(const int x,const int y){a[++len]=(edge){y,fir[x]};fir[x]=len;}
vector<int>pred[maxn];
 
struct node
{
    int x,i;
    friend inline bool operator <(const node x,const node y){return x.x>y.x;}
};
priority_queue<node>q;
int dis[maxn];
void dij()
{
    for(int i=1;i<=N;i++) dis[i]=inf;
    dis[S]=0; q.push((node){0,S});
    while(!q.empty())
    {
        const node now=q.top(); q.pop();
        int x=now.i;
        for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(dis[y]==inf)
            dis[y]=dis[x]+val[y],q.push((node){dis[y],y});
    }
}
 
int dfn[maxn],To[maxn],id,par[maxn];
void build(const int x)
{
    To[dfn[x]=++id]=x;
    for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(!dfn[y])
        par[y]=x,build(y);
}
int idom[maxn],sdom[maxn];
vector<int>V[maxn];
int fa[maxn],fas[maxn];
void findfa(const int x)
{
    if(fa[x]==x) return;
    findfa(fa[x]);
    if(dfn[sdom[fas[fa[x]]]]<dfn[sdom[fas[x]]]) fas[x]=fas[fa[x]];
    fa[x]=fa[fa[x]];
}
 
int t[maxn],tp;
int f[maxn],g[maxn];
 
int main()
{
    //freopen("tmp.in","r",stdin);
    //freopen("tmp.out","w",stdout);
     
    int tcase; scanf("%d",&tcase);
    while(tcase--)
    {
        read(n),read(m),read(S),read(T),read(l); S++,T++; N=n+m;
        len=id=0; for(int i=1;i<=N;i++) fir[i]=dfn[i]=par[i]=val[i]=0;
        for(int i=1;i<=N;i++) pred[i].clear(),V[i].clear();
         
        for(int i=1;i<=m;i++)
        {
            int x,y,c; read(x),read(y),read(c);
            x++,y++;
            val[n+i]=c;
            ins(x,n+i),pred[n+i].push_back(x);
            ins(n+i,y),pred[y].push_back(n+i);
        }
        dij();
        if(dis[T]==inf) { puts("-1");continue; }
        build(S);
        for(int i=1;i<=N;i++) fa[i]=fas[i]=idom[i]=sdom[i]=i;
        for(int i=id;i>=1;i--)
        {
            int x=To[i],&semi=sdom[x];
            for(int j=0;j<pred[x].size();j++)
            {
                int y=pred[x][j]; if(!dfn[y]) continue;
                findfa(y);
                if(dfn[sdom[fas[y]]]<dfn[semi]) semi=sdom[fas[y]];
            }
            for(int j=0;j<V[x].size();j++)
            {
                int y=V[x][j]; findfa(y);
                if(dfn[sdom[fas[y]]]<i) idom[y]=fas[y];
                else idom[y]=x;
            }
            V[semi].push_back(x);
            for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(par[y]==x)
                fa[y]=x;
        }
        for(int i=2;i<=id;i++)
        {
            int x=To[i];
            if(idom[x]!=sdom[x]) idom[x]=idom[idom[x]];
        }
        tp=0; int tx=T;
        while(t[tp]!=S) t[++tp]=tx,tx=idom[tx];
        t[tp+1]=0;
        int L,R,temp=0; L=1;
        for(R=1;R<=tp;R++)
        {
            temp+=L!=R?val[t[R]]:0;
            while(dis[t[L]]-dis[t[R]]+val[t[R]]-val[t[L]]>l) temp-=val[t[++L]];
            int cc=temp+min(l-(dis[t[L]]-dis[t[R]]+val[t[R]]-val[t[L]]),val[t[L]]);
            f[R]=max(f[R-1],cc);
        }
        g[tp+1]=0; R=tp; temp=0;
        for(L=tp;L>=1;L--)
        {
            temp+=L!=R?val[t[L]]:0;
            while(dis[t[L]]-dis[t[R]]>l) temp-=val[t[--R]];
            int cc=temp+min(l-(dis[t[L]]-dis[t[R]]),val[t[R]]);
            g[L]=max(g[L+1],cc);
        }
         
        int ans=0;
        for(int i=1;i<=tp;i++) ans=max(ans,f[i-1]+g[i]);
        l=2*l;
        g[tp+1]=0; R=tp; temp=0;
        for(L=tp;L>=1;L--)
        {
            temp+=L!=R?val[t[L]]:0;
            while(dis[t[L]]-dis[t[R]]>l) temp-=val[t[--R]];
            int cc=temp+min(l-(dis[t[L]]-dis[t[R]]),val[t[R]]);
            g[L]=max(g[L+1],cc);
        }
        ans=max(ans,g[1]);
        ans=-ans;for(int i=1;i<=tp;i++) ans+=val[t[i]];
        printf("%d\n",ans);
    }
     
    return 0;
}

2014-2015 ACM-ICPC, NEERC, Southern Subregional Contest Problem L
Link
给定起点后判哪些边可能在一条简单路径上
考虑搜索树中的几类边,树边,前向边,横叉边显然都可以在一条简单路径上,唯一要考虑的就是返祖边(后向边)是否可能在一条简单路径上,对于一条返祖边(u,v),如果v不是u的必经点,就存在一条简单路径否则不存在,证明显然
建出支配树后就是一个判断v是不是u的祖先的问题
还有注意图不连通的情况,以及答案边集为空的话输出的第二行要是一个空行…

code:

#include<set>
#include<map>
#include<deque>
#include<queue>
#include<stack>
#include<cmath>
#include<ctime>
#include<bitset>
#include<string>
#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<complex>
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;

const int maxn = 410000;
const int maxm = 410000;
const int maxd = 20;

int n,m;
bool ve[maxm];
int e[maxm][2];
struct edge{int y,i,nex;}a[maxm]; int len,fir[maxn];
inline void ins(const int x,const int y,const int i){a[++len]=(edge){y,i,fir[x]};fir[x]=len;}
vector<int>pred[maxn];

int dfn[maxn],id,To[maxn],par[maxn];
bool insta[maxn];
void build(const int x)
{
	To[dfn[x]=++id]=x; insta[x]=true;
	for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) 
	{
		if(!dfn[y]) par[y]=x,build(y);
		else if(insta[y]) ve[a[k].i]=true;
	}
	insta[x]=false;
}
vector<int>V[maxn];
int idom[maxn],sdom[maxn];
int fa[maxn],fas[maxn];
void findfa(const int x)
{
	if(fa[x]==x) return;
	findfa(fa[x]);
	if(dfn[sdom[fas[fa[x]]]]<dfn[sdom[fas[x]]]) fas[x]=fas[fa[x]];
	fa[x]=fa[fa[x]];
}

int dep[maxn],top[maxn][maxd];
void dfs(const int x)
{
	for(int i=1;(1<<i)<=dep[x];i++) top[x][i]=top[top[x][i-1]][i-1];
	for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y)
		dep[y]=dep[x]+1,top[y][0]=x,dfs(y);
}

int ans[maxn],an;

int main()
{
	//freopen("tmp.in","r",stdin);
	//freopen("tmp.out","w",stdout);
	
	while(scanf("%d%d",&n,&m)!=EOF)
	{
		len=id=0; for(int i=1;i<=n;i++) fir[i]=dfn[i]=par[i]=0;
		for(int i=1;i<=n;i++) pred[i].clear(),V[i].clear();
		
		for(int i=1;i<=m;i++)
		{
			ve[i]=false;
			int x,y; scanf("%d%d",&x,&y);
			e[i][0]=x,e[i][1]=y;
			ins(x,y,i),pred[y].push_back(x);
		}
		build(1);
		for(int i=1;i<=n;i++) fa[i]=fas[i]=idom[i]=sdom[i]=i;
		for(int i=id;i>=1;i--)
		{
			int x=To[i],&semi=sdom[x];
			for(int j=0;j<pred[x].size();j++)
			{
				int y=pred[x][j]; if(!dfn[y]) continue;
				findfa(y);
				if(dfn[sdom[fas[y]]]<dfn[semi]) semi=sdom[fas[y]];
			}
			for(int j=0;j<V[x].size();j++)
			{
				int y=V[x][j]; findfa(y);
				if(dfn[sdom[fas[y]]]<i) idom[y]=fas[y];
				else idom[y]=x;
			}
			V[semi].push_back(x);
			for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(par[y]==x)
				fa[y]=x;
		}
		len=0;for(int i=1;i<=n;i++) fir[i]=0;
		for(int i=2;i<=id;i++)
		{
			int x=To[i];
			if(idom[x]!=sdom[x]) idom[x]=idom[idom[x]];
			ins(idom[x],x,0);
		}
		dep[1]=1; dfs(1);
		
		an=0; for(int i=1;i<=m;i++)
		{
			if(!dfn[e[i][0]]) continue;
			if(!ve[i]) ans[++an]=i;
			else
			{
				int x=e[i][0],y=e[i][1];
				for(int j=maxd-1;j>=0;j--) if(dep[x]-dep[y]>=(1<<j))
					x=top[x][j];
				if(x!=y) ans[++an]=i;
			}
		}
		printf("%d\n",an);
		for(int i=1;i<an;i++) printf("%d ",ans[i]);
		if(an) printf("%d",ans[an]);
		puts("");
	}
	
	return 0;
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值