<算法学习>递归与分治

本文介绍递归与分治算法相关知识,阐述二者区别及递归与迭代的区别。还列举多个相关问题,递归问题如汉诺塔、斐波那契数列等;分治问题有二分搜索、棋盘覆盖等,给出问题描述、解决思路及效率分析等。

目录

递归与分治相关知识

递归

分治

递归与分治的区别

递归与迭代的区别

相关问题

递归问题

1、汉诺塔问题

2、斐波那契数列问题

3、求n的阶乘问题

4、整数划分问题

5、全排列问题

分治问题

1、二分搜索问题

2、棋盘覆盖

3、合并排序

4、快速排序

5、最接近点对问题

6、循环赛日程表

 7、找第k小元素问题

 8、Strassen矩阵乘法和大整数乘法


递归与分治相关知识

递归

将问题逐步分解为更小的子问题。直到可以直接解决该子问题。然后将结果逐层返回。

分治

将问题拆解成独立的子问题。分别求解这些子问题,最后将解合并得到原始问题的解。

递归与分治的区别

递归是调用自身解决子问题,是逐层包含的关系。解决完了下层问题的解后逐层返回才可以求上层的问题的解。最终得到原始问题的解。

而分治是拆成独立的不同子问题,解决子问题时不一定要用同样的方法,各个子问题的解也不会互相影响。

递归与迭代的区别

递归是通过不断调用自身解决子问题。递归过程需要递归内容和递归终止条件。递归时需要的栈存储空间取决于递归深度。若递归次数过多,可能会造成栈溢出

而迭代是通过循环解决问题的,不需要专门写递归函数。一般来说效率比较高。但代码复杂度会比递归高。

一般来说递归问题和迭代问题的求解可以相互转化。

相关问题

递归问题

1、汉诺塔问题

问题描述:

相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘(如图1)。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。

问题解决: 

设起始柱子为begin,终止柱子为end,中间暂用的柱子为temp。

#include <iostream>
#include <algorithm>
using namespace std;
int sum=0;
void hano(int n,char begin,char end,char temp)
{
    if(n==1)
    {
        cout<<sum+1<<": ";
        cout<<1<<" from "<<begin<<" to "<<end<<endl;
        sum++;
    }
    else
    {
        hano(n-1,begin,temp,end);
        cout<<sum+1<<": ";
        cout<<n<<" from "<<begin<<" to "<<end<<endl;
        sum++;
        hano(n-1,temp,end,begin);
    }
}

int main()
{
    int n;
    cin>>n;
    getchar();
    hano(n,'a','b','c');
    cout<<"all steps: "<<sum<<endl;
    return 0;
}

测试用例: 

2、斐波那契数列问题

问题一:输入n输出斐波那契数列的第n项。

原始方法:

#include <iostream>
#include <algorithm>
using namespace std;
int feibo(int n)
{
    if(n==1) return 1;
    else if(n==2) return 1;
    else return feibo(n-1)+feibo(n-2);
}
int main()
{
    int n;
    cin>>n;
    cout<<feibo(n)<<endl;
    return 0;
}

测试用例: 

矩阵快速幂方法:

矩阵快速幂是一种高效求解斐波那契数列的方法,可以将时间复杂度降到O(log n)级别。

#include <iostream>
#include <vector>
using namespace std;
 
// 定义矩阵类型
typedef vector<vector<long long>> matrix;
 
// 矩阵乘法
matrix multiply(const matrix& A, const matrix& B) 
{
    int n = A.size(); //A矩阵行数
    int m = A[0].size();  //A矩阵列数、B矩阵行数
    int k = B[0].size();  //B矩阵列数
    matrix C(n, vector<long long>(k)); //乘出来的矩阵行数为A的行数,列数为B的列数
    for (int i = 0; i < n; i++)  //矩阵乘法
    {
        for (int j = 0; j < k; j++) 
        {
            for (int p = 0; p < m; p++) 
            {
                C[i][j] += A[i][p] * B[p][j];
            }
        }
    }
    return C;  //返回乘完后所得矩阵
}
 
// 矩阵快速幂
matrix matrix_pow(matrix A, int n) 
{
    matrix ans(A.size(), vector<long long>(A.size()));
    // 初始化为单位矩阵
    for (int i = 0; i < ans.size(); i++) 
    {
        ans[i][i] = 1;
    }
    while (n > 0) 
    {
        if (n & 1) //n的最后一位为1
        {
            ans = multiply(ans, A);
        }
        A = multiply(A, A); //更新A矩阵(累乘起来)
        n >>= 1; //右移,相当于除以2
    }
    return ans;  //返回乘完后的矩阵
}
 
// 计算Fibonacci数列的第n项
long long fibonacci(int n) {
    if (n <= 0) {
        return 0;
    }
    matrix A = {{0, 1}, {1, 1}};  // 初始矩阵
    matrix ans = matrix_pow(A, n);
    return ans[1][0];
}
 
int main() 
{
    int n;
    while(cin>>n)
    {
        cout << fibonacci(n) << endl;
    }
    return 0;
}

 测试用例:

关于矩阵快速幂的理论解释可以看一下这个视频,跟着推导一下会很清楚。

【竞赛向】斐波那契数列的矩阵快速幂求法

问题二:求第n项斐波那契数是奇数还是偶数。

奇+奇=偶,奇+偶=奇,偶+偶=偶

#include <iostream>
#include <algorithm>
using namespace std;
int feibo(int n)
{
    if(n==1) return 1;  //返回1代表奇数
    else if(n==2) return 1;
    else return (feibo(n-1)+feibo(n-2))%2;  //返回模2计算结果
}
int main()
{
    int n;
    cin>>n;
    if(feibo(n)==0) cout<<"even"<<endl;
    else cout<<"odd"<<endl;
    return 0;
}

测试用例: 

3、求n的阶乘问题

输入整数n,输出n的阶乘

一、简单解法:

#include <iostream>
#include <algorithm>
using namespace std;
long long jiecheng(int n)
{
    if(n==1||n==0) return 1;
    else return n*jiecheng(n-1);
}
int main()
{
    int n;
    cin>>n;
    long long ans;
    ans=jiecheng(n);
    cout<<ans<<endl;
    return 0;
}

测试用例: 

二、高精度的阶乘算法

算法思想来源于大数乘法。当c++无法处理long long以外的整数乘法时,需要借助高精度算法。

用数组倒序存储阶乘数。每乘一个新数时都要让这个新的数乘已算过的阶乘数的每一位,并进位(相当于先乘后加)

#include <iostream>
#include <algorithm>
#define N 100010
using namespace std;
int a[N];
int main()
{
    long long n;
    cin>>n;
    a[0]=1;  //设置哨兵位置存放数字有多少位
    a[1]=1;  //阶乘数至少为1,个位数先初始化为1
    for(int i=2;i<=n;i++)
    {
        int carry=0; //进位
        for(int j=1;j<=a[0];j++)
        {
            a[j]=a[j]*i+carry;  //新的乘数要乘已有阶乘数的每一位。注意进位
            carry=a[j]/10; //计算进位
            a[j]%=10;  //取个位
        }
        while(carry)  //处理最高位的进位
        {
            a[0]++;
            a[a[0]]=carry%10;
            carry/=10;
        }
    }
    for(int i=a[0];i>=1;i--) //逆序输出计算好的阶乘数
    {
        cout<<a[i];
    }
    cout<<endl;
    return 0;
}

测试用例:

###关于高精度阶乘算法更详细的数学解释内容可以看这篇博文###

高精度阶乘计算

4、整数划分问题

问题描述:

整数划分问题是将一个正整数n分解成若干个正整数的和,其中每个正整数都可以重复使用,且分解出来的数的个数是不限定的。例如,将整数4分解成若干个正整数的和,可以有以下五种不同的分解方式:

4
3 + 1
2 + 2
2 + 1 + 1
1 + 1 + 1 + 1
经典的整数划分问题是要求对于给定的正整数n,计算将n分解成若干个正整数的和的所有不同的分解方式的数量,也就是求整数n的划分数。

#include <iostream>
#include <algorithm>
#define N 100010
using namespace std;
void part(int ans[],int l,int n,int nowmax)
{     //ans[]表示一个整数数组,用于存储在当前划分中选择的数字
      //l记录当前ans数组的长度,即划分数
      //n为需要划分的整数,nowmax为在当前递归层数中,可以选择的最大数字

    if(n==0&&l>1)  //找到一种划分方案,当至少有两个分解数时输出
    {
        for(int i=0;i<=l-2;i++)
        {
            cout<<ans[i]<<"+";
        }
        cout<<ans[l-1]<<endl;
    }
    else if(n>0 && nowmax>0) //说明还有可选的数,递归分解
    {
        for(int i=nowmax;i>=1;i--)
        {
            ans[l]=i;
            part(ans,l+1,n-i,i);
        }
    }
}

int ans[N];  //全局变量自动初始化为0

int main()
{
    int n;
    cin>>n;
    part(ans,0,n,n);
    return 0;
}

测试用例:

5、全排列问题

问题描述:排列问题。设R={r1,r2,···,rn}是要进行排列的n个元素,R=R-{r1}。集合X中元素的全排列记为Perm(X)。(ri)Perm(X)表示在全排列 Pe的每个排列前加上前缀得到的排列。R的全排列可归纳定义如下:
当n=1时,Perm(R)=(r),其中r是集合R中唯一的元素;
n>1时,PerPerm(R)由(r1)Perm(R1),(r2)Perm(R2),···,(rn)Perm(Rn)构成。

思路分析:

递归求解。首先将第一个位置摘出来,这样问题就变成了第一个位置的排列和后面剩下数的全排列

对于第一个位置,可以让所有数都当一遍第一,在每个数当第一的时候,继续递归遍历后续数的全排列。

问题解决:

#include <iostream>
#include <algorithm>
using namespace std;
void swap(int &a,int &b) //定义交换函数
{
    int t;
    t=a;
    a=b;
    b=t;
}
void perm(int a[],int start,int end) //排列函数
{
    if(start==end)  //如果找到最后一轮(只剩一个数字)直接输出
    {
        for(int i=0;i<=end;i++)
        {
            cout<<a[i];
        }
        cout<<endl;
        return;
    }
    else //否则继续排列
    {
        for(int i=start;i<=end;i++)  //遍历每个数,让每个数都当一次第一个数
        {
            swap(a[start],a[i]);  //将当前遍历到的数和第一个数交换
            perm(a,start+1,end);  //按此继续全排列除第一个数以外的后面的数
            swap(a[start],a[i]);  //这个数当第一的情况遍历完了,换回来,便于下一个数当第一
        }
    }
}
int main()
{
    int n;
    cin>>n;
    getchar();
    int a[n];
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
        getchar();
    }
    sort(a,a+n);
    perm(a,0,n-1); //注意范围是0到n-1,perm函数里的循环也要相应为<=end
    return 0;
}

测试用例:

更详细的思路推导参考这篇文章

关于递归解决全排列问题的研究

分治问题

分治解题步骤:

  1. 分解:将原问题划分成若干个规模更小的子问题。

  2. 解决:递归地求解每个子问题。

  3. 合并:将子问题的解合并成原问题的解。

 需要注意的是:使用分治算法时,要保证子问题可以独立地求解,即子问题之间没有相关性。

 效率分析:分治算法的时间复杂度为O(nlogn),优于朴素算法的O(n^2)

 缺点:子问题规模可能划分不均衡导致效率降低。

1、二分搜索问题

给定已经排好的n个元素a[0:n-1],要在这n个元素中找出一特定元素x,返回x下标。

#include <iostream>
#include <algorithm>
#define N 10000
using namespace std;
int binarySearch(int a[],int x,int len)
{
    int begin=0;
    int end=len-1;
    int mid=(begin+end)/2;
    while(a[mid]!=x)
    {
        if(a[mid]>x)
        {
            end=mid-1;
            mid=(begin+end)/2;
        }
        else
        {
            begin=mid+1;
            mid=(begin+end)/2;
        }
    }
    return mid;
}
int main()
{
    int a[N];
    int n,x;
    cin>>n;
    getchar();
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
        getchar();
    }
    cin>>x;
    getchar();
    cout<<binarySearch(a,x,n)<<endl;
    return 0;
}

测试用例:

2、棋盘覆盖

问题描述:

在一个2^k * 2^k*(k>=0)个方格组成的棋盘中,恰有一个方格与其它方格不同,称该方格为特殊方格。棋盘覆盖问题要求用图2-5所示的4种不同形状的L型骨牌覆盖给定棋盘上除特殊方格以外的所有方格,且任何两个L型骨牌不得重复覆盖。

 问题分析:

我们可以把棋盘划分为四个子棋盘,按照分治的想法,我们需要求出每个子棋盘中L骨牌覆盖的情况,再合并到一起即为问题的解。

下面我们思考如何求解四个子棋盘内的覆盖情况。特殊的那块格子必定落到某一个子棋盘内。这使得它不同于另外三个子棋盘。如果能采用同样的方法处理四块子棋盘,会使问题大大简化。

所以我们可以在棋盘的最中心,即每个子棋盘提供一个格子,用一块L骨牌覆盖没有特殊格子的子棋盘提供的那三个小格上。这样就相当于每个子棋盘都有了一块特殊格子。这样就可以用同一种方法处理四块子棋盘了。

效率分析:

按照教材上的算法代码可以解决此问题:

#include <iostream>
#include <algorithm>
using namespace std;
#define N 100
int tile=1; //特殊格子数
int board[N][N];

void ChessBoard(int tr,int tc,int dr,int dc,int size)
{
    if(size==1)
    {
        return;
    }
    int t=tile++;  //先增加特殊格子数
    int s=size/2;  //分割棋盘(s为当前棋盘的大小)

    //覆盖左上角的子棋盘
    if(dr<tr+s && dc<tc+s)  //特殊格子在此子棋盘中,继续递归
    {
        ChessBoard(tr,tc,dr,dc,s);
    }
    else   //若不在,就把右下角格子变成特殊格子(用t号骨牌覆盖)
    {
        board[tr+s-1][tc+s-1]=t;
        ChessBoard(tr,tc,tr+s-1,tc+s-1,s);  //继续递归覆盖其余方格
    }

    //覆盖右上角的子棋盘
    if(dr<tr+s && dc>=tc+s)  //特殊格子在此子棋盘中,继续递归
    {
        ChessBoard(tr,tc+s,dr,dc,s);
    }
    else   //若不在,就把左下角格子变成特殊格子(用t号骨牌覆盖)
    {
        board[tr+s-1][tc+s]=t;
        ChessBoard(tr,tc+s,tr+s-1,tc+s,s);  //继续递归覆盖其余方格
    }

    //覆盖左下角的子棋盘
    if(dr>=tr+s && dc<tc+s)  //特殊格子在此子棋盘中,继续递归
    {
        ChessBoard(tr+s,tc,dr,dc,s);
    }
    else   //若不在,就把右上角格子变成特殊格子(用t号骨牌覆盖)
    {
        board[tr+s][tc+s-1]=t;
        ChessBoard(tr+s,tc,tr+s,tc+s-1,s);  //继续递归覆盖其余方格
    }

    //覆盖右下角的子棋盘
    if(dr>=tr+s && dc>=tc+s)  //特殊格子在此子棋盘中,继续递归
    {
        ChessBoard(tr+s,tc+s,dr,dc,s);
    }
    else   //若不在,就把左上角格子变成特殊格子(用t号骨牌覆盖)
    {
        board[tr+s][tc+s]=t;
        ChessBoard(tr+s,tc+s,tr+s,tc+s,s);  //继续递归覆盖其余方格
    }

}
int main()
{
    int size=1;
    cin>>size;
    int dr,dc;
    cin>>dr>>dc;
    ChessBoard(0,0,dr,dc,size);
    for(int i=0;i<size;i++)
    {
        for(int j=0;j<size;j++)
        {
            printf("%6d",board[i][j]);
        }
        cout<<endl;
    }
    return 0;
}

测试用例:

3、合并排序

分:不断将待排序列分半

治:分别排序

合:再合并,并将排序结果copy到原数组

效率分析:

注意空间复杂度为O(n),因为设置了一个辅助数组用于合并排序结果。

#include <iostream>
#include <algorithm>
using namespace std;
#define N 1000
int num[N];
int tem[N];
void merge_sort(int a[],int left,int right)
{
    if(left>=right) return; //递归出口

    int mid=(left+right)/2;
    merge_sort(a,left,mid);  //分成两个子部分,分别排序
    merge_sort(a,mid+1,right);

    int k=0,i=left,j=mid+1;
    while(i<=mid&&j<=right)   //分别排完序后合并
    {
        if(a[i]<=a[j])
        {
            tem[k++]=a[i++];
        }
        else
        {
            tem[k++]=a[j++];
        }
    }
    while(i<=mid) tem[k++]=a[i++];  //没排完的继续放入,连着放即可,先i后j
    while(j<=right) tem[k++]=a[j++];

    for(int i=left,j=0;i<=right;i++,j++) //合并完的结果copy到原数组
    {
        a[i]=tem[j];  //一定要注意tem[]要单独设一个j下标遍历,因为上面每次都是从k=0开始放的
    }
}

int main()
{
    int n;
    cin>>n;
    getchar();
    for(int i=0;i<n;i++)
    {
        cin>>num[i];
        getchar();
    }
    merge_sort(num,0,n-1);
    for(int i=0;i<n;i++)
    {
        cout<<num[i]<<" ";
    }
    cout<<endl;
    return 0;
}

测试用例:

4、快速排序

关于快速排序之前写过一版选基准记录的三者取中快排算法,可以直接看这个

<数据结构学习>排序——快速排序

由于划分的时候pivot选取、移动的代码可以有很多不同形式,在这里也再写一版较为易懂的。

#include <iostream>
#include <algorithm>
using namespace std;
#define N 1000
void swapp(int &x,int &y)
{
    int t;
    t=x;
    x=y;
    y=t;
}
int part(int a[],int left,int right)
{
    int pivot = a[left];  //直接选最左边的元素当pivot
    int i=left,j=right;
    while(i<j)
    {
        while (a[j]>=pivot && i<j) j--;
        while(a[i]<=pivot && i<j) i++;
        if(i<j) swapp(a[i],a[j]);
    }
    a[left]=a[i];  //将基准元素换到中间
    a[i]=pivot;
    return i;    //返回基准元素下标
}
void quicksort(int a[],int left,int right)
{
    if(left>=right) return;   //递归出口

    int mid=part(a,left,right);
    quicksort(a,left,mid-1);  //左边再排序
    quicksort(a,mid+1,right);  //右边再排序

}
int main()
{
    int n;
    cin>>n;
    getchar();
    int num[N];
    for(int i=0;i<n;i++)
    {
        cin>>num[i];
        getchar();
    }
    quicksort(num,0,n-1);
    for(int i=0;i<n;i++)
    {
        cout<<num[i]<<" ";
    }
    cout<<endl;
    return 0;
}

测试用例:

5、最接近点对问题

基本思想:

以二维点(平面上的散点)举例。首先将所有点按照横坐标排序。点数较小(<=3)时可直接两两求间距取最小。若点数较多可分治,分成左右两部分,分别求左边最近的两点间距和右边最近的两点间距。在左右求最小距离时同理,点数少直接求,点数多继续分治。

需要注意的是,左右求出最小值,比较再取最小(记为min)之后还不是问题的解。我们还需要考虑中间被划分的地方是否有解优于左右的解

所以,寻找距离划分中点<=min的一部分点,并按纵坐标将其排序,考察距离划分中点的纵坐标差,大于min的直接舍去,因为横纵坐标平方和不可能比min还小。纵坐标差小于min的可以算一下距离,若小于min就更新min。

例题:套圈问题

可以看之前写的这篇<算法学习>分治——求最近两点间距离

6、循环赛日程表

题目描述:

设有n=2^k个运动员要进行网球循环赛。现要设计一个满足以下要求的比赛日程表:
(1)每个选手必须与其他n-1个选手各赛一次;
(2)每个选手一天只能参赛一次;
(3)循环赛在n-1天内结束

解题方法:

此问题可以通过递归和循环两种方式解决。

这篇文章写得很清晰 循环赛日程表 (递归与分治)

 可以看一下算法分析部分图文。

具体代码我再多解释一下

1、递归方法 (只看table函数即可,其他不过多解释)

#include<iostream>
#include<cmath>
using namespace std;

int a[100][100];

void table(int k, int d)
//          边长   步长    
{
	if (k == d)  //边长等于步长时,说明分块copy已经完成,直接返回
		return;
	int i, j;
	for (i = 0; i < d; i++)  //copy的具体过程
	{
		for (j = 0; j < d; j++) 
		{
			a[i + d][j + d] = a[i][j];  //先将左上角的小边长为d的正方形copy到右下角
			a[i][j + d] = a[i][j] + d;  //再将左上角小边长为d的正方形每个数+d copy到右上角
			a[i + d][j] = a[i][j] + d;                                       //和左下角
		}
	}
	table(k, d * 2);  //扩大步长,继续copy,直到达到规定人数大小的大正方块
}

int main()
{
	//输入人数
	int n;
	cout << "学生人数k=2^n,请输入k:";
	int k;
	cin >> n;
	k = pow(2, n);
	//判断只有一个人时
	if (k == 1)
		a[0][0] = 0;
	else
		a[0][0] = 1;
	//递归
	table(k, 1);
	//输出
	for (int i = 0; i < k; i++)
	{
		for (int j = 0; j < k; j++)
		{
			cout << a[i][j]<<' ';
		}
		cout << endl;
	}
}

2、 循环方法 (只看Table函数即可,其他不过多解释)

#include<iostream>
#include<cmath>
#define N 50
using namespace std;
int a[N][N];
void Table(int k);
void print(int k);
int main()
{
	
	int k;
	cout << "设参赛选手的人数为n(n=2^k),请输入k 的值:";
	do
	{
		cin >> k;
		if (k != 0)
		{
			Table(k);  //注意传的是指数,不是直接传总人数,因为后面要用k做循环轮数
			print(k);
		}
		else
			cout << "您输入的数据有误,请重新输入!" << endl;
	} while (k != 0);
}

void Table(int k)
{
	int n = 1;//数组下标从1开始
	for (int i = 1; i <= k; i++)
		n *= 2;//求总人数(相当于pow函数)

	for (int i = 1; i <= n; i++)
		a[1][i] = i;//初始化,第一行等于1--n

	int m = 1;//填充起始位置(用来控制每一次填表时i行j列的起始填充位置)
	for (int s = 1; s <= k; s++)//总共循环k次(s指对称赋值的总循环次数,即分成几大步进行制作日程表)
	{
		n = n / 2;  //表示一行分几组和上面行进行对称copy
		for (int t = 1; t <= n; t++)//分的块数(t指明内部对称赋值的循环次数)
		{ 
			for (int i = m + 1; i <= 2 * m; i++)  //copy具体过程
				for (int j = m + 1; j <= 2 * m; j++)
				{
					a[i][j + (t - 1) * m * 2] = a[i - m][j + (t - 1) * m * 2 - m];//左上角赋给右下角
					a[i][j + (t - 1) * m * 2 - m] = a[i - m][j + (t - 1) * m * 2];//右上角赋给左下角
				}
		}
		m *= 2;//更新填充起始位置

	}
}

void print(int k)
{
	int  i, j;
	int n = pow(2, k);
	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= n; j++)
		{
			cout << a[i][j] << ' ';
		}
		cout << "\n";
	}

}

 7、找第k小元素问题

跟快速排序有点像,不过不用完全排好,只需要找pivot恰好是在第k个元素位置上即可。

具体算法和例题看这篇比较好。

递归与分治 | 1:选择算法/中位数 —— 例题:油井

 8、Strassen矩阵乘法和大整数乘法

可以直接看这两篇文章,写的都很好,我就不再赘述了。

《算法导论》第四章-矩阵乘法的Strassen算法(含C++代码)

【分治算法2.4】大整数的乘法(C++实现)

the end


参考文章和书目:

算法小课堂(二)递归与分治

计算机算法设计与分析(第五版) 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值