数据结构——图

1. 图 Graph

  • 非线性结构
  • 结点与结点之间关系是任意的

图的形式描述   V:vertex   E:edge
    G = (V, E)      
 
    V是顶点的集合
图是没有空图的  vi | v0, v1...vn-1
    E是边的集合 <vi,vj> | <v0,v1>, <v1,v2>.....
    
    <vi,vj> 是顶点vi到顶点vj之间是否存在路径的判断条件

    
    
图是一个二元组,是顶点以及顶点之间路径的集合

连通图:图中任意两个顶点之间都存在一条路径(即任意两个顶点是连通的)

强连通图:图中任意两个顶点之间都存在双向的有向路径。具体来说,一个有向图是强连通图,当且仅当对于图中的任意两个顶点 (vi) 和 (vj),都存在一条从 (vi) 到 (vj) 的有向路径和一条从 (vj) 到 (vi) 的有向路径

2. 分类

无向图:
    路径 -- 边
        如果<vi, vj>存在  那么<vj, vi>一定存在
        

有向图:
    路径 -- 弧
        如果<vi, vj>存在  那么<vj, vi>不一定存在

3. 网

如果在图的关系<vi,vj>或<vj,vi>上 附加了一个权值w     称w为弧或边上的权

带权的图叫做网

4. 顶点的度

无向图:
    顶点的边的条数

有向图:
    入度:以顶点为终点的弧的数目
    出度:以顶点为起点的弧的数目
    顶点的度 = 入度 + 出度

完全图:

  • 无向图:每两个顶点存在一条边    顶点为n    边:n*(n-1)/2
  • 有向图:每两个顶点都存在着两个方向相反的边     顶点为n    边:n*(n-1)

5. 图的物理存储结构

存储图:

  • 顶点的集合
  • 关系的集合

邻接矩阵 (二维数组) <------------------
接表(数组 + 链表)------> 数组存储起点
十字链表( 逆邻接表 ):(数组 + 链表) ------> 数组存储终点
邻接多重链表

邻接点:
    在无向图中,如果存在一条边(vi,vj),则vi和vj互为邻接点
    
    在有向图中,如果存在一条边<vi,vj>,则vi是弧的起点,vj是弧的终点,称vj是vi的邻接点

(1)邻接矩阵 adjacency matrix

#define E_MAX INT32_MAX	// 边 无穷大 不可达
#define V_MAX 256   // 顶点最大个数

typedef char VType;	// 顶点的类型
typedef int WType; // 权的类型 边上的数值

typedef strutc Graph {
	VType V[V_MAX]; // 一维数组 用来存储顶点的集合
	WType E[V_MAX][V_MAX]; // 二维数组 用来存储 顶点之间的距离
	int v_num; // 实际顶点的个数
	int e_num; // 实际边的条数
} Graph;

(2)邻接表 (数组 + 链表)

struct adj {
	int index; // 边的终点在数组中的下标
	int w; // 权值
	struct adj *next;
};

typedef struct {
	VType data;
	struct adj *first; // 指向以数组中的顶点为起始点的第一个终点
} Vextex;

Vertex graph[V_MAX]; // 顶点结构体数组

邻接矩阵 代码的实现

// 找顶点对应的下标
int find_index(VType *v, int n, VType x) {
    for (int i = 0; i < n; i++) {
        if (v[i] == x) { // 找到了
            return i;
        }
    }
    return -1; // 可能找不到顶点
}

/*
    创建一个图
    返回值为:图的类型    
*/
Graph* create_graph() {
    // 1.创建一个空图,把关系集合初始化
    Graph *g = malloc(sizeof(Graph));

    // 把关系的集合赋值为无穷大
    for (int i = 0; i < V_MAX; i++) {
        for (int j = 0; j < V_MAX; j++) {
            g->E[i][j] = E_MAX;
        }
    }
    g->v_num = 0; // 实际顶点的个数
    g->e_num = 0; // 实际边的个数

    // 2.根据用户的输入 初始化顶点的集合
    printf("请输入顶点的值:(eg:abcd):\n");
    scanf("%s", g->V);
    getchar(); // 重点:吸收回车
    g->v_num = strlen(g->V);

    printf("请输入关系与权值:(eg:ab12):\n");
    while (1) {
        VType s, d; // 起点 终点
        WType w; // 权值
        // 输入
        scanf("%c%c%d", &s, &d, &w); // 输入一组数组 不能用空格隔开 ab12
        getchar(); // 重点:吸收回车
        if (s == '#' && d == '#' && w == 0) { // 自己约定退出条件
            break;
        }
        // 找顶点s d在顶点元素集合v中对应的下标
        int si = find_index(g->V, g->v_num, s);
        int di = find_index(g->V, g->v_num, d);
        // 保存关系 权值
        if (si != -1 && di != -1) {
            g->E[si][di] = w;
            g->e_num++;
        }
    }
    return g;
}

6. 图的遍历

图的遍历 是树的推广 是按照某一种规则去访问图中的每一个顶点 每个顶点只能访问一次


为了避免出现同一节点被重复访问的情况,需要记住每个被访问过的结点
设置一个访问标记的数组visited
    顶点i被访问过        visited[i] = 1
    顶点i未被访问过     visited[i] = 0

(1)深度优先搜索DFS: depth first search

类似于树的先序遍历

设 初始时,图中各个顶点都是未被访问 int visited[V_MAX] = {0};

    从图中的某个顶点出发(v0),访问v0
    然后找v0的邻接点vi,如果vi没有被访问,就需要按照同样的方式访问vi
    再去找vi的邻接点..(深度优先搜索)
    
    如果该顶点所有的邻接点都被访问过了,则回到上一个顶点
    然后再去以该顶点 进行深度优先搜索 去访问其他的邻接点
    直到所有的访问的顶点 都被访问完毕 结束    

int visited[V_MAX]; // 访问 标志位数组,表示下标对应的顶点是否被访问过 0没有访问 1已经访问

/*
    得到图中顶点v的下一个邻接点
    g:图
    v:需要找的下一个邻接点顶点的下标
    w:每一次开始找的起点
*/
int find_next_adj(Graph *g, int v, int w) {
    for (int i = w; i < g->v_num; i++) { // 遍历v的这一行 找与v为顶点的下一个邻接点
        if (g->E[v][i] != E_MAX) { // 关系存在,找到以v开头的后面遍历的邻接点 返回其下标
            return i;
        }
    }
    return -1; // 没有找到
}

/*
深度优先搜索(类似于树的先序遍历)
    g:图
    v:开始访问的顶点的下标
*/
void DFS(Graph *g, int v) {
    // 1.先访问v  标记
    printf("%c ", g->V[v]);
    visited[v] = 1;
    // 2.找v的邻接点
    for (int w = find_next_adj(g, v, 0); w != -1; w = find_next_adj(g, v, w + 1)) {
        if (visited[w] == 0) { // 如果找到的邻接点未被访问,则按照同样的方式往下访问
            DFS(g, w);
        }
    }
}

// 解决非连通图
void DFS_traverse(Graph *g) {
    // 全部标志位 置 0
    for (int i = 0; i < g->v_num; i++) {
        visited[i] = 0;
    }
    // DFS
    for (int i = 0; i < g->v_num; i++) {
        if (visited[i] == 0) {
            DFS(g, i);
        }
    }
    printf("\n");
}

(2)广度优先搜索BFS:breath first search 

类似于树的层次遍历

设 初始时,图中各个顶点都是未被访问 int visited[V_MAX] = {0};

    从图中的某个顶点出发(v0),访问v0
    然后找v0的邻接点vi,把所有的vi都访问完了
    然后再从这些已经访问的邻接点中 依照广度优先搜索的方式去全部的邻接点

队列 
    入队元素 先访问再入队
    出队元素 把它所有未被访问过的邻接点入队

int visited[V_MAX]; // 访问 标志位数组,表示下标对应的顶点是否被访问过 0没有访问 1已经访问

/*
    得到图中顶点v的下一个邻接点
    g:图
    v:需要找的下一个邻接点顶点的下标
    w:每一次开始找的起点
*/
int find_next_adj(Graph *g, int v, int w) {
    for (int i = w; i < g->v_num; i++) { // 遍历v的这一行 找与v为顶点的下一个邻接点
        if (g->E[v][i] != E_MAX) { // 关系存在,找到以v开头的后面遍历的邻接点 返回其下标
            return i;
        }
    }
    return -1; // 没有找到
}

// 广度优先搜索(层序遍历--->借用队列)
void BFS(Graph *g, int v) {
	// 1.初始化队列
    int queue[g->v_num + 1];
    int head = 0; // 队头
    int tail = 0; // 队尾
	// 2.访问v 再把v进行入队(入队入下标) 标志位置为1
    queue[++tail] = v;
    visited[v] = 1;
	while (head < tail) { // 队列不为空
		// 出队  获取该顶点的下标i,把所有的未访问的邻接点入队
        int index = queue[++head];
		printf("%c ", g->V[index]);
        // 求邻接点
		for (int w = find_next_adj(g, index, 0); w != -1; w = find_next_adj(g, index, w + 1)) {
			if (visited[w] == 0) { // 邻接点没有被访问
				// 先访问 标志位置为1 再入队
                visited[w] = 1;
                queue[++tail] = w;
			}
		}
	}
}

// 解决非连通图
void BFS_traverse(Graph *g) {
    // 全部标志位 置 0
    for(int i = 0; i < g->v_num; i++) {
        visited[i] = 0;
    }
    // BFS
    for (int i = 0; i < g->v_num; i++) {
        if (visited[i] == 0) {
            BFS(g, i);
        }
    }
    printf("\n");
}

7. 最短路径 

解决带权的有向图  两个顶点之间的最短距离的问题

两个经典的算法:
    
迪杰斯特拉 Dijkstra    两层循环     O(n^2)
    弗洛伊德 Floyd           三层循环     O(n^3)

    
    都是用于比较 v0vi 与 v0vk+vkvi的大小的基本算法

迪杰斯特拉Dijkstra
    是解决从网络中任意的顶点(源点)出发,求他到其他各个顶点的最短距离
    
假设图中有n个顶点,求一个源顶点vm 到 它所有的n-1个顶点vi的最短距离

基本思想:
    vm到vi的最短路径,只有两种情况:
    1. 直接到
          vm和vi存在关系
    2. 间接到
          中间需要经过其他的顶点vk
          vm ----> vk 的距离 + vk ----> vi 的距离
    
          vm -----> vk >= vm --->vi 显而易见 直接到最短的

迪杰斯特拉Dijkstra    两层循环    0(n^2)

为了方便描述,定义数组 dist
    int dist[v_num];
        dist[i]; // 表示 vm 到 vi的距离
        dist[0]; //表示 vm 到 v0的距离
        dist[3]; //表示 vm 到 v3的距离
        ......

(1)直接到    vm 到 vi

        dist[i] = g->E[m][i]; // g->E是一个二维数组 ,保存 每两个顶点间的距离
(2)通过其他的顶点 vi
        vm ...... vk ...... vi
        temp = g->E[m][k] + g->E
[k][i]
        if (temp < dist[i]) {
            dest[i] = temp; // 更新最短路径

        }

需要用到三个辅助数组:

  •  dist数组  最短路径
dist[v_num];
dist[i]; // 表示 vm 到 vi的距离
dist[0]; // 表示 vm 到 v0的距离
dist[3]; // 表示 vm 到 v3的距离
......
  • path 数组 保存已经确定好的最短路径的顶点
char path[v_num][v_num]; // 保存 实际的最短路径经过的顶点
path[i][0] = vm; // 标识起点 vm
path[i]; // 保存 vm 到 vi 的路程
  • s 数组
int s[v_num]; // 标志数组 标识 顶点 vi的路径是否已经确定好了是最短路径
s[i] = 0; // vm 到 vi 的最短路径暂时未确定
s[i] = 1; // vm 到 vi 的最短路径已确定

算法实现:
    总体分两大步
        1.
            确定最短路径
            从 源点 vm 到 其他各顶点的第一个最短距离的长度为
                dist[k] = min{dist[i] | i = 0,1,2,3. ...n-1 | s[i] == 0}
                dist 数组中的最小值 就是dist[k] 就是确定了 最短路径
                但是其实我们所需要的是最小值的下标
            s[k] = 1
        2.
            更新最短路径
            s[i] == 0; (vm 到 vi的最短路径没有确定好)
            if (dist[i] > g->E[k][i] + dist[k]) {
                vm 到 vi 有了更短的路径
                更新
                dist[i] = g->E[k][i] + dist[k];
            }
        
        循环N-1次

/*
    迪杰斯特拉算法
*/
// 辅助数组
int s[V_MAX]; // 标记v到其他顶点的最短路径是否已经求出
int dist[V_MAX]; // 求最短路径的距离
char path[V_MAX][V_MAX]; // V->A->B

/*
    g:图
    v:源点的下标
*/
void Dijkstra(Graph *g, int v) {
    // 参数有效性检查
    if (g == NULL) {
        return;
    }
    // 初始化数组
    for (int i = 0; i < g->v_num; i++) {
        s[i] = 0; // 标记v到其他顶点的最短路径是否已经求出 0:未求出
        dist[i] = g->E[v][i]; // 最短路径初始化为v到各个顶点的直接距离
        path[i][0] = g->V[v]; // 路径数组的第一个全部初始化为原点
        path[i][1] = g->V[i]; // 路径数组的第二个全部初始化为终点
    }
    s[v] = 1; // v->v 已经知道是无穷大 默认已经求出了最短路径

    // 循环n-1次求最短路径
    for (int i = 0; i < g->v_num - 1; i++) {
        // 1.求最短路径   在s[i]标记为0的位置找
        int min = E_MAX;
        int min_index; // 最短路径的下标
        for (int j = 0; j < g->v_num; j++) {
            if (s[j] == 0) { // 还没有求出最短路径
                if (dist[j] < min) {
                    min = dist[j];
                    min_index = j; // 记录当前求出的最短路径的下标
                }
            }
        }
        // dist[min_index]就是最短路径,V[v] -> V[min_index] 的最短路径求出来了 path[min_index]
        s[min_index] = 1;

        // 2.根据现在的最短路径去比较 更新其他的dist
        // 对所有的s[j] = 0 更新dist[j]
        // if (dist[min_index] + g->E[min_index][j] < dist[j]) 更新
        for (int j = 0; j < g->v_num; j++) {
            if (s[j] == 0) { // 最短路径还未被求出
                if (dist[min_index] + g->E[min_index][j] < dist[j]) {
                    // 更新 最短路径
                    dist[j] = dist[min_index] + g->E[min_index][j];
                    // 更新 路径
                    strcpy(path[j], path[min_index]); // VB -> VA
                    path[j][strlen(path[j])] = g->V[j]; // VA -> VAB
                }
            }
        }
    }
    // 打印  V->B[4]:V->D->B
    for (int i = 0; i < g->v_num; i++) {
        if (i == v) {
            continue;
        }
        printf("%c->%c[%d]:", g->V[v], g->V[i], dist[i]);
        // 打印路径  遍历 path[i]
        int j;
        for (j = 0; path[i][j+1]; j++) {
            printf("%c->", path[i][j]); // 最后一个字符 单独处理
        }
        printf("%c\n", path[i][j]);
    }
}

8. 图代码

graph.h

#ifndef _GRAPH_H__
#define _GRAPH_H__

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

#define E_MAX 65535	// 边 无穷大 不可达
#define V_MAX 256	// 顶点最大个数

typedef char VType;	// 顶点的类型
typedef int WType;	// 权的类型 边上的数值

typedef struct Graph {
	VType V[V_MAX]; // 一维数组 来存储顶点的集合
	WType E[V_MAX][V_MAX]; // 二维数组 来存储 顶点之间的关系
	int v_num; // 实际顶点的个数
	int e_num; // 实际边的条数
} Graph;

// 找顶点对应的下标
int find_index(VType *v, int n, VType x);

/*
    创建一个图
    返回值为:图的类型    
*/
Graph* create_graph();

// 打印图
void print_graph(Graph *g);

int find_next_adj(Graph *g, int v, int w);

/*
深度优先搜索(类似于树的先序遍历)
    g:图
    v:开始访问的顶点的下标
*/
void DFS(Graph *g, int v);

// 解决非连通图
void DFS_traverse(Graph *g);

void BFS(Graph *g, int v);

// 解决非连通图
void BFS_traverse(Graph *g);

void Dijkstra(Graph *g, int v);

#endif

graph.c
 

#include "graph.h"

// 找顶点对应的下标
int find_index(VType *v, int n, VType x) {
    for (int i = 0; i < n; i++) {
        if (v[i] == x) { // 找到了
            return i;
        }
    }
    return -1; // 可能找不到顶点
}

/*
    创建一个图
    返回值为:图的类型    
*/
Graph* create_graph() {
    // 1.创建一个空图,把关系集合初始化
    Graph *g = malloc(sizeof(Graph));

    // 把关系的集合赋值为无穷大
    for (int i = 0; i < V_MAX; i++) {
        for (int j = 0; j < V_MAX; j++) {
            g->E[i][j] = E_MAX;
        }
    }
    g->v_num = 0; // 实际顶点的个数
    g->e_num = 0; // 实际边的个数

    // 2.根据用户的输入 初始化顶点的集合
    printf("请输入顶点的值:(eg:abcd):\n");
    scanf("%s", g->V);
    getchar(); // 重点:吸收回车
    g->v_num = strlen(g->V);

    printf("请输入关系与权值:(eg:ab12):\n");
    while (1) {
        VType s, d; // 起点 终点
        WType w; // 权值
        // 输入
        scanf("%c%c%d", &s, &d, &w); // 输入一组数组 不能用空格隔开 ab12
        getchar(); // 重点:吸收回车
        if (s == '#' && d == '#' && w == 0) { // 自己约定退出条件
            break;
        }
        // 找顶点s d在顶点元素集合v中对应的下标
        int si = find_index(g->V, g->v_num, s);
        int di = find_index(g->V, g->v_num, d);
        // 保存关系 权值
        if (si != -1 && di != -1) {
            g->E[si][di] = w;
            g->e_num++;
        }
    }
    return g;
}

// 打印图
void print_graph(Graph *g) {
    // 打印顶点数组
    putchar('\t');
    for (int i = 0; i < g->v_num; i++) {
        printf("%c\t", g->V[i]);
    }
    printf("\n");
    // 打印关系
    for (int i = 0; i < g->v_num; i++) {
        printf("%c\t", g->V[i]);
        for (int j = 0; j < g->v_num; j++) {
            if (g->E[i][j] == E_MAX) {
                printf("-\t");
            } else {
                printf("%d\t", g->E[i][j]);
            }
        }
        putchar('\n');
    }
}

int visited[V_MAX]; // 访问 标志位数组,表示下标对应的顶点是否被访问过 0没有访问 1已经访问

/*
    得到图中顶点v的下一个邻接点
    g:图
    v:需要找的下一个邻接点顶点的下标
    w:每一次开始找的起点
*/
int find_next_adj(Graph *g, int v, int w) {
    for (int i = w; i < g->v_num; i++) { // 遍历v的这一行 找与v为顶点的下一个邻接点
        if (g->E[v][i] != E_MAX) { // 关系存在,找到以v开头的后面遍历的邻接点 返回其下标
            return i;
        }
    }
    return -1; // 没有找到
}

/*
深度优先搜索(类似于树的先序遍历)
    g:图
    v:开始访问的顶点的下标
*/
void DFS(Graph *g, int v) {
    // 1.先访问v  标记
    printf("%c ", g->V[v]);
    visited[v] = 1;
    // 2.找v的邻接点
    for (int w = find_next_adj(g, v, 0); w != -1; w = find_next_adj(g, v, w + 1)) {
        if (visited[w] == 0) { // 如果找到的邻接点未被访问,则按照同样的方式往下访问
            DFS(g, w);
        }
    }
}

// 解决非连通图
void DFS_traverse(Graph *g) {
    // 全部标志位 置 0
    for (int i = 0; i < g->v_num; i++) {
        visited[i] = 0;
    }
    // DFS
    for (int i = 0; i < g->v_num; i++) {
        if (visited[i] == 0) {
            DFS(g, i);
        }
    }
    printf("\n");
}

// 广度优先搜索(层序遍历--->借用队列)
void BFS(Graph *g, int v) {
	// 1.初始化队列
    int queue[g->v_num + 1];
    int head = 0; // 队头
    int tail = 0; // 队尾
	// 2.访问v 再把v进行入队(入队入下标) 标志位置为1
    queue[++tail] = v;
    visited[v] = 1;
	while (head < tail) { // 队列不为空
		// 出队  获取该顶点的下标i,把所有的未访问的邻接点入队
        int index = queue[++head];
		printf("%c ", g->V[index]);
        // 求邻接点
		for (int w = find_next_adj(g, index, 0); w != -1; w = find_next_adj(g, index, w + 1)) {
			if (visited[w] == 0) { // 邻接点没有被访问
				// 先访问 标志位置为1 再入队
                visited[w] = 1;
                queue[++tail] = w;
			}
		}
	}
}

// 解决非连通图
void BFS_traverse(Graph *g) {
    // 全部标志位 置 0
    for(int i = 0; i < g->v_num; i++) {
        visited[i] = 0;
    }
    // BFS
    for (int i = 0; i < g->v_num; i++) {
        if (visited[i] == 0) {
            BFS(g, i);
        }
    }
    printf("\n");
}

/*
    迪杰斯特拉算法
*/
// 辅助数组
int s[V_MAX]; // 标记v到其他顶点的最短路径是否已经求出
int dist[V_MAX]; // 求最短路径的距离
char path[V_MAX][V_MAX]; // V->A->B

/*
    g:图
    v:源点的下标
*/
void Dijkstra(Graph *g, int v) {
    // 参数有效性检查
    if (g == NULL) {
        return;
    }
    // 初始化数组
    for (int i = 0; i < g->v_num; i++) {
        s[i] = 0; // 标记v到其他顶点的最短路径是否已经求出 0:未求出
        dist[i] = g->E[v][i]; // 最短路径初始化为v到各个顶点的直接距离
        path[i][0] = g->V[v]; // 路径数组的第一个全部初始化为原点
        path[i][1] = g->V[i]; // 路径数组的第二个全部初始化为终点
    }
    s[v] = 1; // v->v 已经知道是无穷大 默认已经求出了最短路径

    // 循环n-1次求最短路径
    for (int i = 0; i < g->v_num - 1; i++) {
        // 1.求最短路径   在s[i]标记为0的位置找
        int min = E_MAX;
        int min_index; // 最短路径的下标
        for (int j = 0; j < g->v_num; j++) {
            if (s[j] == 0) { // 还没有求出最短路径
                if (dist[j] < min) {
                    min = dist[j];
                    min_index = j; // 记录当前求出的最短路径的下标
                }
            }
        }
        // dist[min_index]就是最短路径,V[v] -> V[min_index] 的最短路径求出来了 path[min_index]
        s[min_index] = 1;

        // 2.根据现在的最短路径去比较 更新其他的dist
        // 对所有的s[j] = 0 更新dist[j]
        // if (dist[min_index] + g->E[min_index][j] < dist[j]) 更新
        for (int j = 0; j < g->v_num; j++) {
            if (s[j] == 0) { // 最短路径还未被求出
                if (dist[min_index] + g->E[min_index][j] < dist[j]) {
                    // 更新 最短路径
                    dist[j] = dist[min_index] + g->E[min_index][j];
                    // 更新 路径
                    strcpy(path[j], path[min_index]); // VB -> VA
                    path[j][strlen(path[j])] = g->V[j]; // VA -> VAB
                }
            }
        }
    }
    // 打印  V->B[4]:V->D->B
    for (int i = 0; i < g->v_num; i++) {
        if (i == v) {
            continue;
        }
        printf("%c->%c[%d]:", g->V[v], g->V[i], dist[i]);
        // 打印路径  遍历 path[i]
        int j;
        for (j = 0; path[i][j+1]; j++) {
            printf("%c->", path[i][j]); // 最后一个字符 单独处理
        }
        printf("%c\n", path[i][j]);
    }
}

main.c

#include "graph.h"

int main(int argc, char *argv[]) {
    Graph *g = create_graph();
    print_graph(g);

    DFS_traverse(g); // 深度搜索
    BFS_traverse(g); // 广度搜索
    // Dijkstra(g, 0); // 迪杰斯特拉算法

    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值