图的最小生成树之Prim算法

参考:https://wangkuiwu.github.io/2013/04/13/prim-cplus

Prim算法介绍

在介绍普里姆 Prim 算法前,首先需要知道 Prim 算法的作用。Prim 算法是用来求加权连通图的最小生成树的算法。下面先介绍最小生成树。

最小生成树概念

在含有 n 个顶点的连通图中选择 n -1 条边,构成一棵极小连通子图,并使该连通子图中 n -1 条边上权值之和达到最小,则称其为连通图的最小生成树

Prim算法-图1

图 G4

如上连通图 G4 所示,可以有多棵权值总和不相同的生成树。

情况 1:权值总和为 43。

Prim算法-图2

情况 2:权值总和为 36。

Prim算法-图3

情况 3:权值总和为 64。

Prim算法-图4

Prim算法思想

普利姆 算法思想如下:

对于连通图 G 而言,V 是其所有的顶点的集合。现在,设置两个新的集合 U 和 T,其中 U 用于存放 G 的最小生成树中的顶点,T 存放 G 的最小生成树中的边。从所有 uЄUvЄ(V-U)(V-U 表示除去 U 的所有顶点)的边中选取权值最小的边 (u, v),将顶点 v 加入集合 U 中,将边 (u, v) 加入集合 T 中,如此不断重复,直到 U = V 为止,最小生成树构造完毕,这时集合 T 中包含了最小生成树中的所有边。

Prim算法图解

以上面的加权连通图 G4 为例,对 Prim 算法进行演示(从第一个顶点 A 开始通过 Prim 算法生成最小生成树)。

Prim算法-图5

步骤说明:

  • 初始状态:V 是所有顶点的集合,即V = {A, B, C, D, E, F, G},U 和 T 都是空。
  • 第1步:将顶点 A 加入到 U 中。
    此时,U = {A}。
  • 第2步:将顶点 B 加入到 U 中。
    上一步操作之后, U = {A},V - U = {B, C, D, E, F, G}。因此,边 (A, B) 的权值最小。将顶点 B 添加到 U 中。此时,U = {A, B}。
  • 第3步:将顶点 F 加入到 U 中。
    上一步操作之后,U = {A, B},V - U = {C, D, E, F, G}。因此,边 (B, F) 的权值最小。将顶点 F 添加到 U 中。此时,U = {A, B, F}。
  • 第4步:将顶点 E 加入到 U 中。
    上一步操作之后,U = {A, B, F},V - U = {C, D, E, G}。因此,边 (F, E) 的权值最小。将顶点 E 添加到 U 中。此时,U = {A, B, F, E}。
  • 第5步:将顶点 D 加入到 U 中。
    上一步操作之后,U = {A, B, F, E},V - U = {C, D, G}。因此,边 (E, D) 的权值最小。将顶点 D 添加到 U 中。此时,U = {A, B, F, E, D}。
  • 第6步:将顶点 C 加入到 U 中。
    上一步操作之后,U = {A, B, F, E, D},V - U = {C, G}。因此,边 (D, C) 的权值最小。将顶点 C 添加到 U 中。此时,U = {A, B, F, E, D, C}。
  • 第7步:将顶点 G 加入到 U 中。
    上一步操作之后,U = {A, B, F, E, D, C},V - U = {G}。因此,边 (F, G) 的权值最小。将顶点 G 添加到 U 中。此时,U = V。

此时,最小生成树构造完成,它包括的顶点依次是:A, B, F, E, D, C, G

Prim算法的实现

图的邻接矩阵实现Prim算法

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

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

const int VEXNUM = 7;

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();
    //prim最小生成树算法
    void Prim(int start);

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);
};

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;
}

//Prim最小生成树算法
void MatrixUDG::Prim(int start)
{
    int min;
    int i, j;
    int n, m;
    int min_index = 0;
    int prim_index = 0; //prim最小生成树的索引,即prims数组的索引
    char prims[MAX];    //prim最小生成树的结果数组
    int weights[MAX];   //存储顶点间边的权值
    //prim最小生成树中第一个数是图中第start个顶点,因为是从start开始的
    prims[prim_index++] = mVexs[start];
    //初始化顶点的权值数组
    //将每个顶点的权值初始化为第start个顶点到该顶点的权值
    for (i = 0; i < mVexNum; ++i)
    {
        weights[i] = mMatrix[start][i];
    }
    //将第start个顶点到它自身的距离为0
    weights[start] = 0;
    for (i = 0; i < mVexNum; ++i)
    {
        //由于从start开始的,因此不需要再对第start个顶点进行处理
        if (i == start)
            continue;
        j = 0;
        min_index = 0; //标记最小权值下标
        min = INF;
        //在未被加入到最小生成树中的顶点中,找出权值最小的顶点
        while (j < mVexNum)
        {
            //若weights[j] = 0,说明第j个节点已经加入了最小生成树
            if (weights[j] != 0 && weights[j] < min)
            {
                min = weights[j];
                min_index = j; //找到最小权值的顶点下标
            }
            ++j;
        }
        //经过上面的处理后,在未被加入到最小生成树的顶点中,权值最小的顶点是第min_index个顶点
        //将第min_index个顶点加入到最下生成树的结果数组中
        prims[prim_index++] = mVexs[min_index];
        weights[min_index] = 0;
        //当第min_index个顶点被加入最小生成树的结果数组中,更新其他顶点的权值
        //比如从点A开始,B是权值最端的路径,
        //此时weight[0, 0, INF, INF, INF, 16, 14]
        //现在需要遍历B(第min_index个顶点)的邻接点,找到与B连接的最短路径,
        //此时weight[0, 0, 10, INF, INF, 7, INF],
        //下次循环再得出F是权值最短的路径,此时weight[0, 0, 10, INF, INF, 0, INF]
        //现在需要遍历F(第min_index个顶点)的邻接点,找到F连接的最端路径,
        //此时weight[0, 0, 6, INF, 2, 0, 9]
        //下次循环再得出E是权值最短的路径,...
        for (j = 0; j < mVexNum; ++j)
        {
            //当第j个节点没有被处理,并且需要更新时才被处理
            if (weights[j] != 0 && mMatrix[min_index][j] < weights[j])
            {
                weights[j] = mMatrix[min_index][j];
            }
        }
    }
    //计算最小生成树的权值
    int sum = 0;
    for (i = 1; i < prim_index; ++i)
    {
        min = INF;
        //获取prims[i]在顶点中的下标位置
        n = getPosition(prims[i]);
        //在vexs[0...i]中,找出到j的权值最小的顶点
        //比如[A, B, F, E, D, C, G]
        //n=1 B时, j=0; prims[0]=A=12; min = 12
        //n=2 F时, j=0, 1; prims[0]=A=INF, prims[1]=B=7; min=7
        //n=3 E时, j=0, 1, 2; prims[0]=A=INF, prims[1]=B=INF, prims[2]=F=2; min=2
        //...
        for (j = 0; j < i; ++j)
        {
            m = getPosition(prims[j]);
            if (mMatrix[m][n] < min)
            {
                min = mMatrix[m][n];
            }
        }
        sum += min;
    }
    //打印最小生成树
    cout << "Prim:(" << mVexs[start] << ")=" << sum << ":";
    for (i = 0; i < prim_index; ++i)
    {
        cout << prims[i] << " ";
    }
    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.Prim(0);
    //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
Prim:(A)=36:A B F E D C G 

图的邻接表实现Prim算法

#include <iostream>
using namespace std;

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

//边的权重结构体,用来初始化构建邻接表(演示)
struct EData
{
    EData() {}
    EData(char s, char e, int w) : start(s), end(e), weight(w) {}

    char start; //边的起点
    char end;   //边的终点
    int weight; //边的权重
};

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();
    //Prim最小生成树
    void Prim(int start);

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);
};

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;
}

//Prim最小生成树
void ListUDG::Prim(int start)
{
    int i, j;
    int m, n;
    int min = 0;
    int min_index = 0;
    int prim_index = 0; //prim最小生成树的索引,即prims数组的索引
    char prims[MAX];    //prim最小生成树的结果数组
    int weights[MAX];   //顶点间边的权值

    //prim最小生成树中第一个数是图中第start个顶点,因为从start开始
    prims[prim_index++] = mVexs[start].data;
    //初始化顶点权值数组,将每个顶点的权值初始化为第start个顶点,因为是从start开始的
    for (i = 0; i < mVexNum; ++i)
    {
        weights[i] = getWeight(start, i);
    }
    for (i = 0; i < mVexNum; ++i)
    {
        //由于从start开始的,因此不需要再对第start个顶点进行处理
        if (start == i)
            continue;
        j = 0;
        min_index = 0;
        min = INF;
        //在未被加入到最小生成树的顶点中,找出权值最小的顶点
        while (j < mVexNum)
        {
            //若weights[j] = 0,说明第j个节点已经加入了最小生成树
            if (weights[j] != 0 && weights[j] < min)
            {
                min = weights[j];
                min_index = j;
            }
            ++j;
        }
        //经过上面的处理后,在未被加入到最小生成树的顶点中,权值最小的顶点是第min_index个顶点
        //将第min_index个顶点加入到最小生成树的结果数组中
        prims[prim_index++] = mVexs[min_index].data;
        //将第min_index个顶点的权值标记为0,意味着第min_index个节点已经加入到最小生成树的结果数组中
        weights[min_index] = 0;
        //当第min_index个顶点被加入最小生成树的结果数组中,更新其他顶点的权值
        //比如从点A开始,B是权值最端的路径,
        //此时weights[0, 0, INF, INF, INF, 16, 14]
        //现在需要遍历B(第min_index个顶点)的邻接点,找到与B连接的最短路径,
        //此时weights[0, 0, 10, INF, INF, 7, INF],
        //下次循环再得出F是权值最短的路径,此时weights[0, 0, 10, INF, INF, 0, INF]
        //现在需要遍历F(第min_index个顶点)的邻接点,找到F连接的最端路径,
        //此时weights[0, 0, 6, INF, 2, 0, 9]
        //下次循环再得出E是权值最短的路径,...
        int tmp = 0;
        for (j = 0; j < mVexNum; ++j)
        {
            tmp = getWeight(min_index, j);
            //当第j个节点没有被处理,并且需要更新时才被处理
            if (weights[j] != 0 && tmp < weights[j])
            {
                weights[j] = tmp;
            }
        }
    }
    //计算最小生成树的权值
    int sum = 0;
    int tmp = 0;
    for (i = 1; i < prim_index; ++i)
    {
        min = INF;
        //获取prims[i]在矩阵中的位置
        n = getPosition(prims[i]);
        //在vexs[0...i]中,找出到j的权值最小的顶点
        //比如[A, B, F, E, D, C, G]
        //n=1 B时, j=0; prims[0]=A=12; min = 12
        //n=2 F时, j=0, 1; prims[0]=A=INF, prims[1]=B=7; min=7
        //n=3 E时, j=0, 1, 2; prims[0]=A=INF, prims[1]=B=INF, prims[2]=F=2; min=2
        //...
        for (j = 0; j < i; ++j)
        {
            m = getPosition(prims[j]);
            tmp = getWeight(m, n);
            if (tmp < min)
            {
                min = tmp;
            }
        }
        sum += min;
    }
    //打印最小生成树
    cout << "Prim(" << mVexs[start].data << ")=" << sum << ":";
    for (i = 0; i < prim_index; ++i)
    {
        cout << prims[i] << " ";
    }
    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.Prim(0);
    //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)
Prim(A)=36:A B F E D C G
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值