图的最小生成树之Kruskal算法

参考:https://wangkuiwu.github.io/2013/04/12/kruskal-cplus

Kruskal算法介绍

克鲁斯卡尔(Kruskal)算法,和普利姆(Prim)算法类似,也是用来求加权连通图的最小生成树的算法。关于最小生成树的概念,已经在之前的图的最小生成树之Prim算法篇首介绍过。

Kruskal 与 Prim 算法思想出发点不同,Prim 算法是以顶点出发的,而 Kruskal 算法是以边出发的。

其算法思想是:按照权值从小到大的顺序选择 n-1 条边,并保证这 n-1 条边不构成回路。

其具体做法是:首先构造一个只含 n 个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止。

Kruskal算法图解

还是以 Prim 中的图 G4 为例:

Kruskal算法-图1

图 G4

来对 Kruskal 进行演示如下(假设用数组 R 保存最小生成树结果)。

Kruskal算法-图2

第 1 步:将边 <E, F> 加入到 R 中。边 <E, F> 的权值最小,因此将他们加入到最小生成树结果中。

第 2 步:将边 <C, D> 加入到 R 中,上一步操作之后,边 <C, D> 的权值最小,因此将它加入到最小生成树结果 R 中。

第 3 步:将边 <D, E> 加入到 R 中,上一步操作之后,边 <D, E> 的权值最小,因此将它加入到最小生成树结果 R 中。

第 4 步:将边 <B, F> 加入到 R 中,上一步操作之后,边 <C, E> 的权值最小,但 <C, E> 会和已有的边构成回路,因此,跳过边 <C, E>。同理,跳过边 <C, F>。

第 5 步:将边 <E, G> 加入到 R 中,上一步操作之后,边 <E, G> 的权值最小,因此将它加入到最小生成树结果 R 中。

第 6 步:将边 <A, B> 加入到 R 中,上一步操作之后,边 <F, G> 的权值最小,但 <F, G> 会和已有的边构成回路,因此,跳过边 <F, G>。同理,跳过边 <B, C>。最后将边 <A, B> 加入到最小生成树结果 R 中。

Kruskal算法分析

根据前面介绍的克鲁斯卡尔算法的基本思想和做法,就能够了解到,克鲁斯卡尔算法重点需要解决的以下两个问题:

问题 1:对图的所有边按照权值大小进行排序。

问题 2:将边添加到最小生成树中时,怎么判断是否形成了回路。

问题一很好解决,直接采用排序算法进行排序即可。

问题二处理方式是:记录顶点在“最小生成树”中的终点,顶点的终点是“在最小生成树中与它连通的最大顶点”(关于这一点,后面会通过图片给出说明)。然后每次需要将一条边添加到最小生成树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。 以下图来进行说明:

Kruskal算法-图3

在将 <E, F>、<C, D>、<D, E> 加入到最小生成树 R 中之后,这几条边的顶点都有了终点:

C 的终点是 F。

D 的终点是 F。

E 的终点是 F。

F 的终点是 F。

关于终点,就是将所有的顶点按照从小到大的顺序排序好之后,某个顶点的终点就是“与它连通的最大顶点”。因此,接下来,虽然 <C, E> 是权值最小的边。但是 C 和 E 的终点都是 F,即它们的终点相同,因此,将 <C, E> 加入到最小生成树的话,会形成回路,这就是判断回路的方式。

注:这里有些文章对获得顶点的终点的描述是使用并查集,如果 arr 为并查集数组,将边 <E, F> 加入时,arr[E] = F,即把并查集中 E 下标的值改为 F,如果 arr[C] = D,arr[D] = E ,那么查询 arr[C] 和 arr[D] 的终点就是 F(注意:需要按照顶点的顺序将其插入,比如边 <E, F>,因为 E < F,所以必须 E 作为起点,F 作为终点)。其步骤如下:

比如原来的并查集中都是初始值:
A -> A
B -> B
C -> C
D -> D
E -> E
F -> F
G -> G
最小生成树结果中加入:<E, F>
将并查集中E对应的值改为F
A -> A
B -> B
C -> C
D -> D
E -> F
F -> F
G -> G
最小生成树结果中加入:<C, D>
将并查集中C对应的值改为D
A -> A
B -> B
C -> D
D -> D
E -> F
F -> F
G -> G
最小生成树结果中加入:<D, E>
将并查集中D对应的值改为E
A -> A     A -> A
B -> B     B -> B
C -> D     C -> D
D -> D     D -> E
E -> F     E -> F
F -> F     F -> F
G -> G     G -> G
查询C和D的终点就是F

Kruskal算法实现

图的邻接矩阵实现Kruskal算法

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

#define MAX 100
#define INF (~(0x1 << 31)) //无穷大(0x7FFFFFFF)

const int VEXNUM = 7;

struct EData
{
    char start; //边的起点
    char end;   //边的终点
    int weight; //边的权重

    EData() {}
    EData(char s, char e, int w) : start(s), end(e), weight(w) {}
};

class MatrixUDG
{
private:
    char mVexs[MAX];       //顶点集合,如:{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
    int mVexNum;           //顶点数,如:7
    int mEdgNum;           //边数
    int mMatrix[MAX][MAX]; //邻接矩阵
public:
    //创建图(手动输入)
    MatrixUDG();
    //创建图(用提供的矩阵)
    MatrixUDG(char *vexs, int vNum, int matrix[][VEXNUM]);
    ~MatrixUDG();
    //打印邻接表
    void print();
    //Kruskal最小生成树
    void Kruskal();

private:
    //输入一个合法字母
    char readChar();
    //获得一个字母在顶点数组的下标
    int getPosition(char ch);
    //返回顶点v的第一个邻接顶点的索引,失败返回-1
    int firstVertex(int v);
    //范围顶点v相对于w的下一个邻接顶点的索引,失败返回-1
    //比如: A和C, D, E有连接,则A相对于C的下一个结点为D,即返回D的索引
    int nextVertex(int v, int w);
    //获取图中的边
    EData *getEdge();
    //对边按照权值从小到大进行排序
    void sortEdges(EData *edges, int eNum);
    //获取i的终点
    int getEnd(int *vEnds, int i);
};

MatrixUDG::MatrixUDG()
{
    char c1, c2;
    int p1, p2;
    int i, j;
    int weight;

    cout << "输入顶点数:";
    cin >> mVexNum;
    cout << "输入边数:";
    cin >> mEdgNum;
    if (mVexNum < 1 || mEdgNum < 1 || (mEdgNum > (mVexNum * (mVexNum - 1))))
    {
        cout << "输入有误!" << endl;
        return;
    }
    //初始化顶点
    for (i = 0; i < mVexNum; ++i)
    {
        cout << "vertex(" << i << "):";
        mVexs[i] = readChar();
    }
    //初始化边的权值
    for (i = 0; i < mVexNum; ++i)
    {
        for (j = 0; j < mVexNum; ++j)
        {
            if (i == j)
                mMatrix[i][j] = 0;
            else
                mMatrix[i][j] = INF;
        }
    }
    //初始化边的权值,根据用户输入进行初始化
    for (i = 0; i < mEdgNum; ++i)
    {
        cout << "edge(" << i << "):";
        c1 = readChar();
        c2 = readChar();
        cin >> weight;

        p1 = getPosition(c1);
        p2 = getPosition(c2);
        if (p1 == -1 || p2 == -1)
        {
            cout << "输入的边错误!" << endl;
            return;
        }
        mMatrix[p1][p2] = weight;
        mMatrix[p2][p1] = weight;
    }
}

MatrixUDG::MatrixUDG(char *vexs, int vNum, int matrix[][VEXNUM])
{
    if (!vexs || !*matrix)
        return;
    int i, j;
    mVexNum = vNum;
    //初始化顶点
    for (i = 0; i < mVexNum; ++i)
        mVexs[i] = vexs[i];

    //初始化边
    for (i = 0; i < mVexNum; ++i)
    {
        for (j = 0; j < mVexNum; ++j)
        {
            mMatrix[i][j] = matrix[i][j];
        }
    }

    //统计边的数目
    for (i = 0; i < mVexNum; ++i)
    {
        for (j = 0; j < mVexNum; ++j)
        {
            if (i != j && mMatrix[i][j] != INF)
            {
                ++mEdgNum;
            }
        }
    }
    mEdgNum /= 2;
}

MatrixUDG::~MatrixUDG()
{
}

int MatrixUDG::getPosition(char ch)
{
    for (int i = 0; i < mVexNum; ++i)
    {
        if (mVexs[i] == ch)
            return i;
    }
    return -1;
}

char MatrixUDG::readChar()
{
    char ch;
    do
    {
        cin >> ch;
    } while (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')));
    return ch;
}

/*
返回顶点v的第一个邻接顶点的索引,失败返回-1
*/
int MatrixUDG::firstVertex(int v)
{
    if (v < 0 || v > (mVexNum - 1))
        return -1;
    for (int i = 0; i < mVexNum; ++i)
    {
        if (mMatrix[v][i] != 0 && mMatrix[v][i] != INF)
            return i;
    }
    return -1;
}
/*
返回顶点v相对于w的下一个邻接顶点的索引,失败则返回-1
比如:A和C,D,E有连接,则A相对于C的下一个结点为D,即返回D的索引
*/
int MatrixUDG::nextVertex(int v, int w)
{
    if (v < 0 || v > (mVexNum - 1) || w < 0 || w > (mVexNum - 1))
        return -1;
    for (int i = w + 1; i < mVexNum; ++i)
    {
        if (mMatrix[v][i] != 0 && mMatrix[v][i] != INF)
            return i;
    }
    return -1;
}

//获取图中的边
EData *MatrixUDG::getEdge()
{
    int i, j;
    int index = 0;
    EData *edges = new EData[mEdgNum];
    for (i = 0; i < mVexNum; ++i)
    {
        for (j = i + 1; j < mVexNum; ++j)
        {
            if (mMatrix[i][j] != INF)
            {
                edges[index].start = mVexs[i];
                edges[index].end = mVexs[j];
                edges[index].weight = mMatrix[i][j];
                ++index;
            }
        }
    }
    return edges;
}

//对边按照权值大小进行排序(由小到大)
void MatrixUDG::sortEdges(EData *edges, int eNum)
{
    //直接调用STL排序
    std::sort(edges, edges + eNum, [](EData &edge1, EData &edge2) -> bool
              { return edge1.weight < edge2.weight; });
}

int MatrixUDG::getEnd(int *vEnds, int i)
{
    //相当于并查集的操作
    while (vEnds[i] != 0)
    {
        i = vEnds[i];
    }
    return i;
}

//Kruskal最小生成树算法
void MatrixUDG::Kruskal()
{
    int i;
    int m, n;
    int p1, p2;

    int index = 0;            //rets数组的索引
    int vEnds[MAX] = {0};     //用于保存"已有最小生成树"中每个顶点在该最小生成树中的终点(并查集数组)
    EData rets[MAX];          //结果数组,保存kruskal最小生成树的边
    EData *edges = getEdge(); //图对应的所有边
    //将边按照从小到大的顺序进行排序
    sortEdges(edges, mEdgNum);
    for (i = 0; i < mEdgNum; ++i)
    {
        p1 = getPosition(edges[i].start);
        p2 = getPosition(edges[i].end);
        //并查集操作
        m = getEnd(vEnds, p1); // 获取p1在"已有的最小生成树"中的终点
        n = getEnd(vEnds, p2); // 获取p2在"已有的最小生成树"中的终点
        // 如果m!=n,意味着边i与已经添加到最小生成树中的顶点没有形成环路
        if (m != n)
        {
            vEnds[m] = n;             //设置m在"已有的最小生成树"中的终点为n
            rets[index++] = edges[i]; //保存结果
        }
    }
    delete[] edges;
    //统计并打印"kruskal最小生成树"的信息
    int length = 0;
    for (i = 0; i < index; ++i)
    {
        length += rets[i].weight;
    }
    cout << "Kruskal=" << length << ":";
    for (i = 0; i < index; ++i)
    {
        cout << "(" << rets[i].start << "," << rets[i].end << ")";
    }
    cout << endl;
}

void MatrixUDG::print()
{
    for (int i = 0; i < mVexNum; ++i)
    {
        for (int j = 0; j < mVexNum; ++j)
        {
            if (mMatrix[i][j] == INF)
            {
                cout << "INF"
                     << "\t";
            }
            else
            {
                cout << mMatrix[i][j] << "\t";
            }
        }
        cout << endl;
    }
}

int main()
{
    char vexs[VEXNUM] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
    int vNum = sizeof(vexs) / sizeof(vexs[0]);
    int matrix[][VEXNUM] = {
        {0, 12, INF, INF, INF, 16, 14},
        {12, 0, 10, INF, INF, 7, INF},
        {INF, 10, 0, 3, 5, 6, INF},
        {INF, INF, 3, 0, 4, INF, INF},
        {INF, INF, 5, 4, 0, 2, 8},
        {16, 7, 6, INF, 2, 0, 9},
        {14, INF, INF, INF, 8, 9, 0}};

    // 1. 根据提供的数据生成
    MatrixUDG mudg(vexs, vNum, matrix);
    mudg.print(); //打印图
    mudg.Kruskal();
    //2. 手动生成
    // MatrixUDG mudg1;
    // mudg1.print();
}

运行结果如下:

$ ./test
0       12      INF     INF     INF     16      14
12      0       10      INF     INF     7       INF
INF     10      0       3       5       6       INF
INF     INF     3       0       4       INF     INF
INF     INF     5       4       0       2       8
16      7       6       INF     2       0       9
14      INF     INF     INF     8       9       0
Kruskal=36:(E,F)(C,D)(D,E)(B,F)(E,G)(A,B)

图的邻接表实现Kruskal算法

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

#define MAX 100
#define INF (~(0x1 << 31)) //无穷大(0x7FFFFFFF)

const int VEXNUM = 7;

#define MAX 100
#define INF (~(0x1 << 31)) //最大值

struct EData
{
    char start; //边的起点
    char end;   //边的终点
    int weight; //边的权重

    EData() {}
    EData(char s, char e, int w) : start(s), end(e), weight(w) {}
};

class ListUDG
{
private:
    //每一条边
    struct ENode
    {
        int iVex = 0;           //指向的顶点的位置
        int weight = 0;         //该边的权
        ENode *nextEdge = NULL; //指向顶点的下一条边的指针
    };
    //数组中存储的顶点
    struct VNode
    {
        char data;
        ENode *firstEdge = NULL; //指向第一条该顶点的边
    };

private:
    int mVexNum;      //图的顶点数目
    int mEdgeNum;     //图的边的数目
    VNode mVexs[MAX]; //存储顶点
public:
    //创建邻接表对应的图(自己收入)
    ListUDG();
    //创建邻接表对应的图(用已提供的数据)
    ListUDG(char *vexs, int vNum, EData *edges[], int eNum);
    ~ListUDG();
    //打印邻接表
    void print();
    //Kruskal最小生成树
    void Kruskal();

private:
    //读取一个合法的输入字符
    char readChar();
    //返回字符ch在的位置
    int getPosition(char ch);
    //将node结点链接到list的最后
    void linkLast(ENode *list, ENode *node);
    //获取边<start, end>的权值,若start和end不是连通的,则返回无穷大
    int getWeight(int start, int end);
    //获取图中的边
    EData *getEdges();
    //对边按照权值从小到大进行排序
    void sortEdges(EData *edges, int eNum);
    //获取i的终点
    int getEnd(int *vEnds, int i);
};

ListUDG::ListUDG()
{
    char c1, c2;
    int p1, p2;
    ENode *node1, *node2;
    int weight;

    cout << "输入顶点数:";
    cin >> mVexNum;
    cout << "输入边数:";
    cin >> mEdgeNum;
    if (mVexNum > MAX || mEdgeNum > MAX || mVexNum < 1 || mEdgeNum < 1 || (mEdgeNum > (mVexNum * (mVexNum - 1))))
    {
        cout << "输入有误!" << endl;
        return;
    }
    //初始化邻接表的顶点
    for (int i = 0; i < mVexNum; ++i)
    {
        cout << "vertex(" << i << "):";
        mVexs[i].data = readChar();
        mVexs[i].firstEdge = NULL;
    }
    //初始化邻接表的边
    for (int j = 0; j < mEdgeNum; ++j)
    {
        cout << "edge(" << j << "):";
        c1 = readChar();
        c2 = readChar();
        cin >> weight;

        p1 = getPosition(c1);
        p2 = getPosition(c2);

        if (p1 == -1 || p2 == -1)
        {
            cout << "输入的边有错误!" << endl;
            return;
        }
        //初始化node1
        node1 = new ENode();
        node1->iVex = p2;
        node1->weight = weight;
        //将node1链接到p1所在链表的末尾
        if (mVexs[p1].firstEdge == NULL)
            mVexs[p1].firstEdge = node1;
        else
            linkLast(mVexs[p1].firstEdge, node1);
        //初始化node2
        node2 = new ENode();
        node2->iVex = p1;
        node2->weight = weight;
        //将node2链接到p2所在链表的末尾
        if (mVexs[p2].firstEdge == NULL)
            mVexs[p2].firstEdge = node2;
        else
            linkLast(mVexs[p2].firstEdge, node2);
    }
}

ListUDG::ListUDG(char *vexs, int vNum, EData *edges[], int eNum)
{
    if (vNum > MAX || eNum > MAX)
        return;
    char c1, c2;
    int p1, p2;
    ENode *node1, *node2;
    int weight;
    //初始化顶点数和边数
    mVexNum = vNum;
    mEdgeNum = eNum;
    //初始化邻接表的顶点
    for (int i = 0; i < mVexNum; ++i)
    {
        mVexs[i].data = vexs[i];
        mVexs[i].firstEdge = NULL;
    }
    //初始化邻接表的边
    for (int j = 0; j < mEdgeNum; ++j)
    {
        //读取边的起始顶点和结束顶点
        c1 = edges[j]->start;
        c2 = edges[j]->end;
        weight = edges[j]->weight;

        p1 = getPosition(c1);
        p2 = getPosition(c2);
        if (p1 == -1 || p2 == -1)
        {
            cout << "输入的边有错误!" << endl;
            return;
        }
        //初始化node1
        node1 = new ENode();
        node1->iVex = p2;
        node1->weight = weight;
        //将node1链接到p1所在的链表末尾
        if (mVexs[p1].firstEdge == NULL)
            mVexs[p1].firstEdge = node1;
        else
            linkLast(mVexs[p1].firstEdge, node1);
        //初始化node2
        node2 = new ENode();
        node2->iVex = p1;
        node2->weight = weight;
        //将node2链接到p2所在链表末尾
        if (mVexs[p2].firstEdge == NULL)
            mVexs[p2].firstEdge = node2;
        else
            linkLast(mVexs[p2].firstEdge, node2);
    }
}

ListUDG::~ListUDG()
{
}

void ListUDG::linkLast(ENode *list, ENode *node)
{
    ENode *p = list;
    while (p->nextEdge)
        p = p->nextEdge;
    p->nextEdge = node;
}

int ListUDG::getPosition(char ch)
{
    for (int i = 0; i < mVexNum; ++i)
    {
        if (mVexs[i].data == ch)
            return i;
    }
    return -1;
}

char ListUDG::readChar()
{
    char ch;
    do
    {
        cin >> ch;
    } while (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')));
    return ch;
}

int ListUDG::getWeight(int start, int end)
{
    if (start == end)
        return 0;
    ENode *node = mVexs[start].firstEdge;
    while (node != NULL)
    {
        if (end == node->iVex)
        {
            return node->weight;
        }
        node = node->nextEdge;
    }
    return INF;
}

//获得图中的边
EData *ListUDG::getEdges()
{
    int i;
    int index = 0;
    ENode *node = NULL;
    EData *edges = new EData[mEdgeNum];
    for (i = 0; i < mVexNum; ++i)
    {
        node = mVexs[i].firstEdge;
        while (node != NULL)
        {
            if (node->iVex > i)
            {
                edges[index].start = mVexs[i].data;
                edges[index].end = mVexs[node->iVex].data;
                edges[index].weight = node->weight;
                ++index;
            }
            node = node->nextEdge;
        }
    }
    return edges;
}

//对边按照权值大小进行排序(由小到大)
void ListUDG::sortEdges(EData *edges, int eNum)
{
    if (!edges)
        return;
    //直接调用STL排序
    std::sort(edges, edges + eNum, [](EData &edge1, EData &edge2) -> bool
              { return edge1.weight < edge2.weight; });
}

int ListUDG::getEnd(int *vEnds, int i)
{
    //相当于并查集的操作
    while (vEnds[i] != 0)
    {
        i = vEnds[i];
    }
    return i;
}

//Kruskal最小生成树算法
void ListUDG::Kruskal()
{
    int i;
    int m, n;
    int p1, p2;

    int index = 0;             //rets数组的索引
    int vEnds[MAX] = {0};      //用于保存"已有最小生成树"中每个顶点在该最小生成树中的终点(并查集数组)
    EData rets[MAX];           //结果数组,保存kruskal最小生成树的边
    EData *edges = getEdges(); //图对应的所有边
    //将边按照从小到大的顺序进行排序
    sortEdges(edges, mEdgeNum);
    for (i = 0; i < mEdgeNum; ++i)
    {
        p1 = getPosition(edges[i].start);
        p2 = getPosition(edges[i].end);
        //并查集操作
        m = getEnd(vEnds, p1); // 获取p1在"已有的最小生成树"中的终点
        n = getEnd(vEnds, p2); // 获取p2在"已有的最小生成树"中的终点
        // 如果m!=n,意味着边i与已经添加到最小生成树中的顶点没有形成环路
        if (m != n)
        {
            vEnds[m] = n;             //设置m在"已有的最小生成树"中的终点为n
            rets[index++] = edges[i]; //保存结果
        }
    }
    delete[] edges;
    //统计并打印"kruskal最小生成树"的信息
    int length = 0;
    for (i = 0; i < index; ++i)
    {
        length += rets[i].weight;
    }
    cout << "Kruskal=" << length << ":";
    for (i = 0; i < index; ++i)
    {
        cout << "(" << rets[i].start << "," << rets[i].end << ")";
    }
    cout << endl;
}

void ListUDG::print()
{
    ENode *node;
    for (int i = 0; i < mVexNum; ++i)
    {
        cout << i << "(" << mVexs[i].data << "):";
        node = mVexs[i].firstEdge;
        while (node != NULL)
        {
            cout << node->iVex << "(" << mVexs[node->iVex].data << ")";
            node = node->nextEdge;
        }
        cout << endl;
    }
}

int main(int argc, char **argv)
{
    char vexs[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
    EData *edges[] = {
        new EData('A', 'B', 12),
        new EData('A', 'F', 16),
        new EData('A', 'G', 14),
        new EData('B', 'C', 10),
        new EData('B', 'F', 7),
        new EData('C', 'D', 3),
        new EData('C', 'E', 5),
        new EData('C', 'F', 6),
        new EData('D', 'E', 4),
        new EData('E', 'F', 2),
        new EData('E', 'G', 8),
        new EData('F', 'G', 9),
    };
    int vNum = sizeof(vexs) / sizeof(vexs[0]);
    int eNum = sizeof(edges) / sizeof(edges[0]);
    //1. 根据提供的数据生成
    ListUDG ludg(vexs, vNum, edges, eNum);
    ludg.print();
    ludg.Kruskal();
    //2. 手动输入
    // ListUDG ludg;
    // ludg.print();
    return 0;
}

运行结果如下:

$ ./test
0(A):1(B)5(F)6(G)
1(B):0(A)2(C)5(F)
2(C):1(B)3(D)4(E)5(F)
3(D):2(C)4(E)
4(E):2(C)3(D)5(F)6(G)
5(F):0(A)1(B)2(C)4(E)6(G)
6(G):0(A)4(E)5(F)
Kruskal=36:(E,F)(C,D)(D,E)(B,F)(E,G)(A,B)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值