Acwing 第五章模板及详解(动态规划)

一、背包问题
二、线性DP
三、区间DP
四、计数类DP
五、数位统计DP
六、状态压缩DP
七、树形DP
八、记忆化搜索

一、背包问题

01背包问题

在这里插入图片描述

n个物品,每个物品的体积是vi ,价值是wi ,背包的容量是m mm

若每个物品最多只能装一个,且不能超过背包容量,则背包的最大价值是多少?

1、状态表示f(i,j)——需要几枚(有可能是i,或者i、j,或者i、j、t)来表示这个状态,如这里的条件是体积和包含前i个物品,于是我们可以把这两个作为状态中的i,j。而f(i,j)的值表达的则是他的属性,即这里的最大价值
集合——f(i,j)表示的是什么状态,i和j是什么,需要几个参数
属性——存的数集合的属性——如最大价值,最小代价等等

2、状态计算——如何进行状态转移
集合划分,如何把当前f(i,j)这个集合划分成更小的集合表示

第一种不含第i件物品,所以此时f(i,j) = f(i - 1, j)

第二种包含第i件物品,所以此时f(i,j)= f(i - 1,j - vi)+ wi 由于当前选法都包含第i件物品,所以我们可以去掉第i件物品,此时的他们的最大最小的排序也不会发生变化,所以找的就是在在不超过第i-1件物品内,体积不超过j-vi的最大值

最后的f(i,j)其实是他们两种情况取一个max

模板:

int n;              // 物品总数
int m;              // 背包容量
int v[N];           // 重量 
int w[N];           // 价值

// ---------------二维形式---------------
int f[N][M];    // f[i][j]表示在考虑前i个物品后,背包容量为j条件下的最大价值
for(int i = 1; i <= n; ++i) 
    for(int j = 1; j <= m; ++j)
        if(j < v[i]) f[i][j] = f[i-1][j];   //  当前重量装不进,价值等于前i-1个物品   
        else f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]); // 能装,需判断  
cout << f[n][m];

// ---------------一维形式---------------
int f[M];   // f[j]表示背包容量为j条件下的最大价值
for(int i = 1; i <= n; ++i) 
    for(int j = m; j >= v[i]; --j)
        f[j] = max(f[j], f[j - v[i]] + w[i]);           // 注意是倒序,否则出现写后读错误
cout << f[m];           // 注意是m不是n


完全背包问题
每个物品可以取任意个

题目:

有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用。第 i 种物品的费用是 c[i],价值是 w[i] 。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

在这里插入图片描述

这个问题非常类似于 01背包问题,所不同的是每种物品都有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取 0 件、取 1 件、取 2 件 ......等很多种。如果仍然按照解 01背包时的思路,令 f[i][v] 表示前 i 种物品恰放入一个容量为 v 的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程:

这跟 01背包问题一样有  O(VN)  个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态  f[i][v]  的时间是  ,总的复杂度可以认为是  ,是比较大的。 

代码:

 
#include <bits/stdc++.h>
#define N 1002
using namespace std;
 
int f[N][N];
int w[N];
int v[N];
 
int main() 
{
    int n,W; cin >> n >> W;
    for(int i=1;i<=n;i++)
    {
        cin >> w[i] >> v[i];
    }
    for ( int i = 1; i <= n; i++ ) 
    {
        for ( int j = 0; j <= W; j ++)
        {
            for (int k = 0; k*w <= j; k++) 
            {
                f[i][j] = max(f[i-1][j],f[i-1][j-w[i]*k] + v[i]*k);
            }
        }
    }
    cout << f[n][W] <<endl;
    return 0;
}

空间复杂度优化后的代码:

 
#include <bits/stdc++.h>
#define N 1002
using namespace std;
 
int f[N];
int w[N];
int v[N];
 
int main() 
{
    int n,W; cin >> n >> W;
    for(int i=1;i<=n;i++) 
    {
        cin >> w[i] >> v[i];
    }
    for ( int i = 1; i <= n; i++ ) 
    {
        for ( int j = W; j >= 0; j --) 
        {
            for (int k = 0; k*w <= j; k++) 
            {
                f[j] = max(f[j],f[j-w[i]*k] + v[i]*k);
            }
        }
    }
    cout << f[W] <<endl;
    return 0;
}

一个简单有效的优化
完全背包问题有一个很简单有效的优化,是这样的:若两件物品 i 、j 满足  且   ,则将物品 j 去掉,不用考虑。这个优化的正确性显然:任何情况下都可以将价值小费用高的 j 换成物美价廉的 i ,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。这个优化可以简单的 Θ(N2) 实现,一般都可以承受。

然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于 V 的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的哪个,可以 Θ(V + N) 地完成这个优化。

转化为01背包问题求解 
既然 01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为 01背包问题来解。最简单的想法是,考虑到第 i 种物品最多选  V/c[i]  件,于是可以把第 i 种物品转化为  V/c[i]  件费用及价值均不变的物品,然后求解这个 01背包问题。

这样完全没有改进基本思路的复杂度,但毕竟给了我们将完全背包问题转化为 01背包问题的思路:将一种物品拆成多件物品。

更高效的转化方法是:把第 i 种物品拆成费用为  、价值为   的若干件物品,其中 k 满足    。这是二进制的思想,因为不管最优策略选几件第 i 种物品,总可以表示成若干个  件物品的和。这样把每种物品拆成    件物品,是一个很大的改进。

模板:

int n;              // 物品总数
int m;              // 背包容量
int v[N];           // 重量 
int w[N];           // 价值

// ---------------二维形式---------------
// 未优化
int f[N][M];    // f[i][j]表示在考虑前i个物品后,背包容量为j条件下的最大价值
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= m; j++)
        for (int k = 0; k * v[i] <= j; k++)
            f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);


// 已优化
int f[N][M];    // f[i][j]表示在考虑前i个物品后,背包容量为j条件下的最大价值
for(int i = 1; i <= n; ++i) 
    for(int j = 1; j <= m; ++j)
        if(j < v[i]) f[i][j] = f[i-1][j];   //  当前重量装不进,价值等于前i-1个物品   
        else f[i][j] = max(f[i-1][j], f[i][j-v[i]] + w[i]); // 能装,需判断  
cout << f[n][m];

// ---------------一维形式---------------
int f[M];   // f[j]表示背包容量为j条件下的最大价值
for(int i = 1; i <= n; ++i) 
    for(int j = v[i]; j <= m; ++j)
        f[j] = max(f[j], f[j - v[i]] + w[i]);           // 注意是倒序,否则出现写后读错误
cout << f[m];           // 注意是m不是n

形式上和01背包差不多,在二维数组表示下,主要差别在
在选择第i物品时,用的是f[i][j-v]+w,而不是f[i-1][j-v]+w
上述条件决定了在每次迭代时,必须正向遍历,而不是反向遍历
在一维数组表示下,主要差别只表现为迭代的顺序(正向或反向)
在一维数组表示下,01背包只能反向是因为它主要用到上一行的数据来更新当前行数据,如果正向遍历,则会修改上一行的数据,出现写后读错误;完全背包只能正向是因为它需要用到当前行的数据更新,如果反向遍历,使用的是上一行的数据,则不符合公式
 

多重背包问题
第i 个物品至多拿si个。

二进制优化

将k化成二进制,每个二进制的代表数代表一个新的物体,用于对应倍数的体积和价钱
将k个,化成多个新物体
同时包含不同堆的情况,可以很好的表示所有可能

例子,不能选128,是因为这样可以表示超出200的数,所以选择200 - 127 = 73 ,这样可以表示。

// 读入物品个数时顺便打包
int k = 1;      // 当前包裹大小
while (k <= s)
{
    cnt ++ ;            // 实际物品种数
    v[cnt] = a * k;
    w[cnt] = b * k;
    s -= k;
    k *= 2;             // 倍增包裹大小
}
if (s > 0)
{
    // 不足的单独放一个,即C
    cnt ++ ;
    v[cnt] = a * s;
    w[cnt] = b * s;
}
n = cnt;        // 更新物品种数

// 转换成01背包问题
for (int i = 1; i <= n; i ++ )
    for (int j = m; j >= v[i]; j -- )
        f[j] = max(f[j], f[j - v[i]] + w[i]);

cout << f[m] << endl;

用二进制优化后,问题转换成01背包问题

分组背包问题

有 N 组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值

模板

int n;              // 物品总数
int m;              // 背包容量
int v[N][S];         // 重量 
int w[N][S];         // 价值
int s[N];           // 各组物品种数

// 读入数据
 for (int i = 1; i <= n; i ++ )
 {
     cin >> s[i];
     for (int j = 1; j <= s[i]; j ++ )
         cin >> v[i][j] >> w[i][j];
 }

// 处理数据
for (int i = 1; i <= n; i ++ )
    for (int j = m; j >= 1; j -- )
        for (int k = 1; k <= s[i]; k ++ )
            if (v[i][k] <= j)
                f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);

cout << f[m] << endl;

二、线性DP

数字三角形

在这里插入图片描述

 代码:

// 自顶向下(未压缩`f`)
const int INF = 1e9;

for (int i = 0; i <= n; i ++ )
    for (int j = 0; j <= i + 1; j ++ )
        f[i][j] = -INF;

f[1][1] = a[1][1];
    for (int i = 2; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            f[i][j] = max(f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j]);

int res = -INF;
for (int j = 1; j <= n; j ++ ) res = max(res, f[n][j]);

实际上可压缩f,此时只能反向遍历行
还可自底向上实现,若压缩,只能正向遍历行
可以用0x80初始化f,使得元素都小于-2e9
时间复杂度= O ( 状 态 × 转 移 ) = O (  n *n) 
最长上升子序列

在这里插入图片描述代码:

// 朴素法
for (int i = 1; i <= n; i ++ )
{
    f[i] = 1;       // 只有a[i]一个数
    for (int j = 1; j < i; j ++ )
        if (a[j] < a[i])
            f[i] = max(f[i], f[j] + 1);
}

int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, f[i]);

// 二分优化
    vector<int>stk;//模拟堆栈
    stk.push_back(arr[0]);

    for (int i = 1; i < n; ++i) {
        if (arr[i] > stk.back())        
            stk.push_back(arr[i]);          // 如果该元素大于栈顶元素,将该元素入栈
        else
            *lower_bound(stk.begin(), stk.end(), arr[i]) = arr[i];  // 替换掉第一个大于或者等于这个数字的那个数
    }
    cout << stk.size() << endl;

// yxc二分优化
int len = 0;        // 最长上升子序列长度(数组q的长度)
for (int i = 0; i < n; i ++ )
{
    // 在数组q中二分查找第1个>= a[i]的数(结果)
    int l = 0, r = len;
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (q[mid] < a[i]) l = mid;
        else r = mid - 1;
    }
    len = max(len, l + 1);      // q[l] < a[i] < q[l + 1]
    q[l + 1] = a[i];
}

printf("%d\n", len);

朴素法时间复杂度= O ( 状 态 × 转 移 ) = O ( n × n ) 
改进(贪心+二分)
令数组q保存长度为i的上升子序列末尾元素的最小值,例如125和123优先保存123的3,因为它更能接上的后缀种类更多
q[]是单调递增的,否则存在q[i-1]>q[i],说明长度为i的上升子序列的最小末尾元素比长度为i-1的还小,这与q[i-1]的定义不符
为了使上升子序列最长,应在q[]中找到<a[i]的最大q[j],使得q[j]<a[i]<q[j+1],此时子序列的长度为j+1,且q[j+1]=a[i]。这步用整数二分实现
改进后,时间复杂度变为O ( n log ⁡ n ) 
最长公共子序列

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

在这里插入图片描述

 代码:

char a[N], b[N];
int f[N][N];

for (int i = 1; i <= n; i ++ )
    for (int j = 1; j <= m; j ++ )
    {
        f[i][j] = max(f[i - 1][j], f[i][j - 1]);
        if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
    }

printf("%d\n", f[n][m]);


最短编辑距离
给定两个字符串A和B,只允许对A进行字符插入,字符删除和字符替换,求把A变成B的最少操作次数

在这里插入图片描述

 代码:

// 初始化边界
for (int i = 0; i <= m; i ++ ) f[0][i] = i;         // 把B变成空串需要删除字符的次数
for (int i = 0; i <= n; i ++ ) f[i][0] = i;         // 把空串B扩充成A需要插入字符的次数

for (int i = 1; i <= n; i ++ )
    for (int j = 1; j <= m; j ++ )
    {
        f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
        if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
        else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
    }

printf("%d\n", f[n][m]);

说明

三、区间DP

石子合并:相邻两堆石子可以合并,代价为二者石子数的和,求最小代价

 在这里插入图片描述

代码:

int s[N];           // 前缀和
int f[N][N];        

for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1];        // 初始化前缀和

for (int len = 2; len <= n; len ++ )        // len=1时不合并(类似归并排序的merge)
    // 固定窗口大小,从小到大遍历
    for (int i = 1; i + len - 1 <= n; i ++ )
    {
        // 固定窗口左端点,则可确定窗口右端点,注意边界
        int l = i, r = i + len - 1;
        // 窗口内划分
        f[l][r] = 0x7f7f7f7;    // 初始化为无穷大
        for (int k = l; k < r; k ++ )
            f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
    }

printf("%d\n", f[1][n]);

四、计数类DP

把n拆分成1~n的和的方案数(不考虑顺序)

完全背包解法

可以看做有n种物品,第i 种物品的体积为i,背包的容量为n,每个物品可以拿无数次,求装满背包的方案数

假设当前背包容量为j ,则第i 个物品至多装$k=\lfloor \frac{j}{i} \rfloor $个

代码:

// 未压缩f
f[0][0] = 1;
for (int i = 1; i <= n; i ++)
    for (int j = 0; j <= n; j ++)
        if (j >= i) f[i][j] = (f[i - 1][j] + f[i][j - i]) % mod;
        else f[i][j] = f[i - 1][j];
cout << f[n][n] << endl;

// 压缩f
f[0] = 1;
for (int i = 1; i <= n; i ++ )
    for (int j = i; j <= n; j ++ )
        f[j] = f[j] + f[j - i];
cout << f[n] << endl;

 其它算法

在这里插入图片描述


代码:

f[1][1] = 1;
for (int i = 2; i <= n; i ++ )
    for (int j = 1; j <= i; j ++ )
        f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % mod;

int res = 0;
for (int i = 1; i <= n; i ++ ) res = (res + f[n][i]) % mod;

cout << res << endl;

五、数位统计DP

多用于统计数中出现某个数的次数
关键:分类讨论

思路关键:
1、碰到求多个区间内的个数啊值啊——很有可能会可以用前缀和,所以问题就简化为从1-n的个数的问题
2、考虑从1到n的个数的问题,就可以用数学的排列组合取思考。

代码:

/*

001~abc-1, 999

abc
    1. num[i] < x, 0
    2. num[i] == x, 0~efg
    3. num[i] > x, 0~999

*/
int get(vector<int> num, int l, int r)
{
    int res = 0;
    for (int i = l; i >= r; i -- ) res = res * 10 + num[i];
    return res;
}

int power10(int x)
{
    int res = 1;
    while (x -- ) res *= 10;
    return res;
}

int count(int n, int x)
{
    if (!n) return 0;       // 特判

    // 拆分
    vector<int> num;
    while (n)
    {
        num.push_back(n % 10);
        n /= 10;
    }
    n = num.size();

    // 核心
    int res = 0;
    for (int i = n - 1 - !x; i >= 0; i -- )     // 当x=0时,从左起第2位开始遍历
    {
        // 考虑左起第1位时不存在abc,跳过
        if (i < n - 1)
        {
            res += get(num, n - 1, i + 1) * power10(i);
            if (!x) res -= power10(i);      // x=0,abc不能全为0,排除这种情况
        }
        
        // 尽管左起第1位不存在abc,但存在efg,因此保留这部分
        if (num[i] == x) res += get(num, i - 1, 0) + 1;
        else if (num[i] > x) res += power10(i);
    }

    return res;
}

for (int i = 0; i <= 9; i ++ )
    cout << count(b, i) - count(a - 1, i) << ' ';       // 类似前缀和思想

六、状态压缩DP

二进制表示状态——状态压缩
状态压缩dp——状态表示中,其中有一维是二进制状态
状态压缩问题——n不能太大,n<=20,2的n次方已经是极限了
状态压缩dp中状态表示里有一维为二进制状态表示,另外一维为其他表示
状态计算:
1、他可能由几种状态转变过来
2、假设由k状态转变过来,k状态是否合法(是否与当前状态有冲突,要满足k状态为当前状态前驱的条件)
3、判断题目是求和还是最大或者最小

蒙德里安的梦想

在这里插入图片描述

代码:

最短Hamilton路径
给定一张 n 个点的带权无向图,点从 0 ~n − 1 标号,求起点 0  到终点 n − 1  的最短Hamilton路径。 Hamilton路径的定义是从 0 到n − 1  不重不漏地经过每个点恰好一次。
在这里插入图片描述

将f[i][j]所表示的集合中的所有路线,按倒数第二个点分成若干类,其中第k类是指倒数第二个点是k的所有路线。那么f[i][j]的值就是每一类的最小值,再取个min。而第k类的最小值就是f[i - (1 << j)][k] + w[k][j]。

从定义出发,最后f[(1 << n) - 1][n - 1]就表示所有“经过了所有点,且最后位于点n-1的路线”的长度的最小值,也就是我们要求的答案。
 

代码:

memset(f, 0x3f, sizeof f);              // 初始化为无穷大
f[1][0] = 0;    // 表示只有起点0且最后位于起点0的路线的长度是0,此时点集i的最后一位是1,其余为0,因为点集只有起点0,故i=1

for (int i = 0; i < 1 << n; i ++ )          // 穷举所有可能的点集
    for (int j = 0; j < n; j ++ )           // 从当前点集找一个点(二进制串中位为1的位置)
        if (i >> j & 1)
            for (int k = 0; k < n; k ++ )    // 从当前点集找另外一个点(可以和之前找的相同)
                if (i >> k & 1)
                    f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);   // 尝试从后找的点到达点j

cout << f[(1 << n) - 1][n - 1];     // 所有点都在点集,且终点是n-1

最短哈密顿距离

哈密顿距离,通过所有点且不重复的路径。

代码:

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 20, M = 1 << N;

int n;
int w[N][N];
int f[M][N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            cin >> w[i][j];

    memset(f, 0x3f, sizeof f);
    f[1][0] = 0;

    for (int i = 0; i < 1 << n; i ++ )
        for (int j = 0; j < n; j ++ )
            if (i >> j & 1)
                for (int k = 0; k < n; k ++ )
                    if (i >> k & 1)
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);

    cout << f[(1 << n) - 1][n - 1];

    return 0;
}

七、树形DP

1、题目的信息结构为树形
2、树形搜索更新状态
3、状态一般表示为f(当前节点,是否包含当前节点)也可以是其他状态
4、根据子节点和父节点关系更新状态

给定一个带结点值的树,求一个结点集合,使得集合里任意两个结点都不相邻,且结点值的和最大

 在这里插入图片描述

代码:

int n;
int h[N], e[N], ne[N], idx;     
int happy[N];
int f[N][2];
bool has_fa[N];         // 标记是否存在父节点

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

void dfs(int u)
{
    f[u][1] = happy[u];     // 选取根节点的初值为自身的幸福度

    // 遍历子树
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];       // 子结点
        dfs(j);             // 递归子结点

        // 状态转移
        f[u][1] += f[j][0]; 
        f[u][0] += max(f[j][0], f[j][1]);
    }
}

// 核心
memset(h, -1, sizeof h);            // 初始化邻接表头指针

// 读入树结构
for (int i = 0; i < n - 1; i ++ )
{
    int a, b;
    scanf("%d%d", &a, &b);
    add(b, a);                      // 尽管是无向图,但只需要保留一条边(上司指向下属)
    has_fa[a] = true;               // 标记存在父节点
}

// 找树根,不存在父节点的就是树根
int root = 1;
while (has_fa[root]) root ++ ;

dfs(root);      // 从根节点开始遍历

printf("%d\n", max(f[root][0], f[root][1]));

说明

  • 使用邻接表表示树
  • DFS+动态规划

八、记忆化搜索

记忆化搜索

1、用f【】【】来记录状态下的搜索结果
2、记录该状态是否被搜索过——可以给f【】【】赋初值为-1,如果没有则为-1
3、搜索——递推的方式
4、搜索到结果返回的时候,两者间状态的关系,即父节点和子节点对于f数组的关系

给一个矩阵,求一条路径,使得它的长度最长,且路径上的值是递减的。(只能往上下左右移动)

在这里插入图片描述

记忆化搜索指保存中间结果,避免重复计算,用空间换时间

代码

int g[N][N];
int f[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

int dp(int x, int y)
{
    if (f[x][y] != -1) return f[x][y];      // 已经计算过,不必重复计算(记忆化搜索)
    
    f[x][y] = 1;
    for (int i = 0; i < 4; i++) {
        int a = x + dx[i], b = y + dy[i];
        if (g[a][b] < g[x][y]) 
            f[x][y] = max(f[x][y], dfs(a, b) + 1);
    }
    
    return f[x][y];
}

memset(g, 0x3f, sizeof g);      // 边界为无穷大,不能滑到,可以省去边界判断
memset(f, -1, sizeof f);

int res = 0;
for (int i = 1; i <= n; i ++ )
    for (int j = 1; j <= m; j ++ )
        res = max(res, dp(i, j));

关键:很多时候对于状态的初始化!!!!
考虑如果只有一个的时候的情况或者边界的情况,或者最大最小值的影响

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值