基础实验4-2.4 搜索树判断 (25分)

本文介绍了一种算法,用于判断给定的整数序列是否为二叉搜索树或其镜像树的前序遍历,并输出对应的后序遍历序列。通过构建二叉树并比较前序遍历结果,实现了对输入序列的有效验证。

基础实验4-2.4 搜索树判断 (25分)
对于二叉搜索树,我们规定任一结点的左子树仅包含严格小于该结点的键值,而其右子树包含大于或等于该结点的键值。如果我们交换每个节点的左子树和右子树,得到的树叫做镜像二叉搜索树。

现在我们给出一个整数键值序列,请编写程序判断该序列是否为某棵二叉搜索树或某镜像二叉搜索树的前序遍历序列,如果是,则输出对应二叉树的后序遍历序列。

输入格式:
输入的第一行包含一个正整数N(≤1000),第二行包含N个整数,为给出的整数键值序列,数字间以空格分隔。

输出格式:
输出的第一行首先给出判断结果,如果输入的序列是某棵二叉搜索树或某镜像二叉搜索树的前序遍历序列,则输出YES,否侧输出NO。如果判断结果是YES,下一行输出对应二叉树的后序遍历序列。数字间以空格分隔,但行尾不能有多余的空格。

输入样例1:
7
8 6 5 7 10 8 11

输出样例1:
YES
5 7 6 8 11 10 8

输入样例2:
7
8 6 8 5 10 9 11

输出样例2:
NO

//用链表的形式
#include<iostream>
using namespace std;
struct BNode{
    int data;
    struct BNode* left;
    struct BNode* right;
};
    
/*这里的tree是一个引用,去掉引用符号,main()中的tree仍然是NULL,没有改变tree的值;
 有引用&,第一次函数传进来时是NULL,后面传出是tree总是指向头结点8,*/

void Insert(struct BNode* &tree,int data){
    static int cnt=0;
    if(!tree){
        struct BNode* p=(struct BNode*)malloc(sizeof(struct BNode));
        p->data=data;
        //cout<<"check001:"<<p->data<<endl;     8 6 5 7 10 8 11
        p->left=NULL;
        p->right=NULL;
        tree=p;         //tree->left=p;or tree->right=p;
        //cout<<"check002:"<<tree->data<<endl;  8 6 5 7 10 8 11
    }
    else if(data<tree->data){/*cout<<"insert left "<<++cnt<<endl;*/Insert(tree->left, data);}
    else if(data>=tree->data){/*cout<<"insert right "<<++cnt<<endl;*/Insert(tree->right, data);}
    //cout<<"check003:"<<tree->data<<endl; 测试用,输出的值总为8
}

int b[100],a[100];;int cnt1=0;int cnt2=0;

void PreorderTraversal(struct BNode* tree){
    if(tree){
        b[cnt1++]=tree->data;
        //cout<<"cnt1++: "<<tree->data<<endl;
        PreorderTraversal(tree->left);
        PreorderTraversal(tree->right);
    }
}

void PostorderTraversal(struct BNode* tree){
    if(tree){
           a[cnt2++]=tree->data;
            cout<<tree->data<<endl;
           PostorderTraversal(tree->right);
           PostorderTraversal(tree->left);
       }
}

void printPostOder(int N){
    cout<<"后序遍历输出:"<<endl;
    for(int i=N-1;i>=0;i--){
        cout<<a[i]<<" ";
    }
    cout<<endl;
}

int main(){
    struct BNode* tree=NULL;
    int N,data,cnt3=0;
    cin>>N;
    int c[N];
    for(int i=0;i<N;i++){
        cin>>data;
        c[i]=data;
        Insert(tree,data);
    }
    PreorderTraversal(tree);
    for(int i=0;i<N;i++){
        if(b[i]==c[i]){
            ++cnt3;
            //cout<<b[cnt3++]<<"  "<<c[i]<<"  "<<endl;
            //cnt3++;       cn3初始化为0,cn3++返回0, 从0-6,
            //++cnt3,       cn3++返回1, 从1-7,
        }
    }
    //cout<<cnt3;
    if(cnt3==N){
        cout<<"Yes"<<endl;
        PostorderTraversal(tree);
        printPostOder(N);
    }else
        cout<<"NO"<<endl;
    return 0;
}

#include <iostream>
#include<vector>
#include<stdio.h>
using namespace std;
struct bst{
    int data;
    bst *left,*right;
};
vector<int>v1,v2,v3,v4,v5,v6;
bst *init(int key){
    bst *tmp=new bst;
    tmp->data=key;
    tmp->left=tmp->right=NULL;
    return tmp;
}
bst *insert(bst *root,int key){
    if(root==NULL){
        return init(key);
    }else if(key>=root->data){
        root->right=insert(root->right,key);
    }else if(key<root->data){
        root->left=insert(root->left,key);
    }
    return root;
}
void PreTranse(bst *root){
    if(root==NULL){
        return ;
    }else{
        v5.push_back(root->data);
        //printf("%d ",root->data);
        PreTranse(root->left);
        PreTranse(root->right);
    }
}

void InorderTranse(bst *root){
    if(root==NULL){
        return ;
    }else{
        InorderTranse(root->left);
        v1.push_back(root->data);
        v2.push_back(root->data);
        v4.push_back(root->data);
        //printf("%d ",root->data);
        InorderTranse(root->right);
    }
}

void PostTranse(bst *root){
    if(root==NULL){
        return ;
    }else{
        PostTranse(root->left);
        PostTranse(root->right);
        v6.push_back(root->data);
    }
}
bool cmp(int x,int y){
    return x>=y;
}
int main() {
    int n;
    cin>>n;
    int x;
    bst *root=NULL;
    for(int i=0;i<n;i++){
        cin>>x;
        v3.push_back(x);
        root=insert(root,x);
    }
    InorderTranse(root);
    sort(v2.begin(),v2.end());
    sort(v4.begin(),v4.end(),cmp);
    int flag=0,flag1=0;
    int tag=0;
    for(int i=0;i<v1.size();i++){       //判断是否为单增,否则flag=1;
        if(v1[i]==v2[i]){continue;}
        flag=1;
    }
    for(int i=0;i<v1.size();i++){       //判断是否为单减,否则flag1=1;
        if(v1[i]==v4[i]){continue;}
        flag1=1;
    }
    if(flag==1&&flag1==1){cout<<"NO"<<endl;return 0;}
    PreTranse(root);
    for(int i=0;i<v3.size();i++){
        if(v3[i]==v5[i]){continue;}
        else tag=1;
    }
    if(tag==1){cout<<"NO"<<endl;}
    else{
        cout<<"YES"<<endl;
        PostTranse(root);
        int i=0;
        for(;i<v6.size()-1;i++){
            printf("%d ",v6[i]);
        }
        printf("%d\n",v6[i]);
    }
    return 0;
}

摘要\n基于地图环境已知下的机器人自主导航,通过Matlab2018实现RRT与双向RRT算法实现全局路径规划,并将规划的点列写入txt文件。之后通过VS2013读取点列数据并以一种比较粗糙的控制方法实现对机器人移动路径控制,实现从起始点运动到目标点的任务。\n关键词: RRT;双向扩展平衡的连结型双树RRT;全局路径规划;机器人自主导航\n一、实验方法-基于地图已知全局路径规划\n传统的全局路径规划算法有人工势场法、模糊规则法、遗传算法、神经网络、模拟退火算法、蚁群优化算法等。但这些方法都需要在一个确定的空间内对障碍物进行建模,计算复杂度与机器人自由度呈指数关系,不适合解决多自由度机器人在复杂环境中的规划。基于快速扩展随机树(RRT / rapidly exploring random tree)的路径规划算法,通过对状态空间中的采样点进行碰撞检测,避免了对空间的建模,能够有效地解决高维空间和复杂约束的路径规划问题。该方法的特点是能够快速有效地搜索高维空间,通过状态空间的随机采样点,把搜索导向空白区域,从而寻找到一条从起始点到目标点的规划路径,适合解决多自由度机器人在复杂环境下和动态环境中的路径规划。\n本实验在理解了RRT算法为基础,使用双向扩展平衡的连结型双树RRT算法(双向RRT),即RRT_Connect算法来实现在地图已知情况下的全局路径规划问题,采用Matlab与VS2013以txt文件通信相结合方式。\n\n1.1RRT算法\n\n\n图一:RRT算法\nRRT是一种多维空间中有效率的规划方法。它以一个初始点作为根节点,通过随机采样增加叶子节点的方式,生成一个随机扩展树,当随机树中的叶子节点包含了目标点或进入了目标区域,便可以在随机树中找到一条由从初始点到目标点的路径,如图一所示,红线标注路径即为RRT算法找到一条由初始点到目标点路径。 ## 1.2双向RRT 基本的RRT每次搜索都只有从初始状态点生长的快速扩展随机树来搜索整个状态空间,如果从初始状态点和目标状态点同时生长两棵快速扩展随机树来搜索状态空间,效率会更高。为此,基于双向扩展平衡的连结型双树RRT算法,即RRT_Connect算法被提出。 其基本算法:\n\n该算法与原始RRT相比,在目标点区域建立第二棵树进行扩展。每一次迭代中,开始步骤与原始的RRT算法一样,都是采样随机点然后进行扩展。然后扩展完第一棵树的新节点𝑞 𝑛 𝑒 𝑤 𝑞𝑛𝑒𝑤qnew后,以这个新的目标点作为第二棵树扩展的方向。同时第二棵树扩展的方式略有不同(15~22行),首先它会扩展第一步得到𝑞 ′ 𝑛 𝑒 𝑤 𝑞′𝑛𝑒𝑤q′new,如果没有碰撞,继续往相同的方向扩展第二步,直到扩展失败或者𝑞 ′ 𝑛 𝑒 𝑤 = 𝑞 𝑛 𝑒 𝑤 𝑞′𝑛𝑒𝑤=𝑞𝑛𝑒𝑤q′new=qnew表示与第一棵树相连了,即connect了,整个算法结束。当然每次迭代中必须考虑两棵树的平衡性,即两棵树的节点数的多少(也可以考虑两棵树总共花费的路径长度),交换次序选择“小”的那棵树进行扩展。这种双向的RRT技术具有良好的搜索特性,比原始RRT算法的搜索速度、搜索效率有了显著提高,被广泛应用。首先,Connect算法较之前的算法在扩展的步长上更长,使得树的生长更快;其次,两棵树不断朝向对方交替扩展,而不是采用随机扩展的方式,特别当起始位姿和目标位姿处于约束区域时,两棵树可以通过朝向对方快速扩展而逃离各自的约束区域。这种带有启发性的扩展使得树的扩展更加贪婪和明确,使得双向RRT算法较之单向RRT算法更加有效。\n\n1.3 Matlab与VS2013相结合\n如图二程序设计图所示,Matlab与VS2013数据通信采用了txt方式,具体操纵说明如下:\n\n\n图二 程序设计图\n1.3.1在VS2013中对txt进行操作\n(1)将初始点与目标点数据写入txt以供matlab读取\n\nvoid outdata()\n{\n\tINT16 Code[4] = { Initial_rPos.coor_x, Initial_rPos.coor_y, Cur_dPos.coor_x, Cur_dPos.coor_y };//需要存入的数据\n\tofstream output;\n\toutput.open(\"D:\\\\Users\\\\Desktop\\\\Machine\\\\Plan_client\\\\Point.txt\");//存入的文件路径\n\tif (!output.is_open())\n\t{\n\t\tcout \u003C\u003C \"the file open fail\" \u003C\u003C endl;\n\t\texit(1);\n\t}\n\tfor (int i = 0; i \u003C 4; i++)\n\t{\n\t\toutput \u003C\u003C Code[i] \u003C\u003C \" \" \u003C\u003C endl;\n\t}\n\toutput.close();\n}\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n(2)在matlab生成坐标点序列后,VS要读取相应txt文件,以获取matlab的点列\n\nvoid indata()\n{\n\tifstream input;\n\tinput.open(\"E:\\\\Matlab2018a\\\\Path_Plan\\\\Plan_path.txt\");\n\tif (!input.is_open())\n\t{\n\t\tcout \u003C\u003C \"the file open fail\" \u003C\u003C endl;\n\t\texit(1);\n\t}\n\tfor (int i = 0; i \u003C 100; i++)\n\t{\n\t\tfor (int j = 0; j \u003C 2; j++)\n\t\t{\n\t\t\tinput >> Trajm[i][j];\n\t\t\tif (Trajm[i][j] != 0)\n\t\t\t\tsteps++;\n\t\t}\n\t}\n\tinput.close();\n}\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n1.3.2在Matlab2018a中对txt进行操作\n(1)读取VS2013写入的txt文件获取初始点与目标点\n\n Point=load('D:\\\\Users\\\\Desktop\\\\Machine\\\\Plan_client\\\\Point.txt') \n1\n(2)将全局规划的点列写入txt文件以供VS读取\n\nfid = fopen('Plan_path.txt','wt');\nmat = int16(New_path);\nfor i = 1:size(mat, 1)\n fprintf(fid, '%d', mat(i,1));fprintf(fid, '\\n');\n fprintf(fid, '%d', mat(i,2));fprintf(fid, '\\n');\nend\n1\n2\n3\n4\n5\n6\n二、实验过程\n2.1从单向RRT出发——发现问题\n在matlab2018a编写RRT程序,程序见附录1,实现800*1200像素的图片上寻找目标点到初始点可行路径,在这里先不考虑与VS2013的数据通信,选取初始点为(50,50),目标点为(450,450),matlab运行结果如图三所示。经过反复尝试,设定步长为30,节点阈值为20时,既可以满足全局路径规划的速度性,又可以避免在临近目标点出现多次路径寻找。\n\n\n图三:RRT演示\n但是在实验中发现,RRT全局路径规划会出现如图四所示的情况。对于RRT本身来说这是没有问题的,但是考虑到VS程序中我们假定机器人是一个半径为15的圆,这样此图RRT的轨迹便使机器人与障碍物相撞。为了避免这种情况发生,采取了使障碍物膨胀的方法。\n\n\n图四:RRT路径\n此外为了使目标点更好向外扩张,并加快程序的运行速度。我使用时RRT引入了比较常用的概率法:在随机树每次的生长过程中,根据随机概率来决定qrand是目标点还是随机点。在Sample函数中设定参数Prob,每次得到一个0到1.0的随机值p,当0\u003Cp\u003CProb的时候,随机树朝目标点生长行;当Prob\u003Cp\u003C1.0时,随机树朝一个随机方向生长,我设置Prob设置为0.5,经过图三与图四对比实验发现:该方法确实减少了无用路径扩展,加快了程序的运行速度。\n\n\n图五:RRT效果图\n经过文献查找,上述采用RRT为单向RRT算法,每次搜索都只有从初始状态点生长的快速扩展随机树来搜索整个状态空间,如果从初始状态点和目标状态点同时生长两棵快速扩展随机树来搜索状态空间,效率会更高,于是接下来在单向RRT基础上,进行了双向RRT实验。\n\n2.2障碍物膨胀\n障碍物膨胀的原理很简单:以某白像素点为中心,上移20,下移20,左移20,右移20,形成的40*40的区域内进行判断,缩小计算量。之后如果遍历该区域,如果有黑像素点,且该黑像素点到白像素点欧式距离小于等于20,则该白像素点变为黑像素点。Matlab程序如下,程序包含了对图边缘的处理,其中MapT为标志位,目的是使已经变为黑像素的点不影响之后的判断。\n\nMapT=logical(ones(800,1200)-1); \n for i=1:800\n for j=1:1200\n if((map(i,j)==true)&&(MapT(i,j)==false))\n if(i-20>0),m1=i-20;else,m1=1;end\n if(i+20\u003C=800),m2=i+20;else,m2=800;end\n if(j-20>0),n1=j-20;else,n1=1;end\n if(j+20\u003C=1200),n2=j+20;else,n2=1200;end\n num=0;\n for m=m1:m2\n for n=n1:n2\n if(((m-i)^2+(n-j)^2\u003C=20^2)&&(MapT(m,n)==false))…\n&&(num==0)&&(map(m,n)==false) \n map(i,j)=false;MapT(i,j)=true;num=1;end\n end\n end \n end \n end\n end\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n其实现效果如图六所示,仔细观察下图我们发现障碍物膨胀边缘会不平整,但是因为膨胀20的缘故,机器人在运动时候不会受其影响而出现碰撞的情况,这也就是选取障碍物膨胀20的原因。\n\n\n图六:障碍物膨胀图\n2.3 双向RRT实现\n在单向RRT算法的基础上实现双向RRT算法比较简单,参考网上资料与自己理解,编写双向RRT的RRT_Connect程序,程序详见附录2。运行程序如图七所示。\n\n\n\na\n\n\nb\n图七:双向RRT\n图(a)中蓝线表示从初始点向目标点扩展,红线表示从目标点向初始点扩展,图中一条绿线表示就是就是最后一步。图(b)就是双向RRT获取的全局路径。我们可以明显看到路径是曲折的,那么VS获取这样的路径点去控制机器人移动是比较费劲,为何不把路径点去掉一些,使得路径比较平整呢?\n2.4 路径化直\n双向RRT返回回来的数据是一系列的坐标点,这些坐标点是随机扩展出来的。这些点连接接起来的路径时曲折的,为了使机器人少走弯路,将坐标点去掉一部,并将路径化直。该想法实现效果图如图八所示,(b)图将(a)图的RRT生成点轨迹化直。\n\n\na\n\n\nb\n图八 路径化直\n实现程序如下:基本思想就是对双向RRT获取路径点进行取优,假设路径点序列为A,A[0]一定与A[1]无障碍相连,那以A[0]为起点依次判断A[1]、A[2]…是否无障碍相连,假设A[0]与A[i]之间是存在障碍物,那么A[0]与A[i-1]一定是无障碍相连,这样便可以省去A[1]至A[i-2]的点。之后再以A[i-1]为起点重复上述操作思想,直至遍历到最后一点。\n New_path(1,:)=path(1,:);\n step=size(path,1);\n j=1;\n sign=0;\n a=New_path(j,:);\n for i=2:step\n b=path(i,:);\n if(a(1)\u003Cb(1)),m1=a(1);m2=b(1);else,m1=b(1);m2=a(1);end\n if(a(2)\u003Cb(2)),n1=a(2);n2=b(2);else,n1=b(2);n2=a(2);end \n for m=m1:m2\n for n=n1:n2\n if(((map(m,n)==false)&&(map(m,n+1)==false))&&(det([a-b;[m,n]-b])*det([a-b;[m,n+1]-b])\u003C=0))...\n ||(((map(m,n)==false)&&(map(m+1,n)==false))&&(det([a-b;[m,n]-b])*det([a-b;[m+1,n]-b])\u003C=0))...\n ||(((map(m,n)==false)&&(map(m+1,n+1)==false))&&(det([a-b;[m,n]-b])*det([a-b;[m+1,n+1]-b])\u003C=0)) \n sign=1; break;end \n end\n if(sign==1),break;end \n end\n if(sign==1),j=j+1;New_path(j,:)=path(i-1,:);a=New_path(j,:);end\n sign=0;\n end\n j=j+1;New_path(j,:)=path(i,:);\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n2.5 机器人控制\n双向RRT返回路径点序列, VS2013只要根据这些点序列控制机器人移动即可。程序控制思想如下:角速度控制与线速度控制开,机器人先转角度,在移动。粗糙一点,直接计算角度差与距离差,通过固定帧数平摊角度差与距离差赋值给角速度与线速度,这样便可以实现对机器人控制。程序见附录3。\n\n三、实验结果\n3.1双向RRT实现\n双向RRT路径点规划结果如图九所示,这里去掉膨胀后显示的路径规划,实际上也就是机器人移动路径。\n\n\na\n\n\nb\n\n\nc\n\n\nd\n图九:RRT路径规划图\n通过实验发现,Matalb2018a实现双向RRT全局路径规划返回了精确的点序列,方便了在VS2013端实现对机器人运动的控制。\n\n3.2机器人控制实现\n通过设置断点方式并利用txt文本文件实现VS2013与Matlab2018a通信的操作,便可实现对机器人控制,通过上述图九返回的全局规划路径,并利用之前对机器人控制方法可以实现对机器人控制,控制结果如图十所示。下面的图(a)参数设置为10帧,及每一次机器人转10次旋转到下一次要运动方向,机器人位移10次便到达下一个位置点,同理图(b)与图©设置为50帧,而图(d)参数设置为100帧。\n\n\na\n\n\nb\n\n\nc\n\n\nd\n图十:机器人控制结果图\n通过实验结果,VS2013获取双向RRT的路径规划点序列后,通过粗糙的控制方法是可以实现机器人运动控制,当然也存在一些不足,这里放在实验总结中去详细叙述。\n\n四、实验总结\n本次作业使用Matlab2018a与VS2013双软件完成。Matlab2018a实现双向RRT程序的“外包“,当然这样外包是在通过txt文本文件与VS断点实现数据传输通信,它通过读入机器人设定的起始点与目标点坐标规划出一条可行路径,并将此路径点序列写入txt文本文件。VS2013在继续运行后读取RRT路径规划点序列并通过粗糙的控制算法实现对机器人运动控制。当然这样方法存在一些不足之处。\n\n4.1不足之处\n4.1.1双软件——操作繁琐\n如果要实现上述方法,首先在VS2013中读取RRT点列前设置断点,然后运行VS程序,选择好起始点与目标点后,程序将两点信息写入txt文件中后便进入断点暂停。这时运行Matlab中的RRT_Connect程序,程序便会读取你设置的起始点与目标点信息,经过双向RRT后获取优化的路径点序列并将点序列写入另一个txt文件中。完成后你便可以继续运行VS程序,最终你会看到机器人在你控制下从起始点运动至目标点。这样的操作步骤是比较繁琐的,但是幸好传递过来的数据是非常精确的,不会出现数据传输错误问题。\n\n4.1.2 粗糙控制方法存在弊端——决定运行速度不能过快\n最终程序选用50帧参数,因为50帧意味着50次完成依次角度旋转或位置移动,每一次旋转角度,位移距离都是存在误差的,并且这样的误差会累积,形成累计误差。如果累计误差过大会深深影响机器热的运动精度,最终导致机器人偏离预想航向,如下图十一所示,当帧数设置为5,其累计误差较大,最终出现碰撞。\n\n\n图十一 碰撞\n4.1.3 起始点与目标点不能设置靠近障碍物20范围内\n因为当初为了避免RRT算法出现通过狭窄路径情况并避免机器人半径15带来的影响,选择在进行RRT算法之前将障碍物膨胀20。如果你选定目标点与起始点靠近障碍物20内,RRT算法将自动出现错误,因为读入数据将首先进行与地图比较,程序如下,看是否与地图内障碍物冲突。\n\nif ~feasiblePoint(source,map), error('source lies on an obstacle or outside map'); end\nif ~feasiblePoint(goal,map), error('goal lies on an obstacle or outside map'); end\n…………………………………………………………………………….\n%% feasiblePoint函数 检查无碰撞点和内部地图\nfunction feasible=feasiblePoint(point,map)\n feasible=true;\n if ~(point(1)>=1 && point(1)\u003C=size(map,1) && point(2)>=1 && point(2)\u003C=size(map,2) && map(point(1),point(2))==1)\n feasible=false;\n end\nend\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n致谢\n感谢石老师在机器人自主导航与环境建模这门课上的精彩讲解。通过对门课的学习与实践,使我对视觉感知、多传感器信息融合、运动规划与自主定位有了初步的了解。在此了解的基础上,我深入学习了运动规划的内容,并利用其中的RRT算法实现了本次个人作业的考核要求。此外还要感谢石老师认真对我们作业遇到问题进行答疑,解答我们的困惑,使我和同学们能顺利完成此次个人作业。\n二二年十二月 于南京\n链接: 视频演示:link.\n程序代码:link.根据以上内容给我完整
最新发布
11-26
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值