状压DP学习小结

状压DP,即通过二进制位运算将状态压缩(用整数表示集合)作为动态规划的状态来解决问题的办法


例题1 n个点的有向图,给出距离的邻接矩阵,求经过每个点一次的最短路径。n<=20,256MB

题解

解法1:
n!枚举路径
解法2:
状压dp
将已访问过的点和当前访问的点为状态进行dp

dpS,v=dp(S,u)+dist(u,v)(SvS|v=S) d p ( S ′ , v ) = d p ( S , u ) + d i s t ( u , v ) ( S 不 含 v 且 S | v = S ′ )

下面是记忆化搜索和递推的两种实现

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=21,M=1<<21;
const int INF=1<<30;
int dist[N][N];
int d[M][N];
int n;
int dp(int S,int u){
    if(d[S][u]!=-1)
        return d[S][u];
    if(S==(1<<n)-1)
        return 0;
    d[S][u]=INF;
    for(int v=0;v<n;v++)
        if(dist[u][v]!=-1&&!((1<<v)&S)){
            d[S][u]=min(d[S][u],dp(S|1<<v,v)+dist[u][v]);
        }
    return d[S][u];
}
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
            scanf("%d",&dist[i][j]);
    int ans=INF;
    memset(d,-1,sizeof d);
    for(int i=0;i<n;i++){
        ans=min(ans,dp(1<<i,i));
    }
    printf("%d",ans);
}
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=21,M=1<<21;
const int INF=1<<30;
int d[M][N];
int n,map1[N][N],ans,s;
int main()
{
    scanf("%d",&n);
    ans=0x3f3f3f3f;
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
            scanf("%d",&map1[i][j]);
    memset(d,0x7f,sizeof d);
    for(int i=0;i<n;i++)
        d[1<<i][i]=0;
    for(int s=1;s<=(1<<n)-1;s++)
        for(int j=0;j<n;j++)
            for(int k=0;k<n;k++)
                if((s&(1<<j))==(1<<j))
                    d[s][j]=min(d[s][j],d[s-(1<<j)][k]+map1[k][j]);
        for(int j=0;j<n;j++)
            ans=min(ans,d[(1<<n)-1][j]);
    printf("%d",ans);
}

例题2 POJ2686 Travelling by Stagecoach

题目大意:给出一些城市和连接它们的双向边,无重边,无自环,通过道路需要车票,你有n张车票,每张车票都有一个值,只能使用一次,通过i号道路使用j号车票所用时间是di/tj。询问从城市a到b的最短时间,无解输出Impossible。n<=10,点数<=30,边数<=2000

题解

定义状态为:剩余车票集合为S,现在在城市u
d[S][v]=d[S][u]+dist[u][v]/h[i](i|S=S) d [ S ′ ] [ v ] = d [ S ] [ u ] + d i s t [ u ] [ v ] / h [ i ] ( i | S ′ = S )
下面给出递推的实现

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=35,M=2005,T=10;
const int INF=1<<30;
struct node{
    int v,w,nxt;
}edge[M];
int head[N],mcnt;
void add_edge(int u,int v,int w){
    mcnt++;
    edge[mcnt].v=v;
    edge[mcnt].w=w;
    edge[mcnt].nxt=head[u];
    head[u]=mcnt;
}
double d[1<<T][N];
int t[T];
int n,m,p;
int from,to;
int main()
{
    while(1){
        scanf("%d%d%d%d%d",&p,&n,&m,&from,&to);
        if(n==0&&m==0)
            return 0;
        from--,to--;
        for(int i=0;i<p;i++)
            scanf("%d",&t[i]);
        memset(head,0,sizeof head);
        mcnt=0;
        for(int i=0;i<m;i++){
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            add_edge(u-1,v-1,w);
            add_edge(v-1,u-1,w);
        }
        for(int i=0;i<1<<p;i++)
            for(int j=0;j<n;j++)
                d[i][j]=INF;
        d[(1<<p)-1][from]=0;
        double ans=INF;
        for(int S=(1<<p)-1;S>=0;S--){
            ans=min(ans,d[S][to]);
            for(int u=0;u<n;u++)
                for(int i=head[u];i;i=edge[i].nxt){
                    int v=edge[i].v,w=edge[i].w;
                    for(int j=0;j<p;j++)
                        if((1<<j)&S){
                            d[S-(1<<j)][v]=min(d[S-(1<<j)][v],d[S][u]+w*1.0/t[j]);
                        }
                }
        }
        if(ans==INF)
            printf("Impossible\n");
        else
            printf("%lf\n",ans);
    }
}

例题3 给出一个N*M大的格子,染成了黑白两色,用1×2的砖块去覆盖,使得所有白色都被覆盖,黑色都不被覆盖,求方案数(模某个数)

N,M<=15

题解

其实呢,如果只要求求出一种方案,我们可以跑网络流
由于是1×2的砖块,所以其实我们只需要知道本行和上行的信息即可,所以状态压缩dp+滚动数组即可(直接开是开不下的)


习题1 POJ 2441 Arrange the Bulls

题意:
Farmer John 有N头牛,M个篮球场,每头牛有若干个想去的篮球场,一个篮球场只能容纳一头牛(话说一头牛怎么打篮球),求满足所有牛需求的分配方案的个数,保证不超过10000000
N,M<=20,4s,64MB

题解

dp(S,i)表示分配前i头牛,剩余球场集合为S的方案数
转移方程呢一是比较显然,二是表述起来很麻烦,就不写了
直接开是开不下的,需要滚动数组
代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=21,M=1005,T=21;
const int INF=1<<30;
struct node{
    int v,nxt;
}edge[M];
int head[N],mcnt;
void add_edge(int u,int v){
    mcnt++;
    edge[mcnt].v=v;
    edge[mcnt].nxt=head[u];
    head[u]=mcnt;
}
int d[2][1<<T];
int t[T];
int n,m;
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        int p;
        scanf("%d",&p);
        for(int j=1;j<=p;j++){
            int num;
            scanf("%d",&num);
            num--;
            add_edge(i,num);
        }
    }
    int ans=0;
    int *now=d[0],*nxt=d[1];
    for(int i=head[1];i;i=edge[i].nxt){
        int v=edge[i].v;
        now[((1<<m)-1)-(1<<v)]=1;
    }
    for(int u=2;u<=n;u++){
        for(int S=1;S<=(1<<m)-1;S++)
            if(now[S])
                for(int i=head[u];i;i=edge[i].nxt){
                    int v=edge[i].v;
                    if((1<<v)&S){
                        nxt[S-(1<<v)]+=now[S];
                    }
                }
        swap(nxt,now);
        for(int S=0;S<(1<<m);S++)
            nxt[S]=0;
    }
    for(int S=0;S<(1<<m);S++)
        ans+=now[S];
    printf("%d\n",ans);
}

习题2 POJ 3254 Corn Fields

Farmer John 有n*m块地,其中一些地荒掉了不能种玉米。玉米是一种傲娇的植物,种在相邻的地里会导致不孕不育。求所有种法数对模100000000。
n,m<=12

题解

基本上就是和例题3是一样的,数据范围略小一点,这次就可以不用滚动数组了(你要用没人拦你)

#include<cstdio>
long long map[15][15];
long long d[15][10000];
long long mod=100000000;
long long n,m;
void dp(long long line,long long x,long long z,long long last,long long lastd){
    if(x==m+1){
        d[line][z]=(d[line][z]+lastd)%mod;
        return ;
    }
    dp(line,x+1,z*2,last,lastd);
    if(!(z&1)&&map[line][x]&&!(last&(1<<(m-x)))){
        dp(line,x+1,z*2+1,last,lastd);
    }
}
int main()
{
    scanf("%lld%lld",&n,&m);
    for(long long i=1;i<=n;i++)
        for(long long j=1;j<=m;j++)
            scanf("%lld",&map[i][j]);
    dp(1,1,0,0,1);
    for(long long i=2;i<=n;i++){
        for(long long j=0;j<=(1<<m)-1;j++){
            if(!d[i-1][j])
                continue ;
            dp(i,1,0,j,d[i-1][j]);
        }
    }
    long long ans=0;
    for(long long i=0;i<=(1<<m)-1;i++){
        ans=(ans+d[n][i])%mod;
    }
    printf("%lld",ans);
}

习题3 2836 Rectangular Covering

题意:平面上有N个点,现在要用一些矩形(各边都在格线上)来覆盖它们,每个矩形至少覆盖2个点,求最少的矩形总面积
n<=15

题解

预处理将n个点两两组合形成n * (n-1) / 2个矩形
d(S)表示点集为S时的最小面积

d(S|c[i])=d(S)+m[i] d ( S | c [ i ] ) = d ( S ) + m [ i ]

c[i]为矩形内的点,m[i]为矩形面积


习题4 POJ 1795 DNA Laboratory

题意 :给出N个字符串,寻找一个字符串使得这N个字符串都是它的子串。输出最短的满足要求的串,如果有多个最短,输出字典序最小的
len<=100
n<=15
5s

题解

预处理把j号串拼在i号串前面所增加的长度
(如aab拼在bca前增加长度为2)
d(S,i)表示目前答案串包含集合为S,最前面的是i号串

d(S+j,j)=d(S,i)+cost[i][j] d ( S + j , j ) = d ( S , i ) + c o s t [ i ] [ j ]


习题5 POJ 3411 Paid Roads

题意:N个城市间有m条单向路,分别从a到b,可以在c处交P路费(只能预交,不能赊账),也可以直接交R路费。无法到达输出impossible
n<=10

题解

d(u,S)表示走到u节点,已经走过的点集为S

d(v,S|(1<<v))=d(u,S)+cost d ( v , S | ( 1 << v ) ) = d ( u , S ) + c o s t

cost=((1<<c)&S)&&(P<R)?P:R;

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
struct node{
    int v,c,cost1,cost2;
}p;
vector<node> edge[20];
int d[20][5000],n;
bool vis[20];
queue<int> q;
void spfa(){
    vis[1]=1;
    memset(d,-1,sizeof d);
    d[1][2]=0;
    q.push(1);
    while(!q.empty()){
        int x=q.front();
        vis[x]=0;
        q.pop();
        for(int i=1;i<=(1<<n);i++){
            if(d[x][i]==-1)
                continue ;
            for(int j=0;j<edge[x].size();j++){
                int v=edge[x][j].v,c=edge[x][j].c,cost1=edge[x][j].cost1,cost2=edge[x][j].cost2;
                int cost=cost2;
                if(i&(1<<c)&&cost2>cost1)
                    cost=cost1;
                if(d[v][i|(1<<v)]==-1||d[x][i]+cost<d[v][i|(1<<v)]){
                    d[v][i|(1<<v)]=d[x][i]+cost;
                    if(!vis[v]){
                    vis[v]=1;
                    q.push(v);}
                }
            }
        }
}
}
int main()
{
    int m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int u;
        scanf("%d",&u);
        scanf("%d%d%d%d",&p.v,&p.c,&p.cost1,&p.cost2);
        edge[u].push_back(p);
    }
    spfa();
    int ans=1<<30;
    for(int i=1;i<=(1<<(n+1));i++){
        if(d[n][i]!=-1&&d[n][i]<ans)
            ans=d[n][i];
    }
    if(ans==1<<30)
        printf("impossible");
    else
        printf("%d",ans);
}

习题6 treasure

参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋, 也给出了这 n 个宝藏屋之间可供开发的 m 条道路和它们的长度。

小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远, 也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路 则相对容易很多。

小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某 个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。

在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路 所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏 屋之间的道路无需再开发。
新开发一条道路的代价是: L*K
L代表这条道路的长度,K代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的 宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋)
请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代价最小,并输出这个最小值
对于 20% 的数据: 保证输入是一棵树,1≤n≤8 , v≤5000 且所有的 v 都相等。
对于 40% 的数据: 1≤n≤8,0≤m≤1000 , v≤5000 且所有的 v 都相等。
对于 70% 的数据: 1≤n≤8,0≤m≤1000,v≤5000
对于 100% 的数据: 1≤n≤12,0≤m≤1000 , v≤500000

题解

首先,很容易发现打通后的道路一定是一棵树,并且,若以起点为根并令其深度为0,则题目中的 K 即为这条路所连向的点的深度。

状态:dp[i][S] 表示考虑到树的第i层,前i层已选的点的集合为S(二进制状压)的最小代价。:
转移:已知dp[i][S]时,可枚举所有由不在S中的点构成的集合作为第i+1层,则状态转移为
dp[i][S]→dp[i+1][S|S′]+=(i+1)×Σ min{G[a][b]|a∈S,b∈S′,S∩S′=∅}

简单一点,就是
dp[i][S]→dp[i+1][S|S′]+=(i+1)×sval[S′][S]}

其中sval[A][B]表示集合A到集合B的最短距离,即集合A中所有点到集合B的最短距离之和。可以先预处理出每个点到每个集合的最短距离pval[i][S](也就是点i到集合S中所有点的距离的最小值),然后用pval[i][B]更新sval[A][B]。
边界条件:枚举根节点,设为root,则dp[0][1<<(root−1)]=0
此部分来源:http://blog.youkuaiyun.com/phantomagony/article/details/78702573

<think>我们首先需要理解用户的需:用户希望了解缩动态规划(DP)的详细原理和应用。根据引用资料,我们可以总结出DP的核心思想、适用条件以及基本实现方法。 ### 缩动态规划(DP)详解 缩动态规划是一种通过将态信息缩(通常使用二进制)来表示动态规划态的方法,适用于态中每个元素只有两种可能(如存在/不存在、选中/未选中)且态规模较小(通常不超过32个元素)的问题。 #### 1. 适用条件 - **态单元简单**:每个态单元只有两种态(例如0/1)。 - **态规模小**:态单元的数量不能太大(通常不超过32,因为一个32位整数可以表示32个态单元)。 - **需要态**:如果不缩,态数量会非常大(例如$2^n$级别),导致无法存储。 #### 2. 基本原理 DP的核心在于用一个整数的二进制位来表示一个集合。例如,一个长度为$n$的二进制数,每一位(0或1)表示某个元素的态(如是否被选中)。这样,一个整数就代表了一个集合。 例如,有5个元素,用5位二进制数表示: - 二进制数`10101`(十进制21)表示第0、2、4个元素被选中(从右向左,最低位为第0位)。 - 这样,我们就可以用一个整数(如21)来代表一个集合。 #### 3. 位运算基础 DP中常用的位运算操作: - **左移(<<)**:`1 << i` 表示将1左移i位,得到第i位为1,其余为0的数。常用于构造某个态。 - **与(&)**:判断某一位是否为1,例如`state & (1<<i)`,如果结果非0,则第i位为1。 - **或(|)**:将某一位设为1,例如`state | (1<<i)`。 - **异或(^)**:将某一位取反,例如`state ^ (1<<i)`。 - **取反(~)**:将每一位取反,例如`~state`(注意整数位数,避免高位干扰)。 #### 4. DP的步骤 DP的解题步骤与普通DP类似,但态表示使用了缩的整数: 1. **定义态**:通常用$dp[i][state]$表示处理到第$i$个阶段(或位置)且当前态为$state$(一个缩的整数)时的最优解(或方案数)。 2. **态转移**:根据问题的规则,从之前的态转移到当前态。通常需要枚举所有可能的前一个态,检查是否满足转移条件。 3. **初始化**:确定初始态(如$dp[0][0]=0$)。 4. **结果提取**:根据最终态(如$dp[n][final\_state]$)得到答案。 #### 5. 经典问题举例:旅行商问题(TSP) 问题描述:有$n$个城市,旅行商从起点0出发,访问每个城市恰好一次回到起点最短路径。 - **态表示**:$dp[state][i]$表示已经访问过的城市集合为$state$,当前位于城市$i$的最短路径长度。 - **态转移**:$dp[state][i] = \min\{ dp[state \setminus \{i\}][j] + dist[j][i] \}$,其中$j$是上一个访问的城市,且$j$在$state$中,$state \setminus \{i\}$表示从集合$state$中移除城市$i$(即态中第$i$位置0)。 - **缩**:$state$是一个$n$位的二进制数,第$k$位为1表示城市$k$已经访问过。 #### 6. 代码示例(TSP问题) ```python def tsp(dist): n = len(dist) # 态总数:2^n total_states = 1 << n # 初始化dp数组:dp[state][i]表示在态state下,当前在i城市的最短路径 dp = [[float('inf')] * n for _ in range(total_states)] # 起点态1<<0表示只包含0号城市,当前在0城市,路径长度为0 dp[1][0] = 0 # 遍历所有态 for state in range(total_states): # 遍历当前态下的每个城市 for i in range(n): # 如果当前态中不包含i,跳过 if not (state & (1 << i)): continue # 尝试从其他城市j转移到i for j in range(n): # 如果态中包含j,且j不是i,则尝试转移 if i != j and (state & (1 << j)): # 从态state中移除i,得到前一个态prev_state prev_state = state ^ (1 << i) # 更新dp[state][i] dp[state][i] = min(dp[state][i], dp[prev_state][j] + dist[j][i]) # 最终态:所有城市都访问过(全1),并且回到起点0 res = float('inf') for i in range(1, n): # 最后一步从i回到0 res = min(res, dp[total_states-1][i] + dist[i][0]) return res ``` #### 7. 优化与技巧 - **滚动数组**:当态转移只依赖前一阶段时,可优化空间。 - **预处理合法态**:对于某些问题,可以先预处理出所有合法态(如不冲突的态),减少枚举量。 - **剪枝**:在态转移过程中,通过条件判断提前终止无效转移。 #### 8. 应用场景 DP常用于组合优化问题,如: - 旅行商问题(TSP) - 铺砖问题(用1x2砖块铺满网格) - 任务分配问题(每个任务分配给不同的人,每人只能做一个) - 棋盘覆盖问题(如炮兵布阵) ### 总结 缩动态规划通过将缩为整数,有效解决了态空间过大问题。其核心在于位运算的应用和态转移的设计。掌握DP需要熟悉位运算操作,并能够将问题态抽象为二进制表示。 §§相关问题§§ 1. DP中常用的位运算操作有哪些? 2. 如何判断一个态是否包含某个元素? 3. 旅行商问题(TSP)的态转移方程是什么? 4. DP适用于哪些类型的问题? 5. 在缩中,如何表示一个集合的并集、交集和差集?</think>### 缩动态规划(DP)详解 #### 一、核心概念 DP是一种将**集合缩为整数**的动态规划技术。它适用于态中的每个元素只有两种可能(如存在/不存在、选中/未选中),且态规模较小(通常不超过32个元素)的问题。核心思想是将一个集合映射为一个整数的二进制位: - **二进制表示**:用整数的二进制位表示集合元素态 例如:集合 $\{A,C\}$ 在3元素系统中可表示为二进制 `101`(十进制5) - **缩**:将多维缩为单整数 态 $dp[i][S]$ → $dp[S]$,其中 $S$ 是缩后的整数态 #### 二、适用条件(引用[3]) 1. **态单元二元化**:每个态单元仅需两种态(0/1) 2. **态规模小**:态单元数 $n \leq 32$(int存储上限) 3. **态可缩**:态数据能映射为二进制整数 4. **态依赖复杂**:多个元素态共同影响决策(引用[2]) #### 三、位运算基础(关键操作) | 操作 | 符号 | 示例(二进制) | 作用 | |---------------|------|----------------------------|--------------------------| | 判断元素存在 | `&` | `state & (1<<i)` | 检查第 $i$ 位是否为1 | | 添加元素 | `\|` | `state \| (1<<i)` | 将第 $i$ 位置1 | | 删除元素 | `&~` | `state & ~(1<<i)` | 将第 $i$ 位置0 | | 切换元素态 | `^` | `state ^ (1<<i)` | 翻转第 $i$ 位 | | 枚举子集 | `&` | `sub = (sub-1) & state` | 遍历态的所有子集 | #### 四、算法步骤(以经典问题为例) **问题:旅行商问题(TSP)** 访问所有城市(每个城市仅一次)并返回起点最短路径 1. **态定义** $dp[state][i]$:当前已访问城市集合为 $state$(缩整数),最后位于城市 $i$ 的最短路径 2. **态转移** $$ dp[state][i] = \min_{j \in state} \{ dp[state \setminus \{i\}][j] + dist[j][i] \} $$ 其中 $state \setminus \{i\}$ 用位运算实现:`state ^ (1<<i)` 3. **初始化** - 起点:$dp[1<<0][0] = 0$(从城市0出发) - 其他态初始化为无穷大 4. **结果提取** $$ \min_{i} \{ dp[full\_state][i] + dist[i][0] \} $$ $full\_state = (1<<n)-1$(所有城市已访问) #### 五、代码实现(TSP问题) ```python def tsp(dist): n = len(dist) total_states = 1 << n # 态总数 2^n dp = [[float('inf')] * n for _ in range(total_states)] # 初始化:从0号城市出发 dp[1 << 0][0] = 0 for state in range(total_states): for i in range(n): if not (state & (1 << i)): # 跳过未访问城市 continue for j in range(n): if state & (1 << j): # j必须在已访问集合中 prev_state = state ^ (1 << i) # 移除当前城市 dp[state][i] = min(dp[state][i], dp[prev_state][j] + dist[j][i]) # 返回最终结果(回到起点0) full_state = (1 << n) - 1 return min(dp[full_state][i] + dist[i][0] for i in range(n)) ``` #### 六、典型应用场景 1. **棋盘覆盖问题** - 如炮兵布阵(引用[4]):每个位置放/不放炮兵 → 二进制位表示 - 约束:同行炮兵间距≥2 → 位运算检查 `state & (state>>1)` 或 `state & (state>>2)` 2. **子集选择问题** - 如背包变种:选择物品组合满足特定条件 - 态:$dp[mask]$ 表示选中物品集合为 $mask$ 的最优解 3. **路径覆盖问题** - 如最小权路径覆盖:用二进制表示已覆盖节 #### 七、优化技巧 1. **滚动数组**:空间优化(如 $dp[state]$ 只依赖 $dp[state']$) 2. **预处理合法态**:提前计算满足约束的态集合 3. **剪枝**:跳过无效态(如 `state` 中不包含必要元素时) #### 八、复杂度分析 - **时间复杂度**:$O(2^n \times n^2)$(TSP为例) - **空间复杂度**:$O(2^n \times n)$ 当 $n>20$ 时需谨慎使用(引用[3]) > DP通过将高维缩为整数,显著降低了态空间维度,是解决小规模组合优化问题的利器(引用[1][2])。其核心在于**用位运算实现集合操作**,将复杂的集合关系转化为整数运算。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值