图操作集锦(概念,存储,遍历,最小生成树,最短路径,拓扑排序)

图基础全解(概念,存储,遍历,最小生成树,最短路径,拓扑排序)

一、 图

1.1 图的基础概念

1、图的定义
 图G由顶点集V和边集E组成,记为G=(V,E),V不能为空,E可以为空,顶点的个数,也叫做图G的阶。
2、有向图
 当E是有向边(弧)的有限集合时,图G为有向图,弧是顶点的有序对,记为<v,w>,v和w是顶点,弧从v(弧尾)指向w(弧头)
3、无向图
 当E是无向边的有限集合时,图G为无向图。边(v,w),既可以从v到w,也可以从w到v
4、简单图(数据结构仅讨论简单图)与多重图相对
 ①不存在重复边
 ②不存在顶点到自身的边
5、完全图(任意两点之间均存在边)
 对于无向图,存在n*(n-1)/2条边,称为无向完全图
 对于有向图,存在n*(n-1)条弧,称为有向完全图
6、子图
 图G1的顶点和边都包含于图G2,则称G1是G2的子图
7、连通,连通图和连通分量(无向图)
 在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。
 若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。
 无向图中的极大连通子图称为连通分量
 若一个图中有n个顶点,并且边数小于n-1,则此图必是非连通图
8、强连通图,强连通分量(有向图)
 有向图中,若从顶点v到w和从w到v都有路径,则称这两个顶点是强连通的,则称此图为强连通图。
 有向图中的极大强连通子图称为有向图的强连通分量。
9、顶点的度
 顶点的度,以该顶点为一个端点的边的数目
 无向图,全部顶点的度的和等于边数的2倍,因为每条边和两个顶点相连
 有向图,顶点的v的度分为出度+入度,入度是指向顶点v的弧的数目,出度是以顶点v为起点的弧的数目。有向图的全部顶点的入度和出度之和相等,并且等于边数。因为每个有向边都有一个起点和终点
10、边的权和网
 图中每条边都可以标上具有某种含义的数值,称为该边的权值。这种边上带有权值的图为带全图,也称网
11、稠密图和稀疏图
 两者是相对而言的,一般当图G满足 |E|<|V|log|V| ,可以视作稀疏图
12、路径,路径长度,回路
 顶点v到顶点p之间的一条路径是指顶点序列,当然关联的边也可以理解为路径的构成要素。路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路或环。
 若一个图有n个顶点,并且有大于n-1条边,则此图一定有环。
13、简单路径,简单回路
 在路径序列中,顶点不重复出现的路径称为简单路径。除第一个顶点和最后一个顶点外,其他顶点不重复出现的回路称为简单回路

1.2 图的存储与遍历

1.2.1 邻接矩阵

矩阵A如下:
A = ( 0 1 0 1 1 0 1 0 0 1 0 1 1 0 1 0 ) A=\begin{gathered} \begin{pmatrix} 0&1&0& 1 \\ 1&0&1& 0\\ 0&1&0& 1\\ 1&0&1& 0 \end{pmatrix} \end{gathered} A=0101101001011010
 A[i][j]=1,代表顶点 i 与顶点 j 之间有路径,<i , j>是E中的一条边,反之则为0
无向图的邻接矩阵一般为对称矩阵,第i行非0元素的个数,正好是第i个顶点的度

有向带权图的邻接矩阵
A = ( ∞ 5 ∞ 3 ∞ ∞ 8 ∞ 4 3 ∞ 6 8 ∞ 9 ∞ ) A=\begin{gathered} \begin{pmatrix} ∞&5&∞& 3\\ ∞&∞&8&∞\\ 4&3&∞& 6\\ 8&∞&9& ∞ \end{pmatrix} \end{gathered} A=48538936
 对于有向带权图,邻接矩阵中,如果两个顶点i和j之间有边,则A[i][j]中存放相应权值,否则就是无穷大(当然在数组中存储时,不可能是真的存储无穷大,而是一个远大于所有权值的一个数)
 第i行非0或非∞的元素的个数是该顶点 i 的出度,第i列非0或非∞的元素的个数是该顶点 i 的入度

图的邻接矩阵存储定义如下

#define MAX_SIZE 100
#define INF 999999
typedef struct{
    int edge[MAX_SIZE][MAX_SIZE];
    int vexnum,arcnum;
}MGraph;
1.2.1.1 图的邻接矩阵创建
//创建邻接矩阵
void CreateMGraph(MGraph *G){
    int i,j,k,w;
    printf("input the number of the vertex and the arc:");
    scanf("%d,%d",&G->vexnum,&G->arcnum);
    for(i = 0;i < G->vexnum;i++){
        for(j = 0;j < G->vexnum;j++){
            G->edge[i][j] = INF;
        }
    }
    for(k = 0;k < G->arcnum;k++){
        printf("input the arc:");
        scanf("%d %d %d",&i,&j,&w);
        G->edge[i][j] = w;
        G->edge[j][i] = w;
    }
}
1.2.1.2 图基于邻接矩阵的广度优先遍历
//BFS
void BFS(MGraph G,int v){
    int visited[MAX_SIZE];     //标记数组
    int queue[MAX_SIZE];        //队列
    int front = 0,rear = 0;     //队列头尾指针
    int i;
    for(i = 0;i < G.vexnum;i++){
        visited[i] = 0;
    }
    int t = 1;
    printf("%d ",v);        //访问第一个顶点
    visited[v] = 1;                 //标记为已访问
    queue[rear++] = v;
    while(front != rear){
        v = queue[front++];
        for(i = 0;i < G.vexnum;i++){
            if(G.edge[v][i] != INF && visited[i] == 0){
                printf("%d ",i);
                visited[i] = 1;
                queue[rear++] = i;
                t++;
            }
        }
    }
    if(t < G.vexnum){               //如果t小于顶点数,说明图不连通
        printf("the graph is not connected!");
    }
}
1.2.1.3 图基于邻接矩阵的深度优先遍历
//DFS
int visited[MAX_SIZE];
int i;
for(i = 0;i < G.vexnum;i++){
        visited[i] = 0;
}
void DFS(MGraph G,int v){
    int i;
    printf("%d ",v);
    visited[v] = 1;
    for(i = 0;i < G.vexnum;i++){
        if(G.edge[v][i] != INF && visited[i] == 0){
            DFS(G,i);
        }
    }
}

1.2.2 邻接表

当一个图为稀疏图时,使用邻接矩阵法显然要浪费大量的存储空间,而图的邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费。
 所谓邻接表,是指对图G中的每个顶点v建立一个单链表,第i个单链表中的结点表示依附于顶点v的边(对于有向图则是以顶点v为尾的弧),这个单链表就称为顶点v的边表(对于有向图则称为出边表)。 
 边表的头指针和顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种结点:顶点表结点和边表结点
顶点结点:
顶点域 ↓                  边表头指针 ↓

datafirstarc

边表结点
邻接点域 ↓                  指针域 ↓

adjvexnextarc

无向图邻接表

无向图邻接表

在这里插入图片描述

有向图邻接表
邻接表结点定义
#define MAX_SIZE 100
typedef struct Arcnode{
    int adjvex;
    struct Arcnode *next;
}Arcnode,*ArcnodePtr;

typedef struct Vnode{
    int data;
    ArcnodePtr firstarc;
}Vnode,AdjList[MAX_SIZE];

typedef struct{
    AdjList vertices;
    int vexnum,arcnum;
}ALGraph;

①图的邻接表不是唯一的,因为临边的次序可以改变
②无向图G的邻接表存储空间为O(|V|+2|E|),每条边出现两次
 有向图G的邻接表存储空间为O(|V|+|E|)
③对于稀疏图,邻接表可以极大的节省空间
3、十字链表(有机会再补)
4、邻接多重表(有机会再补)

1.2.2.1 图的邻接表的创建
//创建邻接表
void CreateALGraph(ALGraph *G){
    int i,j,k;
    ArcnodePtr p;
    printf("input the number of the vertex and the arc:");
    scanf("%d,%d",&G->vexnum,&G->arcnum);
    for(i = 0;i < G->vexnum;i++){
        printf("input the data of the vertex:");
        scanf("%d",&G->vertices[i].data);
        G->vertices[i].firstarc = NULL;
    }
    for(k = 0;k < G->arcnum;k++){
        printf("input the arc:");
        scanf("%d %d",&i,&j);
        p = (ArcnodePtr)malloc(sizeof(Arcnode));
        p->adjvex = j;
        p->next = G->vertices[i].firstarc;
        G->vertices[i].firstarc = p;

        p = (ArcnodePtr)malloc(sizeof(Arcnode));
        p->adjvex = i;
        p->next = G->vertices[j].firstarc;
        G->vertices[j].firstarc = p;
    }
}
1.2.2.2 图基于邻接表的广度优先遍历
//BFS
void BFS(ALGraph G,int v){
    int visited[MAX_SIZE];     //标记数组
    int queue[MAX_SIZE];        //队列
    int front = 0,rear = 0;     //队列头尾指针
    int i;
    for(i = 0;i < G.vexnum;i++){
        visited[i] = 0;
    }
    int t = 1;
    printf("%d ",v);        //访问第一个顶点
    visited[v] = 1;                 //标记为已访问
    queue[rear++] = v;
    while(front != rear){
        v = queue[front++];
        ArcnodePtr p = G.vertices[v].firstarc;
        while(p != NULL){
            if(visited[p->adjvex] == 0){
                printf("%d ",p->adjvex);
                visited[p->adjvex] = 1;
                queue[rear++] = p->adjvex;
            }
            p = p->next;
        }
    }
}
1.2.2.3 图基于邻接表的深度优先遍历
//DFS
int visited[MAX_SIZE];
    int i;
for(i = 0;i < G.vexnum;i++){
     visited[i] = 0;
}
void DFS(ALGraph G,int v){
    printf("%d ",v);
    visited[v] = 1;
    ArcnodePtr p = G.vertices[v].firstarc;
    while(p != NULL){
        if(visited[p->adjvex] == 0){
            DFS(G,p->adjvex);
        }
        p = p->next;
    }
}

1.2.3 图的遍历(DFS与BFS)

图的遍历是指从图中的某一顶点出发,按照某种搜索方法沿着图中的边对图中的所有顶点访
问一次且仅访问一次。注意到树是一种特殊的图,所以树的遍历实际上也可视为一种特殊的图的
遍历。图的遍历算法是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。

1.2.3.1 广度优先搜索(BFS)

类似于二叉树的层次遍历,需要用到队列进行辅助,算法的执行过程大概概括如下
①访问起始顶点v,将v入队,并标记已经访问
②当队列不为空时,循环执行:出队,一次检查出队顶点的所有邻接顶点,访问没有被访问过的邻接顶点并将其入队
③当队列为空时,跳出循环,广度优先搜索完成

BFS性能:
邻接表情况下,n个顶点均需入队一次,最坏的情况下,空间复杂度O(|V|),每个顶点均需搜索一次,每条边至少访问一次,所以总时间复杂度O(|V|+|E|)
邻接矩阵情况下,n个顶点均需入队一次,最坏的情况下,空间复杂度O(|V|),时间复杂度O(|V|2)

1.2.3.2 深度优先搜索(DFS)

DFS性能分析:
递归算法,需要借助空间栈,空间复杂度O(|V|);
使用邻接矩阵时的时间复杂度O(|V|2
使用邻接表的时间复杂度O(|V|+|E|)

1.3 最小生成树

一个连通图的生成树,包含图的所有顶点,并且只含尽可能少的边。对于生成树来说。砍去一条边,则会使生成树变成非连通图,增加一条边,则会形成图中的一条回路
所有生成树权值之和最小的那棵称为最小生成树
①当图中权值互不相等时,最小生成树唯一;若无向连通图G的边数比顶点数少1,即G本身是一棵树的时候,最小生成树是G本身
②虽然最小生成树不唯一,但权值之和唯一,是最小的
③最小生成树的边数为顶点数减1

1.3.1 普里姆算法(prim)

执行过程如下
1)将V0到其他顶点的所有边当做后选边
2)重复以下步骤n-1遍,使得其他n-1个顶点被并入到树中
①从候选边中挑出权值最小的边输出,并将另一端相邻的顶点v并入树中
②检查所有剩余顶点vi,如果(v,vi)的权值小于lowcost[vi],则用(v,vi)的权值更新lowcost[vi];

//普利姆算法
void Prim(MGraph G){
    int lowcost[MAX_SIZE];      //最小权值
    int adjvex[MAX_SIZE];       //最小权值对应的顶点
    int i,j,k,min;
    int sum = 0;
    adjvex[0] = 0;
    lowcost[0] = 0;
    for(i = 1;i < G.vexnum;i++){
        lowcost[i] = G.edge[0][i];
        adjvex[i] = 0;
    }
    for(i = 1;i < G.vexnum;i++){
        min = INF;
        j = 1;k = 0;
        while(j < G.vexnum){
            if(lowcost[j] != 0 && lowcost[j] < min){
                min = lowcost[j];
                k = j;
            }
            j++;
        }
        printf("(%d,%d)",adjvex[k],k);
        sum += G.edge[adjvex[k]][k];
        lowcost[k] = 0;
        for(j = 1;j < G.vexnum;j++){
            if(lowcost[j] != 0 && G.edge[k][j] < lowcost[j]){
                lowcost[j] = G.edge[k][j];
                adjvex[j] = k;
            }
        }
    }
    printf("the sum of the weight is %d",sum);
}

Prim时间复杂度分析,在邻接矩阵存储结构下,为O(n2)

1.3.2 克鲁斯卡尔算法(Kruskal)

执行过程:
1)对所有边的按照权值大小进行排序,从最小边进行扫描,并检测并入当前边是否构成回路,如不构成回路,将该边并入生成树
2)检查是否产生回路,用到并查集,检查该边的两个顶点是否属于同一棵树,即是否具有相同的根

//克鲁斯卡尔算法
typedef struct{     //边集数组
    int begin;
    int end;
    int weight;
}Edge;
void swap(Edge *a,Edge *b){     //交换函数
    Edge temp = *a;
    *a = *b;
    *b = temp;
}
void sort(Edge edges[],int n){      //冒泡排序
    int i,j;
    for(i = 0;i < n;i++){
        for(j = i+1;j < n;j++){
            if(edges[i].weight > edges[j].weight){
                swap(&edges[i],&edges[j]);
            }
        }
    }
}
int Find(int *parent,int f){        //查找连通分量
    while(parent[f] > 0){
        f = parent[f];
    }
    return f;
}
void Kruskal(MGraph G){     //克鲁斯卡尔算法
    int parent[MAX_SIZE];
    int i,j,n,sum=0;
    Edge edges[MAX_SIZE];
    n = 0;
    for(i = 0;i < G.vexnum;i++){            //构造边集数组
        for(j = i+1;j < G.vexnum;j++){
            if(G.edge[i][j] != INF){
                edges[n].begin = i;
                edges[n].end = j;
                edges[n].weight = G.edge[i][j];
                n++;
            }
        }
    }
    sort(edges,n);                      //对边集数组排序
    for(i = 0;i < G.vexnum;i++){        //初始化连通分量
        parent[i] = 0;
    }
    for(i = 0;i < n;i++){               //循环每一条边
        int a = Find(parent,edges[i].begin);
        int b = Find(parent,edges[i].end);
        if(a != b){
            parent[a] = b;
            printf("(%d,%d)",edges[i].begin,edges[i].end);
            sum += edges[i].weight;
        }
    }
    printf("the sum of the weight is %d",sum);
}

1.4 最短路径

1.4.1 迪杰斯特拉算法(dijkstra)(单源最短路径)

引入三个数组:
dist[vi]:表示当前已找到的从v0到每个终点vi的最短路径的长度。初始化为,v0到vi有边,则dist[vi]为权值,否则为∞(一个远大于各边权值的数)
path[vi]:保存从v0到vi最短路径上的vi的前一个顶点。初始化为,v0到vi有边,path[vi]=v0,否则为-1;
set[vi]:标记数组,0表示vi没有并入最短路径,1表示已经并入。初始化为,set[v0]=1,其余为0;
执行过程:
1)从当前dist[ ]选出最小值,假设为vk,将set[vk]=1,表示当前新并入的顶点为vk
2)循环扫描图中顶点,检测是否并入,如果vj没有,则比较dist[vj]与dist[vk]+w,w为<vk,vj>的权值。意思是比较旧的最短路径与经过vk的新的最短路径的到达vj哪个小,如果新的小,则更新dist[vj],且path[vj]=vk;
3)循环执行n-1次(n为图中顶点个数),即可得到v0到其他所有顶点的最短路径

#define MAX_SIZE 100
#define INF 999999
typedef struct{
    int edge[MAX_SIZE][MAX_SIZE];
    int vexnum,arcnum;
}MGraph;
//创建邻接矩阵
void CreateMGraph(MGraph *G){
    int i,j,k,w;
    printf("input the number of the vertex and the arc:");
    scanf("%d %d",&G->vexnum,&G->arcnum);
    for(i = 0;i < G->vexnum;i++){
        for(j = 0;j < G->vexnum;j++){
            G->edge[i][j] = INF;
        }
    }
    for(k = 0;k < G->arcnum;k++){
        printf("input the arc:");
        scanf("%d %d %d",&i,&j,&w);
        G->edge[i][j] = w;
    }
}
void Dijkstra(MGraph *g,int v,int dist[],int path[])
{
    int set[MAX_SIZE];
    int min,k;
    for(int i=0;i<g->vexnum;i++)
    {
        dist[i]=g->edge[v][i];				//初始化dist[ ]数组
        set[i]=0;							//初始化set数组
        if(g->edge[v][i]<INF)				//初始化path数组
            path[i]=v;
        else
            path[i]=-1;
    }
    set[v]=1;									//标记已经并入最短路径
    path[v]=-1;									//没有前驱
    for(int i=0;i<g->vexnum;i++)
    {
        min = INF;
        for(int j=0;j<g->vexnum;j++)
        {
            if(set[j]==0&&dist[j]<min)			//找出侯选边(邻接边)中权值最小的
            {
                min = dist[j];
                k=j;
            }
        }
        set[k]=1;								//并入最短路径
        for(int j=0;j<g->vexnum;j++)
        {
            if(set[j]==0&&dist[k]+g->edge[k][j]<dist[j])	//如果k作为中间顶点
            {												//是否产生新的最短路径
                dist[j]=dist[k]+g->edge[k][j];				//如果有,更新dist数组,path数组
                path[j]=k;
            }
        }
    }
}
void print(MGraph *g,int path[],int dist[])
{
    for(int i=1;i<g->vexnum;i++)				//循环打印0到其他所有顶点的最短路径
    {
        int j=i;
        int stack[MAX_SIZE],top=-1;				//利用栈进行顺序打印
        while(path[j]!=-1)
        {
            stack[++top]=j;
            j=path[j];
        }
        stack[++top]=j;
        while(top!=0)
        {
            printf("%d-->",stack[top--]);
        }
        printf("%d\t\t",stack[top--]);
        printf("dist=%d",dist[i]);
        printf("\n");
    }
}
int main()
{
    int dist[MAX_SIZE],path[MAX_SIZE];
    MGraph *g = (MGraph *)malloc(sizeof(MGraph));
    CreateMGraph(g);
    Dijkstra(g,0,dist,path);
    print(g,path,dist);
}

运行截图:
在这里插入图片描述

1.4.2 弗洛伊德(Floyd)(一对顶点间的最短路径)

执行过程:引进矩阵A和矩阵Path
1)将图的邻接矩阵赋值给A,将矩阵Path中的元素全部设置为-1;
2)以顶点k为中间顶点,k取0~n-1,对图中所有的顶点对(i,j)进行如下检测
如果A[i][j]>A[i][k]+A[k][j],则更新A[i][j]的值,将Path[i][j]设置为k

 #define MAX_SIZE 100
#define INF 999999
typedef struct{
    int edge[MAX_SIZE][MAX_SIZE];
    int vexnum,arcnum;
}MGraph;
//创建邻接矩阵
void CreateMGraph(MGraph *G){
    int i,j,k,w;
    printf("input the number of the vertex and the arc:");
    scanf("%d %d",&G->vexnum,&G->arcnum);
    for(i = 0;i < G->vexnum;i++){
        for(j = 0;j < G->vexnum;j++){
            G->edge[i][j] = INF;
        }
    }
    for(k = 0;k < G->arcnum;k++){
        printf("input the arc:");
        scanf("%d %d %d",&i,&j,&w);
        G->edge[i][j] = w;
    }
}
//弗洛伊德算法
void Floyd(MGraph *g,int dist[][MAX_SIZE],int path[][MAX_SIZE])
{
    int i,j,k;
    for(i=0;i<g->vexnum;i++)
    {
        for(j=0;j<g->vexnum;j++)
        {
            dist[i][j]=g->edge[i][j];
            path[i][j]=-1;
        }
    }
    for(k=0;k<g->vexnum;k++)
    {
        for(i=0;i<g->vexnum;i++)
        {
            for(j=0;j<g->vexnum;j++)
            {
                if(dist[i][k]+dist[k][j]<dist[i][j])
                {
                    dist[i][j]=dist[i][k]+dist[k][j];
                    path[i][j]=k;
                }
            }
        }
    }
}
//打印路径
void print(int path[][MAX_SIZE],int i,int j)
{
    if(path[i][j]==-1)
    {
        printf("%d->%d",i,j);
    }
    else
    {
        print(path,i,path[i][j]);
        printf("->%d",j);
    }
}
int main()
{
    MGraph g;
    int dist[MAX_SIZE][MAX_SIZE];
    int path[MAX_SIZE][MAX_SIZE];
    CreateMGraph(&g);
    Floyd(&g,dist,path);
    int i,j;
    for(i=0;i<g.vexnum;i++)
    {
        for(j=0;j<g.vexnum;j++)
        {
            printf("%d ",dist[i][j]);
        }
        printf("\n");
    }
    for(i=0;i<g.vexnum;i++)
    {
        for(j=0;j<g.vexnum;j++)
        {
            print(path,i,j);
            printf("\n");
        }
    }
    return 0;
}

1.5 拓扑排序

拓扑排序不唯一,当过程中存在多个入度为0的点时,取决于邻接表的输入顺序

#include<iostream>
#define MaxNum 10
using namespace std;
typedef struct ArcNode
{
	int adjvex;
	struct ArcNode *nextarc;
	int info;
}ArcNode;
typedef struct VNode
{
	int data;
	int count;
	ArcNode *firstarc;
}VNode,AdjList[MaxNum];
typedef struct
{
	AdjList vextices;
	int vexnum,arcnum;
}ALGraph;
void CreatALGraph(ALGraph *g)
{
    int v1,v2;
    int l,m;
    ArcNode *p;
    cin>>g->vexnum>>g->arcnum;						//顶点数,边数
    for(int i=0;i<g->vexnum;i++)
    {
        g->vextices[i].data=i;
        cin>>g->vextices[i].count;					//输入顶点的入度
        g->vextices[i].firstarc=NULL;
    }
    for(int i=0;i<g->arcnum;i++)
    {
        cin>>v1>>v2;								//先输入弧尾顶点,再输入弧头顶点
        p = new ArcNode();
        p->adjvex = v2;
        p->nextarc = g->vextices[v1].firstarc;		//头插法建立邻接表
        g->vextices[v1].firstarc = p;
    }
}

int TopSort(ALGraph *g)
{
    int stack[MaxNum],top=-1;
    int k,j,n=0;
    ArcNode *p;
    for(int i=0;i<g->vexnum;i++)
    {
        if(g->vextices[i].count==0)					//查找入度为0的点入栈
            stack[++top]=i;
    }
    while(top!=-1)
    {
        k=stack[top--];
        n++;										//统计当前顶点
        cout<<k<<" ";
        p=new ArcNode();
        p=g->vextices[k].firstarc;					//查找邻边
        while(p!=NULL)
        {
            j=p->adjvex;							//将临边的指向的顶点的入度减1
            (g->vextices[j].count)--;
            if(g->vextices[j].count==0)				//为0则入栈
                stack[++top]=j;
            p=p->nextarc;							//去下一条临边
        }
    }
    if(n==g->vexnum)								//计算器等于顶点数,则排序成功
        return 1;
    else
        return 0;
}
int main()
{
    ALGraph *g = new ALGraph();
    CreatALGraph(g);
    int k=TopSort(g);
    cout<<endl<<k;
}

结束语

写了两整天,算是自己复习的一个过程,查漏补缺,尽量在写的过程中,将自己曾经有过的困惑表达清楚,就这样。

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值