拓扑排序

拓扑排序

1. 拓扑排序原理

原理

  • 只有有向无环图(DAG)才有拓扑排序。
  • 具体做法是首先将图中入度为0的点入队,然后从这些点开始扩展,每出队一个点,将其所指向的点的度数减一,如果所指向的点的入度变为0了,则将其入队,直到队列为空为止。
  • 最终整个队列中存储的就是拓扑排序后的结果。
  • 如果队列中点的数目小于图中点的数目,说明图中存在环,不存在拓扑排序序列。

代码模板

#include <iostream>
#include <cstring>

using namespace std;

const int N = 100010, M = 100010;

int n, m;
int h[N], e[M], ne[M], idx;
int q[N];  // 普通队列
int d[N];  // 入度

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

bool topsort() {
    
    int hh = 0, tt = -1;
    for (int i = 1; i <= n; i++)
        if (!d[i])
            q[++tt] = i;
        
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (--d[j] == 0)
                q[++tt] = j;
        }
    }
    
    return tt == n - 1;
}

int main() {
    
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);
    
    while (m--) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
        d[b]++;
    }
    
    if (!topsort()) puts("-1");
    else {
        for (int i = 0; i < n; i++) printf("%d ", q[i]);
        printf("\n");
    }
    
    return 0;
}

2. AcWing上的拓扑排序题目

AcWing 1191. 家谱树

问题描述

分析

  • 直接使用拓扑排序的模板即可。

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 110, M = N * N / 2;

int n;
int h[N], e[M], ne[M], idx;
int q[N];  // 普通队列
int d[N];  // 存储每个点的入度

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void topsort() {
    
    int hh = 0, tt = -1;
    for (int i = 1; i <= n; i++)
        if (!d[i])
            q[++tt] = i;
    
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (--d[j] == 0)
                q[++tt] = j;
        }
    }
}

int main() {
    
    cin >> n;
    
    memset(h, -1, sizeof h);
    
    for (int i = 1; i <= n; i++) {
        int son;
        while (cin >> son, son) {
            add(i, son);
            d[son]++;
        }
    }
    
    topsort();
    
    for (int i = 0; i < n; i++)
        cout << q[i] << ' ';
    cout << endl;
    
    return 0;
}

AcWing 1192. 奖金

问题描述

分析

  • 这一题是一道差分约束的题目,但是是一个简化版的差分约束,可以使用差分约束求解,但是有种"大炮打蚊子"的感觉,这一题没必要使用差分约束求解。

  • 此题让我们求最小值,我们应该使用最长路(不懂的话可以参考这里),题目的不等式条件是:

    a ≥ b + 1 a \ge b + 1 ab+1,相当于建一条从b指向a的边权为1的边;

    a ≥ 100 a \ge 100 a100,建立虚拟源点 x 0 = 0 x_0 = 0 x0=0,则 a ≥ x 0 + 100 a \ge x_0 + 100 ax0+100,建立一条从 x 0 x_0 x0到a边权为100的边。其实代码中不需要真实建立出来

  • 关于差分约束的三道题目的对比:

(1)AcWing 1169. 糖果:关于这一题的讲解网址;这一题的边权是0或者1,其实是负数也可以,差分约束可以求解任意边权的问题。

(2)AcWing 368. 银河:关于这一题的讲解网址;抽象之后,这一题和AcWing 1169. 糖果是一模一样的,边权是0或者1,只要边权都是非负的,然后让我们求最长路,我们就可以使用有向图的强联通分量求解(如果边权都是非正的,然后让我们求最短路,我们也可以使用有向图的SCC求解)。

(3)AcWing 1192. 奖金:也就是本题,因为所有的边权都是相同的(为1),直接使用拓扑排序求解即可,若拓扑排序无解,说明图中一定含有正环,无解;否则有解的话说明是拓扑图,我们可以直接递推求解最长路径。

  • 总结:这三题判断是否有解的方式不同,第(1)题使用spfa判断是否存在正环,第(2)题使用有向图的tarjan算法判断是否存在正环,第(3)题使用是否存在拓扑排序判断是否存在正环。如果可以确定图是DAG,那么就可以使用递推求解最长路或者最短路(边权是正是负无所谓)。

  • 边权是任意值的情况下使用拓扑排序也可以判断出图中是否存在环,但是不能判断出是存在正环还是负环。

  • 值得一提的是我们并不需要建立虚拟源点 x 0 x_0 x0,因为有解的话图一定是DAG,可以将所有点的初始距离初始化为100,然后递推求解即可。

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 10010, M = 20010;

int n, m;
int h[N], e[M], ne[M], idx;  // 权重只可能为1,因此不用存储
int q[N];  // 普通队列
int d[N];  // 入度
int dist[N];  // 距离

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

bool topsort() {
    
    int hh = 0, tt = -1;
    for (int i = 1; i <= n; i++)
        if (!d[i]) 
            q[++tt] = i;
    
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (--d[j] == 0)
                q[++tt] = j;
        }
    }
    
    return tt == n - 1;
}

int main() {
    
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    while (m--) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(b, a);
        d[a]++;
    }
    
    if (!topsort()) puts("Poor Xed");
    else {
        // 递推求最长路径
        for (int i = 1; i <= n; i++) dist[i] = 100;
        
        for (int i = 0; i < n; i++) {
            int j = q[i];
            for (int k = h[j]; ~k; k = ne[k])
                dist[e[k]] = max(dist[e[k]], dist[j] + 1);
        }
        
        int res = 0;
        for (int i = 1; i <= n; i++) res += dist[i];
        
        printf("%d\n", res);
    }
    
    return 0;
}

AcWing 164. 可达性统计

问题描述

分析

  • 因为该图是一个有向无环图,因此我们可以在该图上使用DP求解,其实AcWing 1192. 奖金这个题递推过程本质上也是DP。
  • 为什么DAG(有向无环图)上可以使用DP呢?这是因为DAG中不存在环,也就不存在状态间的循环依赖问题,俗称没有后效性
  • DP本质上就是有向无环图上的最短路问题。

在这里插入图片描述

  • 我们在该DAG上求一遍拓扑排序,然后根据拓扑排序的逆序求解每一个 f [ i ] f[i] f[i]即可。
  • 我们怎么表示每个集合呢?我们可以使用一个二维bool数组表示每个点可以达到的点,因为点数最多为三万,二维数组的空间达到了九亿,空间太大,另外时间复杂度大约是 O ( n 2 ) O(n^2) O(n2)量级的,也会超时,因此不能使用该策略。
  • 我们可以使用C++中的STL容器,即bitset进行求解,因为int是32为,时间复杂度和空间复杂度都降为了大约三千万的量级,可行,关于C++的STL容器的使用,可以参考如下网址:C++常用STL及常用库函数

代码

  • C++
#include <iostream>
#include <cstring>
#include <bitset>

using namespace std;

const int N = 30010, M = 30010;

int n, m;
int h[N], e[M], ne[M], idx;
int q[N];
int d[N];
bitset<N> f[N];

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void topsort() {
    
    int hh = 0, tt = -1;
    for (int i = 1; i <= n; i++)
        if (!d[i])
            q[++tt] = i;
    
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (--d[j] == 0)
                q[++tt] = j;
        }
    }
}

int main() {
    
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);
    
    for (int i = 0; i < m; i++) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
        d[b]++;
    }
    
    topsort();
    
    for (int i = n - 1; i >= 0; i--) {
        int j = q[i];
        f[j][j] = 1;
        for (int k = h[j]; ~k; k = ne[k])
            f[j] |= f[e[k]];
    }
    
    for (int i = 1; i <= n; i++) printf("%d\n", f[i].count());
    
    return 0;
}

AcWing 456. 车站分级

问题描述

分析

  • 对于每一个车次,都会在某个连续的区间运行,在这个区间内部(包含端点)有些车站会停车(这些车站的集合记为A),有些车站不会停车(这些车站的集合记为B),则A中所有元素的级别都严格大于B中车站的级别,即 a i ≥ b i + 1 a_i \ge b_i + 1 aibi+1 a i ∈ A , b i ∈ B a_i \in A, b_i \in B aiA,biB,另外还有 a i ≥ 1 a_i \ge 1 ai1,可以看出这是一个差分约束问题。
  • 于是问题变为了让我们求解在满足上述两个不等式的条件下,在所有可能的车站编号方式(可能存在多种编号方式),对于每种编号方式里的所有车站编号都会存在一个最大值,在这些最大值中找到最小的那一个。
  • 对于每一个车次,根据不等式,我们可以从 b i b_i bi a i a_i ai连一条边权为1的边。对于所有的车次都这样操作,我们就可以得到一个图。在这个图上求最长路即可,此时我们会得到一种最优的编号方式,即会得到每个车次的最小编号,取最大的那一个即可。
  • 至此,这一题本质上就是AcWing 1192. 奖金这道题,但是问题有那么简单吗?答案是问题没有那么简单。
  • 我们考虑需要建立多少条边,最多存在1000个车站,最多有1000个车次,最坏情况下对于每个车次我们需要建立 500 × 500 = 2.5 × 1 0 5 500 \times 500 = 2.5 \times 10 ^ 5 500×500=2.5×105条边,因此1000个车次需要建立 2.5 × 1 0 8 2.5 \times 10^8 2.5×108条边,这对于内存以及时间都是不可接受的,因此我们需要优化这一题的建边方式。
  • 对于每个车次,我们不直接让集合B中所有的点连向集合A中所有的点,而是在两者之间建立一个虚拟源点,这样 500 × 500 500 \times 500 500×500就可以变成 500 + 500 500+500 500+500了,这样时间和空间都可以接收了,如下示意图:

在这里插入图片描述

  • 在这个新图上求最长路即可。注意,假设图中有n个点,我们用1~n表示原来的点,代码实现中,使用n+i表示第i个虚拟源点(i从1开始)。
  • AcWing 1192. 奖金这道题类似,递推的过程中我们并不需要在代码中显示的建立超级源点(根据 a i ≥ 1 a_i \ge 1 ai1可以建立超级源点),因为有解的话图一定是DAG,可以将所有点的初始距离初始化为100,然后递推求解即可。
  • 考虑点数和边数的最大值,因为每个车次都要建议一个虚拟源点(注意和超级源点的区别),因此点数最多为2000;每个车次最多建立1000条边,因此最多建立一百万条边。

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 2010, M = 1000010;

int n, m;  // n个火车站, m个车次
int h[N], e[M], w[M], ne[M],idx;
int q[N];  // 拓扑排序使用的队列
int d[N];  // 每个点的入度
int dist[N];  // 每个点距离超级源点的最长距离
bool st[N];  // 记录每个车次中哪些车站需要停车

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
    d[b]++;  // b的入度加1
}

void topsort() {
    
    int hh = 0, tt = -1;
    for (int i = 1; i <= n + m; i++)
        if (!d[i])
            q[++tt] = i;
            
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (--d[j] == 0)
                q[++tt] = j;
        }
    }
}

int main() {
    
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);
    
    for (int i = 1; i <= m; i++) {
        
        memset(st, 0, sizeof st);
        int cnt;  // 当前车次经停的车站数量
        scanf("%d", &cnt);
        int start = n, end = 1;  // 起始站和终点站
        while (cnt--) {
            int stop;
            scanf("%d", &stop);
            start = min(start, stop);
            end = max(end, stop);
            st[stop] = true;
        }
        
        int ver = n + i;  // 虚拟源点
        for (int j = start; j <= end; j++)
            if (!st[j]) add(j, ver, 0);  // 集合B向ver连一条权值为0的边, 可以参考上面的分析
            else add(ver, j, 1);  // 从ver向会停车的集合A连一条权值为1的边
    }
    
    topsort();
    
    // 递推求最长路
    for (int i = 1; i <= n; i++) dist[i] = 1;
    for (int i = 0; i < n + m; i++) {  // 拓扑序列在q[0]~q[n+m-1]中
        int j = q[i];
        for (int k = h[j]; ~k; k = ne[k])  // 边(j, e[k])
            dist[e[k]] = max(dist[e[k]], dist[j] + w[k]);
    }
    
    int res = 0;
    for (int i = 1; i <= n; i++) res = max(res, dist[i]);
    
    printf("%d\n", res);
    
    return 0;
}

3. 力扣上的拓扑排序题目

Leetcode 0207 课程表

问题描述

分析

  • 直接使用拓扑排序即可。

代码

  • C++
/**
 * 执行用时:44 ms, 在所有 C++ 提交中击败了35.62%的用户
 * 内存消耗:12.8 MB, 在所有 C++ 提交中击败了28.56%的用户
 */
class Solution {
public:
    bool canFinish(int n, vector<vector<int>> &edges) {

        vector<vector<int>> g(n);
        vector<int> d(n);  // 存储入度
        for (auto &e : edges) {
            int b = e[0], a = e[1];
            g[a].push_back(b);
            d[b]++;
        }

        queue<int> q;
        for (int i = 0; i < n; i++)
            if (d[i] == 0)
                q.push(i);

        int cnt = 0;
        while (q.size()) {
            auto a = q.front();
            q.pop();
            cnt++;
            for (auto b : g[a])
                if (--d[b] == 0)
                    q.push(b);
        }
        return cnt == n;
    }
};
  • Java
/**
 * Date: 2021/1/31 10:55
 * 执行用时:5 ms, 在所有 Java 提交中击败了72.23%的用户
 * 内存消耗:39.4 MB, 在所有 Java 提交中击败了22.93%的用户
 */
public class Solution {
    public boolean canFinish(int n, int[][] edges) {

        List<List<Integer>> g = new ArrayList<>();
        for (int i = 0; i < n; i++) g.add(new ArrayList<>());
        int[] d = new int[n];  // 存储入度
        for (int[] e : edges) {
            int b = e[0], a = e[1];
            g.get(a).add(b);
            d[b]++;
        }

        Queue<Integer> q = new LinkedList<>();
        for (int i = 0; i < n; i++)
            if (d[i] == 0)
                q.add(i);

        int cnt = 0;
        while (!q.isEmpty()) {
            int a = q.remove();
            cnt++;
            for (int b : g.get(a))
                if (--d[b] == 0)
                    q.add(b);
        }
        return cnt == n;
    }
}

Leetcode 0210 课程表 II

问题描述

分析

  • 直接使用拓扑排序即可。

代码

  • C++
/**
 * 执行用时:20 ms, 在所有 C++ 提交中击败了98.80%的用户
 * 内存消耗:13 MB, 在所有 C++ 提交中击败了96.10%的用户
 */
class Solution {
public:
    vector<int> findOrder(int n, vector<vector<int>> &edges) {

        vector<vector<int>> g(n);
        vector<int> d(n);  // 入度
        for (auto &e : edges) {
            auto b = e[0], a = e[1];
            g[a].push_back(b);
            d[b]++;
        }

        queue<int> q;
        for (int i = 0; i < n; i++)
            if (d[i] == 0)
                q.push(i);

        vector<int> res;
        while (q.size()) {
            auto a = q.front();
            q.pop();
            res.push_back(a);
            for (auto b : g[a])
                if (--d[b] == 0)
                    q.push(b);
        }
        if (res.size() < n) res = {};
        return res;
    }
};
  • Java
/**
 * Date: 2021/1/31 11:06
 * 执行用时:5 ms, 在所有 Java 提交中击败了76.64%的用户
 * 内存消耗:39.7 MB, 在所有 Java 提交中击败了40.87%的用户
 */
public class Solution {
    public int[] findOrder(int n, int[][] edges) {

        List<List<Integer>> g = new ArrayList<>();
        for (int i = 0; i < n; i++) g.add(new ArrayList<>());
        int[] d = new int[n];  // 存储入度
        for (int[] e : edges) {
            int b = e[0], a = e[1];
            g.get(a).add(b);
            d[b]++;
        }

        Queue<Integer> q = new LinkedList<>();
        for (int i = 0; i < n; i++)
            if (d[i] == 0)
                q.add(i);

        int[] res = new int[n];
        int cnt = 0;
        while (!q.isEmpty()) {
            int a = q.remove();
            res[cnt++] = a;
            for (int b : g.get(a))
                if (--d[b] == 0)
                    q.add(b);
        }
        if (cnt < n) return new int[]{};
        return res;
    }
}

Leetcode 1203 项目管理

问题描述

分析

  • 因为本题有要求:同一小组的项目,排序后在列表中彼此相邻。,所以我们必须做两次拓扑排序,如果没有这个要求,我们直接根据项目之间的关系求解一遍即可。
  • 首先我们需要将每个组看成一个点,组之间的边根据项目之间的关系确定,然后对组进行拓扑排序;
  • 如果上面发现没有矛盾(即组间存在拓扑序),之后在每个组内进行拓扑排序,得到最终的结果。

代码

  • C++
/**
 * 超出时间限制
 */
class Solution {
public:
    vector<int> topoSort(int deg[], vector<int> items, vector<vector<int>> g) {
        queue<int> q;
        for (int id : items) {
            if (!deg[id])
                q.push(id);
        }

        vector<int> res;
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            res.push_back(u);
            for (int v : g[u])
                if (--deg[v] == 0)
                    q.push(v);
        }

        if (res.size() != items.size()) res.clear();
        return res;
    }

    vector<int> sortItems(int n, int m, vector<int> &group, vector<vector<int>> &beforeItems) {

        vector<vector<int>> groupItem(n + m);  // 项目分组

        int t = m;  // 新的组号从m开始,不会和原来的组号(0~m-1)冲突
        for (int i = 0; i < group.size(); i++) {
            if (group[i] == -1) group[i] = t++;  // 说明项目i 单独一组
            groupItem[group[i]].push_back(i);  // 同一组的放在一起
        }

        vector<vector<int>> in(n);  // 组内拓扑关系
        vector<vector<int>> out(n + m);  // 组间拓扑关系

        // 拓扑排序需要的入度
        int degIn[n];  // 组内入度
        int degOut[n + m];  // 组间入度
        memset(degIn, 0, sizeof(degIn));
        memset(degOut, 0, sizeof(degOut));

        // 建图、求入度
        for (int to = 0; to < beforeItems.size(); ++to) {
            vector<int> before = beforeItems[to];  // 项目to所依赖的项目
            int toId = group[to];  // 当前项目to所属的组id
            for (int from : before) {
                if (group[from] == toId) {  // 同一组内拓扑排序
                    degIn[to]++;
                    in[from].push_back(to);  // 组内建图,添加边(from, to)
                } else {
                    degOut[toId]++;
                    out[group[from]].push_back(toId);  // 组间建图,添加边(group[from], toId)
                }
            }
        }

        // 组间拓扑排序
        vector<int> groupId;
        for (int i = 0; i < n + m; i++) groupId.push_back(i);
        vector<int> outSort = topoSort(degOut, groupId, out);
        if (outSort.empty()) return vector<int>{};  // 说明组间存在环

        // 组内拓扑排序
        vector<int> res;
        for (int gid : outSort) {  // 根据组间拓扑序遍历每一组
            vector<int> items = groupItem[gid];
            if (items.empty()) continue;
            vector<int> inSort = topoSort(degIn, items, in);
            if (inSort.empty()) return vector<int>{};  // 说明组内存在环
            for (int i : inSort) res.push_back(i);
        }

        return res;
    }
};
  • Java
/**
 * Date: 2021/1/12 16:44
 * 分为组间拓扑排序 和 组内拓扑排序,先组间后组内
 * 执行用时:48 ms, 在所有 Java 提交中击败了46.58%的用户
 * 内存消耗:58.7 MB, 在所有 Java 提交中击败了52.17%的用户
 */
public class Solution {
    // deg : 入度, items : 图中的点, g : 图
    private List<Integer> topoSort(int[] deg, List<Integer> items, List<List<Integer>> g) {
        Queue<Integer> q = new LinkedList<>();
        for (Integer id : items) {
            if (deg[id] == 0)
                q.add(id);
        }

        List<Integer> res = new ArrayList<>();
        while (!q.isEmpty()) {
            int u = q.remove();
            res.add(u);
            for (int v : g.get(u))
                if (--deg[v] == 0)
                    q.add(v);
        }

        // 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
        return res.size() == items.size() ? res : new ArrayList<>();
    }

    public int[] sortItems(int n, int m, int[] group, List<List<Integer>> beforeItems) {

        List<List<Integer>> groupItem = new ArrayList<>();  // 项目分组
        for (int i = 0; i < n + m; i++) groupItem.add(new ArrayList<>());  // 初始化小组

        int t = m;  // 新的组号从m开始,不会和原来的组号(0~m-1)冲突
        for (int i = 0; i < group.length; i++) {
            if (group[i] == -1) group[i] = t++;  // 说明项目i 单独一组
            groupItem.get(group[i]).add(i);  // 同一组的放在一起
        }

        List<List<Integer>> in = new ArrayList<>();  // 组内拓扑关系
        List<List<Integer>> out = new ArrayList<>();  // 组间拓扑关系
        for (int i = 0; i < n; i++) in.add(new ArrayList<>());  // 组内项目编号(0~n-1)
        for (int i = 0; i < n + m; i++) out.add(new ArrayList<>());  // 组间编号最大到 n+m-1

        // 拓扑排序需要的入度
        int[] degIn = new int[n];  // 组内入度
        int[] degOut = new int[n + m];  // 组间入度

        // 建图、求入度
        for (int to = 0; to < beforeItems.size(); to++) {
            List<Integer> before = beforeItems.get(to);  // 项目to所依赖的项目
            int toId = group[to];  // 当前项目to所属的组id
            for (Integer from : before) {
                if (group[from] == toId) {  // 同一组内拓扑排序
                    degIn[to]++;
                    in.get(from).add(to);  // 组内建图,添加边(from, to)
                } else {
                    degOut[toId]++;
                    out.get(group[from]).add(toId);  // 组间建图,添加边(group[from], toId)
                }
            }
        }

        List<Integer> groupId = new ArrayList<>();  // 所有组id,可能组内没有顶点
        for (int i = 0; i < n + m; i++) groupId.add(i);
        List<Integer> outSort = topoSort(degOut, groupId, out);
        if (outSort.size() == 0) return new int[0];  // 组间无法拓扑排序

        int[] res = new int[n];
        int index = 0;
        for (Integer gid : outSort) {  // 遍历排序后的所有小组id
            List<Integer> items = groupItem.get(gid);  // 根据小组id 拿到这一小组中的所有成员
            if (items.size() == 0) continue;  // 说明这组里面没有节点
            List<Integer> inSort = topoSort(degIn, items, in);
            if (inSort.size() == 0) return new int[0];  // 组间无法拓扑排序
            for (Integer i : inSort) res[index++] = i;
        }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值