5.图论.题目3

题目

15.软件构造/BFS拓扑搜索

题目连接
在这里插入图片描述
概括来说,给出一个 有向图,把这个有向图转成线性的排序 就叫拓扑排序。当然拓扑排序也要检测这个有向图 是否有环,即存在循环依赖的情况,因为这种情况是不能做线性排序的。
实现拓扑排序的算法有两种:卡恩算法(BFS)和DFS;这题要使用卡恩算法(BFS)实现,这就类型广度优先搜索。
BFS算法代码:

#include <iostream>
#include "vector"
#include "queue"
#include "unordered_map"

using namespace std;

int main(){
    int m,n,s,t;
    cin>> n>> m;
    vector<int> indegree(n,0);
    unordered_map<int, vector<int>> graph;
    for(int i=0;i<m;i++){
        cin>> s>> t;
        graph[s].push_back(t);
        indegree[t]++;
    }
    queue<int> q;
    for(int i=0; i<n; i++){
        if(indegree[i]==0) q.push(i);
    }

    vector<int> res;
    // BFS
    while (!q.empty()){
        int cur = q.front();
        q.pop();
        res.push_back(cur);
        vector<int> files = graph[cur];
        if(!files.empty()){
            // cur有被依赖的文件
            for(int file : files) {
                indegree[file]--;
                if (indegree[file] == 0) q.push(file); // 将新的入度为0的节点放入que中
            }
        }
    }
    if(res.size() == n){
        for(int i=0;i<n-1;i++){
            cout<< res[i]<<" ";
        }
        cout<< res[n-1];
    }
    else cout<< -1<< endl;
    return 0;
}

16.参加科学大会/dijkstra 算法

题目链接
在这里插入图片描述
本题就是求最短路,最短路是图论中的经典问题即:给出一个有向图,一个起点,一个终点,问起点到终点的最短路径。这题可以使用dijkstra 算法解决。
dijkstra算法:在有权图(权值非负数)中求从起点到其他节点的最短路径算法,需要注意两点:

  • dijkstra算法可以同时求 起点到所有节点的最短路径
  • 权值不能为负数
    在这里插入图片描述

17.城市货物运输1/bellman_ford 算法

题目链接
在这里插入图片描述
解决经典的带负权值的单源最短路问题,此时就轮到Bellman_ford登场了。
共有两个关键点。

  1. “松弛”究竟是个啥?
  2. 为什么要对所有边松弛 n - 1 次 (n为节点个数) ?

在这里插入图片描述
B节点的mindist值可以由A,C节点值推导出来,状态一: minDist[A] + value 可以推出 minDist[B] 状态二: minDist[B]本身就有权值 (可能是其他边链接的节点B 例如节点C,以至于 minDist[B]记录了其他边到minDist[B]的权值)
如果 通过 A 到 B 这条边可以获得更短的到达B节点的路径,这就是松弛的概念;而在路径搜索中这成为剪枝操作;也是采用了动态规划的思想,即:将一个问题分解成多个决策阶段,通过状态之间的递归关系最后计算出全局最优解。

if (minDist[B] > minDist[A] + value) minDist[B] = minDist[A] + value

对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离
节点数量为n,那么起点到终点,最多是 n-1 条边相连。那么无论图是什么样的,边是什么样的顺序,我们对所有边松弛 n-1 次 就一定能得到 起点到达 终点的最短距离。

普通版Bellman_ford算法

#include <iostream>
#include "vector"
#include "list"
#include "climits"


using namespace std;

struct Edge {
    int to;  // 链接的节点
    int val; // 边的权重

    Edge(int t, int w): to(t), val(w) {}  // 构造函数
};

int main(){
    int m,n,p1,p2,val;
    cin>> n>> m;
    vector<vector<Edge>> graph(n+1); // 邻接表,但其实可以使用邻接矩阵进行存储图
    for(int i=0; i<m; i++){
        cin>> p1>> p2>> val;
        graph[p1].emplace_back(p2, val);
    }
    vector<int> mindist(n+1, INT_MAX);
    mindist[1]=0;
    // n-1次松弛
    for(int j=1; j<n; j++){
        // 遍历邻接表所有的边
        for(int i=1; i<=n; i++){
            for(auto &e: graph[i]){
                int from = i;
                int to = e.to;
                int price = e.val;
                if(mindist[from]!=INT_MAX && mindist[to]>mindist[from]+price) mindist[to] = mindist[from]+price;
            }
        }
        cout<< "松弛第 "<< j<< "次"<< endl;
        for(int k=1; k<=n; k++){
            cout<< mindist[k]<< " ";
        }
        cout<< endl;
    }
    if(mindist[n]==INT_MAX) cout<< "unconnected"<< endl;
    else cout<< mindist[n]<< endl;
    return 0;
}

队列优化版Bellman_ford算法
Bellman_ford 队列优化算法(Queue improved Bellman-Ford) ,也叫SPFA算法(Shortest Path Faster Algorithm)。大家可以发现 Bellman_ford 算法每次松弛 都是对所有边进行松弛。但真正有效的松弛,是基于已经计算过的节点在做的松弛
基于以上思路,==如何记录上次松弛的时候更新过的节点呢?==用队列来记录。(其实用栈也行,对元素顺序没有要求)
基于队列优化的算法,要比bellman_ford 算法 减少很多无用的松弛情况,特别是对于边数众多的大图 优化效果明显

    while (!que.empty()) {

        int node = que.front(); que.pop();

        for (Edge edge : grid[node]) {
            int from = node;
            int to = edge.to;
            int value = edge.val;
            if (minDist[to] > minDist[from] + value) { // 开始松弛
                minDist[to] = minDist[from] + value;
                que.push(to);
            }
        }
    }

Bellman_ford队列优化版 的时间复杂度 并不稳定,效率高低依赖于图的结构;
例如 如果是一个双向图,且每一个节点和所有其他节点都相连的话,那么该算法的时间复杂度就接近于 Bellman_ford 的 O(N * E) N 为节点数量,E为边的数量。
所以如果图越稠密,则 SPFA的效率越接近与 Bellman_ford。反之,图越稀疏,SPFA的效率就越高


18.城市运输货物2/bellman_ford 算法

题目链接
在17题的基础上,条件在评估从城市 1 到城市 n 的所有可能路径中综合政府补贴后的最低运输成本时,存在一种情况:图中可能出现负权回路
负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。
为了避免在使用Bellman_ford算法遇到该情况时,出现这种异常情况,需要插入一个判断环节,返回这种异常情况。
在这里插入图片描述
在 bellman_ford 算法中,松弛 n-1 次所有的边 就可以求得 起点到任何节点的最短路径,松弛 n 次以上,minDist数组(记录起到到其他节点的最短距离)中的结果也不会有改变;而在有负权回路的情况下,一直都会有更短的最短路,所以 松弛 第n次,minDist数组也会发生改变。

那么很自然,我们在基础版的还是队列优化版的bellman_ford算法松弛n-1次的基础上,再额外松弛一次,判断mindist数组中是否发生改变,若有发生改变,则图中必定存在负权回路。

  • 基础版bellman_ford:n个节点,最多n-1条边即可保证起点1至n连通,所以bellman_ford松弛n-1次即可得到到达n节点的距离最小路径。。在顶层for循环中增加一次for循环,在最后的循环中检查mindist数组是否满足松弛条件
  • 队列优化版bellman_ford:在极端情况下,即:所有节点都与其他节点相连,每个节点的入度为 n-1 (n为节点数量),所以每个节点最多加入 n-1 次队列。在while循环下,检查松弛条件生效时,全局计数器数组对应节点下标加1,如果存在某节点加入队列次数超过 n-1次 就说明该图与负权回路。

以队列优化版Bellman_ford算法为例子:

#include <iostream>
#include "vector"
#include "queue"
#include "climits"

using namespace std;

struct Edge {
    int to;  // 链接的节点
    int val; // 边的权重

    Edge(int t, int w): to(t), val(w) {}  // 构造函数
};

int main(){
    int m,n,p1,p2,val;
    cin>> n>> m;
    vector<vector<Edge>> graph(n+1); // 邻接表
    for(int i=0; i<m; i++){
        cin>> p1>> p2>> val;
        graph[p1].emplace_back(p2, val);
    }
    vector<int> mindist(n+1, INT_MAX);
    mindist[1]=0;
    queue<int> q; // 队列存放待遍历的节点
    q.push(1);
    
    vector<int> count(n+1, 0); // 记录每个节点的入队次数
    count[1]++;
    bool flag = false;
    while(!q.empty()){
        int node = q.front(); q.pop();
        for(auto edge: graph[node]){
            int from = node;
            int to = edge.to;
            int value = edge.val;
            if(mindist[to]>mindist[from]+value){
                mindist[to] = mindist[from]+value;
                q.push(to);
                count[to]++;
                if(count[to]==n) {
                    flag = true;
                    while(!q.empty()) q.pop(); //释放
                    break;
                }
            }
        }
    }
    if(flag) cout<< "circle"<< endl;
    else if(mindist[n]==INT_MAX) cout<< "unconnected"<< endl;
    else cout<< mindist[n]<< endl;

    return 0;
}

19.城市运输货物3/bellman_ford 算法

题目链接
在17题的基础上,解决经典的带负权值的单源最短路问题,考虑最多经过 k 个城市的条件下,而不是一定经过k个城市,也可以经过的城市数量比k小,但要最短的路径。
最多经过k个节点,即最多经过k+1个节点到达终点的最短距离。此时在使用普通版的bellman_ford 算法时,处理带负权值回路的情况。
在这里插入图片描述
根据普通版代码每次顶层for循环遍历所有边可知,以上mindist是普通版bellman_ford算法得到第一次松弛的结果;所有边进行的第二次松弛,minDist数组为 : -2 -2 -1 0 所有边进行的第三次松弛,minDist数组为 : -3 -3 -2 -1等。
理论上来说,对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离。但我们在实际代码运行发现不仅仅 与起点一条边相连的节点更新了,所有节点都更新了。这说明至多经过k个节点这个限制没有起作用。因为理论上节点三最快在对所有边第二次松弛时,才会更新,而在第一次松弛时,当时是基于已经计算好的 节点2(minDist[2])来做计算了。

所以在每次计算 minDist 时候,要基于 对所有边上一次松弛的 minDist 数值才行,所以我们要记录上一次松弛的minDist(直观的想法是每次顶层遍历时先拷贝上一次计算的结果)

    for (int i = 1; i <= k + 1; i++) {
        minDist_copy = minDist; // 获取上一次计算的结果
        for (vector<int> &side : grid) {
            int from = side[0];
            int to = side[1];
            int price = side[2];
            // 注意使用 minDist_copy 来计算 minDist 
            if (minDist_copy[from] != INT_MAX && minDist[to] > minDist_copy[from] + price) {  
                minDist[to] = minDist_copy[from] + price;
            }
        }
    }

时间复杂度: O(K * E) , K为至多经过K个节点,E为图中边的数量;空间复杂度: O(N) ,即 minDist 数组所开辟的空间

本题本质
在之前17,18题没有使用mindist_copy拷贝上一次最小节点距离数组怎么会没有影。在17题,是没有负权回路的,那么无论比n-1多松弛多少次,mindist结果不会发生改变。因此在对所有边进行第一次松弛的时候,如果基于 本次计算的 minDist 来计算 minDist (相当于多做松弛了),也是对最终结果没影响。在18题,是判断是否有 负权回路,一旦有负权回路,则异常返回。
因此本题的关键区别是:
1.可以有负权回路,说明只要多做松弛,结果是会变的。
2.要求最多经过k个节点,对松弛次数是有限制的。

SPFA
要求最多经过k个节点,对松弛次数是有限制的。使用技巧,可以用一个变量 que_size 记录每一轮松弛入队列的所有节点数量;下一轮松弛的时候,就把队列里 que_size 个节点都弹出来,就是上一轮松弛入队列的节点。

    int que_size;
    while (k-- && !que.empty()) {

        minDist_copy = minDist; // 获取上一次计算的结果
        que_size = que.size(); // 记录上次入队列的节点个数
        while (que_size--) { // 上一轮松弛入队列的节点,这次对应的边都要做松弛
            int node = que.front(); que.pop();
            for (Edge edge : grid[node]) {
                int from = node;
                int to = edge.to;
                int price = edge.val;
                if (minDist[to] > minDist_copy[from] + price) {
                    minDist[to] = minDist_copy[from] + price;
                    que.push(to);
                }
            }

        }
    }

理论上,SPFA的时间复杂度不是要比 bellman_ford 更优吗?因为在该过程中queue的进出元素操纵,耗时很大,所以同的时间复杂度的情况下,SPFA 实际上更耗时了。

能否使用dijkstra算法?
在这里插入图片描述
此时最多经过2个节点的搜索就完毕了,但结果中minDist[7] (即节点7的结果)并没有被更;使用dijkstra算法所遍历的节点是1-->2-->3-->4,dijkstra每一步都采取贪心的策略,以最小的代价值到达下一个没访问的节点,因此没办法找到1-->2-->6-->7这条路径。


20.小明逛公园/Floyed 算法

题目链接
在这里插入图片描述
本题是经典的多源最短路问题。与之前的单源最短路径,之能有一个起点不同。这题是求多个起点到多个终点的多条最短路径
通过该题学习Floyd算法,Floyd 算法对边的权值正负没有要求,都可以处理。
Floyd算法核心思想是动态规划
在动态规划下有五部曲:
1.确定dp数组以及下标的含义grid[i][j][k] = m,表示 节点i 到 节点j 以[1...k] 集合为中间节点的最短距离为m。因为节点i 到 节点j 的最短路径中 一定是经过很多节点,那么这个集合用[1...k] 来表示。
2.确定递推公式:1)节点i 到 节点j 的最短路径经过节点k,递推式为grid[i][j][k] = grid[i][k][k - 1] + grid[k][j][k - 1] 在该递推式子中第一部分是节点i 到 节点k 的最短距离 是不经过节点k,中间节点集合为[1...k-1];第二部分是节点k 到 节点j 的最短距离 也是不经过节点k,中间节点集合为[1...k-1] 2)节点i 到 节点j 的最短路径不经过节点k,递推式为grid[i][j][k] = grid[i][j][k - 1],如果节点i 到 节点j的最短距离 不经过节点k,那么 中间节点集合[1…k-1]。 最后我们取两种情况的最小值:grid[i][j][k] = min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1])
3.dp数组初始化:grid[i][j][k] = m,表示 节点i 到 节点j 以[1…k] 集合为中间节点的最短距离为m,而初始化的时候k是不确定的,所以只能填0,0是没有意义的。这样我们在下一轮计算的时候,就可以根据 grid[i][j][0] 来计算 grid[i][j][1],此时的 grid[i][j][1] 就是 节点i 经过节点1 到达 节点j 的最小距离了。

vector<vector<vector<int>>> grid(n + 1, vector<vector<int>>(n + 1, vector<int>(n + 1, 10005)));  // C++定义了一个三位数组,10005是因为边的最大距离是10^4

for(int i = 0; i < m; i++){
    cin >> p1 >> p2 >> val;
    grid[p1][p2][0] = val;
    grid[p2][p1][0] = val; // 注意这里是双向图
} 

4.遍历顺序:从递推关系看出而 k 依赖于 k - 1, i 和j 的到 并不依赖与 i - 1 或者 j - 1 等;我们已经把 k =0 的 i 和j 对应的数值都初始化了,这样才能去计算 k = 1 的时候 i 和 j 对应的数值。遍历的顺序是从底向上 一层一层去遍历k。至于遍历 i 和 j 的话,for 循环的先后顺序无所谓。而且必须是k在外层,因为这样才能保证k层grid的值能利用k-1层的值。因为 k 放在最里面,先就把 节点1 和 节点 2 的最短距离就确定了。

for (int k = 1; k <= n; k++) {
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]);
        }
    }
}

5.打印dp数组:从k=0层开始递推grid图像

空间优化:空间上的优化,从滚动数组的角度来看,我们定义一个 grid[n + 1][ n + 1][2] 这么大的数组就可以,因为k 只是依赖于 k-1的状态,并不需要记录k-2,k-3,k-4 等等这些状态;但其实这题的dp数组[k]层定义的没不涉及唯一性(强调一一对应),而是在[1…k]集合中节点为中间节点的最短距离,因此可以进一步简化:
思考一下:如果 本层刚计算好的 grid[i][k] 比上一层 (即k-1层)计算的 grid[i][k] 小,说明确实有 i 到 k 的更短路径,那么基于 更小的 grid[i][k] 去计算 gird[i][j] 没有问题。如果 本层刚计算好的 grid[i][k] 比上一层 (即k-1层)计算的 grid[i][k] 大, 这不可能,因为这样也不会做更新 grid[i][k]的操作。
因此本层计算中,使用了本层计算过的 grid[i][k] 和 grid[k][j] 是没问题的,因此只用一个二维数组表示dp数组就可以了。(但其实是有点难理解的)

#include <iostream>
#include "vector"

using namespace std;

int main(){
    int n,m,p1,p2,val;
    cin>>n>>m;
    vector<vector<int>> graph(n+1, vector<int>(n+1,10005));
    for(int i=0; i<m; i++){
        cin>>p1>>p2>>val;
        graph[p1][p2]=val;
        graph[p2][p1]=val;
    }

    for(int k=1; k<=n; k++){
        for(int i=1; i<=n; i++){
            for(int j=1; j<=n; j++){
                graph[i][j] = min(graph[i][j], graph[i][k]+graph[k][j]);
            }
        }
    }

    int z, start, end;
    cin>> z;
    while(z--){
        cin>> start>> end;
        if(graph[start][end]==10005) cout<< -1<< endl;
        else cout<< graph[start][end]<< endl;
    }
    return 0;
}

时间复杂度: O(n^3);空间复杂度:O(n^2)


21.骑士进攻/A*算法

题目链接
在这里插入图片描述
不同于之前题目给出节点间的边,本题给出棋盘大小 1000 x 1000(棋盘的 x 和 y 坐标均在 [1, 1000] 区间内,包含边界),而且是棋盘问题(无权图),相邻节点间边权值是相同1。
简单的想法是:使用广搜BFS,但当图足够大,且 n 也有可能很大,导致有非常多的查询,超时了(BFS是没有目的性的一圈一圈从起点向外层搜索,做了许多多余的扩展)。

Astar 是一种 BFS广搜的改良版; 有的是 Astar是 dijkstra 的改良版。
两者的差别体现在:但其实是场景不同而已,如果是无权图(边的权值都是1) 那就用广搜,代码简洁,时间效率和 dijkstra 差不多 (具体要取决于图的稠密);如果是有权图(边有不同的权值),优先考虑 dijkstra。
Astar 关键在于 启发式函数Heuristic function,该函数的作用是影响 广搜或者 dijkstra 从 容器(队列)里取元素的优先顺序。相比于BFS的无方向性,Astar是有方向性的搜索

启发式函数 要影响的就是队列里元素的排序
对队列里节点进行排序,就需要给每一个节点权值,通过公式计算F = G + H
G:目前遍历的节点到达终点的距离
H:起点达到目前遍历节点的距离
起点达到目前遍历节点的距离 + 目前遍历的节点到达终点的距离 就是起点到达终点的距离。

本题的图是无权网格状,在计算两点距离通常有如下三种计算方式:
1.曼哈顿距离: d = abs(x1-x2)+abs(y1-y2)
2.欧式距离:d = sqrt( (x1-x2)^2 + (y1-y2)^2 )
3.切比雪夫距离:d = max(abs(x1 - x2), abs(y1 - y2))
选择不同的距离计算方式,会导致Astar算法 的结果不尽相同;

#include <iostream>
#include "queue"
#include "vector"
#include "algorithm"

using namespace std;

vector<vector<int>> moves(1001, vector<int>(1001));
int dir[8][2]={-2,-1,-2,1,-1,2,1,2,2,1,2,-1,1,-2,-1,-2};
int b1,b2;

struct Knight{
    int x,y;
    int g,h,f;
    bool operator < (const Knight & k) const{  // 重载运算符, 从小到大排序
        return k.f < f;
    }
};

priority_queue<Knight> que;

int heuristic(const Knight& k){
    return (k.x - b1)*(k.x - b1) + (k.y - b2)*(k.y - b2);
}

void astar(const Knight& k){
    Knight cur, next;
    que.push(k);
    while(!que.empty()) {
        cur = que.top();
        que.pop();
        if (cur.x == b1 && cur.y == b2) break;
        for (auto i: dir) {
            next.x = cur.x + i[0];
            next.y = cur.y + i[1];
            if (next.x < 1 || next.x > 1000 || next.y < 1 || next.y > 1000) continue;
            if (!moves[next.x][next.y]) {
                moves[next.x][next.y] = moves[cur.x][cur.y] + 1;
                next.g = cur.g + 5;
                next.h = heuristic(next);
                next.f = next.g + next.h;
                que.push(next);
            }
        }
    }
}

int main(){
    int n, a1, a2;
    cin>> n;
    while(n--){
        cin>> a1>> a2>> b1>> b2;
        fill(moves.begin(), moves.end(), vector<int>(1001, 0)); //初始化graph
        Knight start;
        start.x = a1;
        start.y = a2;
        start.g = 0;
        start.h = heuristic(start);
        start.f = start.g + start.h;
        astar(start);
        while(!que.empty()) que.pop(); //清空队列
        cout<< moves[b1][b2]<< endl;
    }
    return 0;
}

复杂度分析
A star算法的时间复杂度 其实是不好去量化的,因为他取决于 启发式函数怎么写。
最坏情况下,Astart 退化成广搜BFS;最好的情况下 从起点直接到终点,时间复杂度为 O(dlogd),d 为起点到终点的深度。
实际上 Astar 的时间复杂度是介于 最优 和最坏 情况之间, 可以 非常粗略的认为 A * 算法的时间复杂度是 O(nlogn) ,n 为节点数量。
A * 算法的空间复杂度 O(b ^ d) ,d 为起点到终点的深度,b 是 图中节点间的连接数量,本题因为是无权网格图,所以 节点间连接数量为 4。

扩展
A star 算法 并不是一个明确的最短路算法,A * 算法搜的路径如何,完全取决于 启发式函数怎么写。也并不能保证一定是最短路,因为在设计 启发式函数的时候,要考虑 时间效率与准确度之间的一个权衡。因此保证运行效率的情况下,Astar 算法中的启发式函数 设计往往不是最短路,而是接近最短路的 次短路设计。

Astar的缺点:
在扩展节点时 队列里添加了很多节点,但真正从队列里取出来的 仅仅是 靠启发式函数判断 距离终点最近的节点。相对了 普通BFS,A star 算法只从 队列里取出 距离终点最近的节点。因此Astar 在一次路径搜索中,大量不需要访问的节点都在队列里,会造成空间的过度消耗
IDA * 算法 对这一空间增长问题进行了优化。

给出 多个可能的目标,然后在这多个目标中 选择最近的目标,这种 A star 就不擅长解决这类问题, Astar 只擅长给出明确的目标 然后找到最短路径

最短路径问题总结

至此已经讲解了四大最短路算法,分别是Dijkstra、Bellman_ford、SPFA 和 Floyd
在这里插入图片描述
注意:因为A * 属于启发式搜索,和上面最短路算法并不是一类,不适合一起对比,所以没有放在一起

大体使用场景的分析

  1. 如果遇到单源且边为正数的情况,直接Dijkstra 算法。至于 使用朴素版还是 堆优化版 还是取决于图的稠密度, 多少节点多少边算是稠密图,多少算是稀疏图,一般可以直接用堆优化版本。
  2. 如果遇到单源边可为负数,直接 Bellman-Ford 算法,使用普通版还是队列优化版取决于图的稠密度
  3. 如果是遇到多源点且边为正求最短路,直接 Floyd 算法
  4. 对于A * ,由于其高效性,所以在实际工程应用中使用最为广泛 ,由于其 结果的不唯一性,也就是可能是次短路的特性,一般不适合作为算法题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值