常见动态规划问题

最长上升子序列LIS

这类问题的特点:
1.目标序列 是 原序列某些数拎出来组成的,不是原序列的某个连续的子序列。

题意理解:

给定一个数列,求最长严格递增子序列的长度。
//注意:严格递增不等同于非降序!严格递增中间不能出现等于的情况。

题解:

第一种方法:动规

算法思路:
1.考虑到输入的数据是一个一维数组,所以设置一个一维的dp表。
2.dp[i]:定义为考虑前 i 个元素,以第 i 个元素为结尾的最长上升子序列的长度。
3.状态转移方程为:
// 状态转移方程 基于 dp 的定义而来:
在这里插入图片描述
4.最终要解的答案 最长的上升子序列的长度 即为 dp[]数组中的最大值。

算法理解:
1.以i元素为结尾的最长上升子序列的长度,可以由以j元素为结尾的最长上升子序列长度转化而来。(递推)
2.答案(最长上升子序列长度)存在dp[i]中。

代码实现:

#include <bits/stdc++.h>
using namespace std;

const int N = 2550;
int n, nums[N], dp[N], Max = 0;  // dp[i] 的 定义:在nums[1]~nums[i]中以nums[i]为结尾的最长上升子序列的长度 

int main()
{
	cin >> n;                         // 输入 
	for(int i = 1; i <= n; i++)
		cin >> nums[i];
	memset(dp, 0, sizeof(dp)); 
	for(int i = 1; i <= n; i++)  // 遍历数组 nums[] 
	{
		for(int j = i - 1; j >= 1; j--)  // 遍历前i-1个数 
		{
			if(nums[j] < nums[i])
				dp[i] = max(dp[i], dp[j] + 1);
		}
		if(dp[i] == 0)  // 细节:如果没有可作为nums[i]前驱的元素,则 dp[i] = 1; 
			dp[i] = 1;
		Max = max(dp[i], Max);  // Max 保存 dp[] 中的最大值 
	}
	cout << Max << endl;
//	for(int i = 1; i <= n; i++)   // 调试语句 
//		cout << dp[i] << ' ';
//	cout << endl;
	return 0;
}

算法分析:
时间复杂度:O(n^2) // 该算法中有两层循环
空间复杂度:O(n) // 开了额外的辅助空间 dp 数组

第二种方法:动规+贪心

算法思路:
首先,我们把思考点焦距在我们要求解的东西:最长上升序列的长度。
基于这个目标,我们可以想到一种合理的说法:

如果我们要使上升子序列尽可能的长,那么我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。

怎么去实现我们这一想法?
维护一个一维数组dp:dp[len]维护长度为len的最长上升子序列的末尾元素的最小值。
// 注:初始时,dp[1] = nums[1]。
// 注:易知,dp[len]是单调递增的。即 当len1 < len2,则 dp[len1] < dp[len2],故可考虑用高效的二分。

代码实现:

#include <bits/stdc++.h>
using namespace std;

const int N = 2550;
int n, nums[N], dp[N];  // dp[len]表示长度为len的最长上升子序列的末尾元素的最小值 

int main()
{
	cin >> n;                      // 输入 
	for(int i = 1; i <= n; i++)
		cin >> nums[i];
	dp[1] = nums[1];
    int len = 1;
	for(int i = 2; i <= n; i++)  // 遍历 nums[2]~nums[n] 
	{
        int p=lower_bound(dp+1,dp+len+1,nums[i])-dp; //查找大于nums[i]的第一个元素
        if(p==len+1)
            dp[++len]=nums[i]; // 增长序列长度
        else
            dp[p]=nums[i]; // 对长度为p的序列的末尾元素进行更新
	}
    
	cout << len << endl;
	return 0;
}

算法分析:
时间复杂度:O(nlogn)
// lower_bound()是O(logn)的时间复杂度,再加最外层的for循环,总的时间复杂度是:O(nlongn)
空间复杂度:O(n)
// 用了额外的辅助空间 dp[]

对第二种方法再理解:

1.最终得到的 dp[] 序列不一定是一个 可行解(譬如对于数列:2 4 10 3 最终得到的 dp[] 序列是:2 3 10 这个不是一个可行解

2.dp[]数组的作用是放小元素,使得序列增得比较缓,譬如对于序列:2 9 10 3 9 10 正是有dp[]数组的存在,才可能得到序列:2 3 9 10。否则,可能得到 2 9 10 , 后面的元素舍弃。
// 虽然 3 被安插进来,看着很奇怪,但是,正是要有 3 被安插进来,才可能使得后面能排进 9 和 10

思考总结:

1.用动态规划算法,一定要用到dp表去记录不同阶段的信息,以此根据之前的信息实现状态转移。

2.所谓 动态规划 大概可以理解为在 动中规划(动:可以是指遍历数组的过程~~~规划 可以理解是 怎么处理 动 中的 每一个阶段)。

参考资料:

https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-by-leetcode-soluti/

关于dp[]单调递增的证明:

假如dp[j] >= dp[i] 且 j <i
考虑从长度为 i 的最长上升子序列的末尾删除 i−j 个元素,那么这个序列长度变为 j ,且第 j 个元素 x(末尾元素)必然小于 dp[i]
根据dp[j] >= dp[i],所以也就小于 dp[j]
这样我们就找到了一个长度为 j 并且末尾元素比 dp[j] 小的最长上升子序列,从而产生了矛盾。(根据定义)
因此数组 dp 的单调性得证。

最长上升子序列LIS问题-同类题型:
HDU-1087 Super Jumping! Jumping! Jumping!
状态定义:
dp[i]:前i个元素,以第i个元素为结尾得到的分数的最大值

状态转移方程:
当a[j] < a[i]时,dp[i]=max(dp[j]+a[i],dp[i]);

参考代码:

#include <bits/stdc++.h>
using namespace std;

#define ll long long 
const int N=1e3+10;
ll n,a[N],dp[N];

int main()
{
	while(cin >> n)
	{
		if(n==0) break;
		memset(dp,0,sizeof dp);
		for(int i=1;i<=n;++i) cin >> a[i];
		dp[1]=a[1];
		ll mx=dp[1];
		for(int i=2;i<=n;++i)
		{
			dp[i]=a[i]; // WA了一发,这里忘记赋值 
			for(int j=1;j<i;++j)
			{
				if(a[j] < a[i])
					dp[i]=max(dp[j]+a[i],dp[i]);	
			}
			mx=max(mx,dp[i]);	
		}
		cout << mx << endl;	
	}
}

这类问题的注意点:
1.当不满足条件时,dp[]也需要给他附上值,所以可以直接一开始时,附上初值。
2.它的解是在中间某个状态产生的,不是dp[n]。

(类似)最长上升子序列LIS问题-同类题型:

HDU-1069 Monkey and Banana

给定n种方块类型,(xi,yi,zi),每种类型的方块有无限块。现在要将若干个方块叠起来,要求在下面的方块的底面的长和宽都大于上面方块的底面的长和宽。问可以得到的最大高度。

状态定义:
dp[i]:前i个元素,以第i个元素为结尾得到的高度的最大值

状态转移方程:
if(v[j].fi.fi>v[i].fi.fi && v[j].fi.se>v[i].fi.se) dp[i]=max(dp[i],dp[j]+v[i].se);

参考代码:

#include <bits/stdc++.h>
using namespace std;

#define ll long long
#define fi first
#define se second
ll idx,cnt,n,dp[200];
vector<pair<pair<ll,ll>,ll>> v;

void add(ll t1,ll t2,ll t3)
{
	cnt++;
	v.push_back({{t1,t2},t3});
}

bool cmp(pair<pair<ll,ll>,ll> x,pair<pair<ll,ll>,ll> y)
{
	if(x.fi.fi==y.fi.fi)
	{
		if(x.fi.se==y.fi.se)
			return x.se>y.se;
		return x.fi.se>y.fi.se;
	}
	return x.fi>y.fi;
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	while(cin >> n)
	{
		if(n==0) break;
		cnt=0;
		idx++;
		memset(dp,0,sizeof dp);
		v.clear();
		for(int i=1;i<=n;++i)
		{
			long long t1,t2,t3;
			cin >> t1 >> t2 >> t3;
			add(t1,t2,t3); //这里也可以只放三组,需要排一下序
			add(t1,t3,t2);
			add(t2,t1,t3);
			add(t2,t3,t1);
			add(t3,t1,t2);
			add(t3,t2,t1);
		}
		ll mx=0;
		sort(v.begin(),v.end(),cmp);
		for(int i=0;i<cnt;++i)
		{
			dp[i]=v[i].se;
			for(int j=1;j<i;++j)
				if(v[j].fi.fi>v[i].fi.fi && v[j].fi.se>v[i].fi.se)
					dp[i]=max(dp[i],dp[j]+v[i].se);
			mx=max(dp[i],mx);
		}
		printf("Case %d: maximum height = %lld\n",idx,mx);
	}
	return 0;
}

同样地:我们看到这道题也是:1.不符合条件也需要赋值。 2.最优解是在中间某个状态产生的。

最长公共子序列LCS问题:

这类问题的提法一般是:给定两个数组(或字符串),求这两个数组(或字符串)的最长公共子序列。

关于最长公共子序列问题有以下两种类型:

最长公共子序列(数字)

在网上看到有一篇博客讲这个讲得非常得好,分享:https://blog.csdn.net/hrn1216/article/details/51534607

一点说明:输入的两个数列分别用s1[]和s2[]存储,下标分别是从1 ~ n 和 1 ~ m

算法思路:
1.由于输入有两个序列,故考虑用二维数组dp[][]存放动规过程的重要信息。定义dp[i][j]数组的含义为:s1[1 ~ i]s2[1 ~ j] 的最长公共子序列的长度。
2.对问题进行分析:
假设问题的最优解为 Z={z1, z2, ..., zk}zk = s1[i] = s2[j],那么 dp[i][j] = dp[i - 1][j - 1] + 1;否则,dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])

故:
根据分析和定义可以得到如下的状态转移关系:
在这里插入图片描述
代码实现:
第一种方法:非递归实现:

#include <bits/stdc++.h>
using namespace std;

const int N = 1100;
int n, m, s1[N], s2[N], dp[N][N]; 

int main()
{
	cin >> n >> m;
	for(int i = 1; i <= n; i++)
		cin >> s1[i];	
	for(int i = 1; i <= m; i++)
		cin >> s2[i];
	for(int i = 1; i <= n; i++)   // 遍历s1[]     // 非递归写法! 
	{
		for(int j = 1; j <= m; j++)   // 遍历s2[] 
		{
			if(s1[i] == s2[j])
				dp[i][j] = dp[i - 1][j - 1] + 1;
			else
				dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
		}
	}
	cout << dp[n][m] << endl;
	// 输出轨迹 (我是倒着输出的) 
	int i = n, j = m;
	while(i >= 1 && j >= 1)
	{
		if(s1[i] == s2[j])
		{
			cout << s1[i] << ' ';
			i--;
			j--;
		}
		else
		{
			if(dp[i - 1][j] > dp[i][j - 1])
				i--;
			else
				j--;
		}
	}
	return 0;
}

算法分析:
时间复杂度:O(nm) // 两层循环
空间复杂度:O(n^2) // 开了一个二维的辅助数组 dp[][]

第二种方法:递归实现:

#include <bits/stdc++.h>
using namespace std;

const int N = 1100;
int n, m, s1[N], s2[N], dp[N][N]; 
int dg(int x, int y);

int main()
{
	cin >> n >> m;
	for(int i = 1; i <= n; i++)
		cin >> s1[i];	
	for(int i = 1; i <= m; i++)
		cin >> s2[i];
	dg(n, m);
	cout << dp[n][m] << endl;
	// 输出轨迹 (我是倒着输出的) // 可以通过某种方法把它正序输出,譬如先存在stack里,再最后输出
	int i = n, j = m;
	while(i >= 1 && j >= 1)
	{
		if(s1[i] == s2[j])
		{
			cout << s1[i] << ' ';
			i--;
			j--;
		}
		else
		{
			if(dp[i - 1][j] > dp[i][j - 1])
				i--;
			else
				j--;
		}
	}
	return 0;
}

int dg(int x, int y)      // 递归写法 
{
	if(x == 0 || y == 0)
	{
		dp[x][y] = 0;
		return 0;
	}
	if(dp[x][y] != 0)   // 如果子问题之前计算过,直接返回就行,避免子问题重复计算! 
		return dp[x][y];
	if(s1[x] == s2[y])  // 仍是围绕状态转移方程 
		dp[x][y] = dg(x - 1, y - 1) + 1;
	else
		dp[x][y] = max(dg(x - 1, y), dg(x, y - 1));
	return dp[x][y];
}

算法分析:
时间复杂度:O(n) //还不会分析 // 但是,根据输出,递归实现的求解的子问题少了
空间复杂度:O(nm) // 开了一个二维数组dp[][]

最长公共子序列(字符串)

原题链接:https://leetcode-cn.com/problems/longest-common-subsequence/

一点说明:输入的两个字符串分别用s1[]和s2[]存储,下标分别是从 0 ~ (n-1) 和0 ~ (m-1)

算法思路:
// 最长公共子序列(字符串)的解题想法 和 最长公共子序列(数字)相似。
状态转移方程是一样的,如下:
在这里插入图片描述
代码实现:

第一种方法:二维数组实现

#include <bits/stdc++.h>
using namespace std;

const int N = 700;
int n, m;
string s1, s2;
int dp[N][N];

int main()
{
	cin >> s1 >> s2;
	n = s1.size() - 1;
	m = s2.size() - 1;
	memset(dp, 0, sizeof(dp));
	for(int i = 0; i <= n; i++)
	{
		for(int j = 0; j <= m; j++)
		{
			if(s1[i] == s2[j])
			{
				if(i == 0 || j == 0)
					dp[i][j] = 1;
				else
					dp[i][j] = dp[i - 1][j - 1] + 1;	
			}
			else
			{
				if(i > 0 && j > 0)
					dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
				else if(i > 0)
					dp[i][j] = dp[i - 1][j];
				else if(j > 0)
					dp[i][j] = dp[i][j - 1];
				else;
			}
		}
	}
	cout << dp[n][m] << endl;
	return 0;
}

算法分析:
时间复杂度:O(nm) // 两层循环
空间复杂度:O(nm) // 开了二维辅助空间 dp[][]

第二种方法:一维数组实现

#include <bits/stdc++.h>
using namespace std;

const int N = 700;
int n, m;
string s1, s2;

int main()
{
	cin >> s1 >> s2;
	n = s1.size() - 1;
	m = s2.size() - 1;
	int dp[N];
	memset(dp, 0, sizeof(dp));  // 初始化赋值为 0 
	int t1 = 0, t2 = 0;
	for(int i = 0; i <= n; i++)  // 遍历s1 
	{
		for(int j = 0; j <= m; j++)  // 遍历s2 
		{
			t1 = t2;   // 记录 [i-1][j-1] 
			t2 = dp[j];   // 记录 [i-1][j] 
			if(s1[i] == s2[j])
            {
                if(i == 0 || j == 0)
                    dp[j] = 1;
                else
                    dp[j] = t1 + 1;	  // [i-1][j-1] + 1 
            }
            else
            {
                if(i > 0 && j > 0)
                    dp[j] = max(t2, dp[j - 1]);
                else if(i > 0)
                    dp[j] = t2;
                else if(j > 0)
                    dp[j] = dp[j - 1];
                else;
            }
		}
	}
	cout << dp[m] << endl;
//	for(int i = 0; i <= m; i++) 
//		cout << dp[i] << endl;
	return 0;
}  

算法分析:
时间复杂度:O(nm) // 两层循环
空间复杂度:O(n) // 开了一维辅助空间 dp[]

最长公共上升子序列LCIS

简单的LCIS

#include <bits/stdc++.h>
using namespace std;

#define fir(i,n) for(int i=1;i<=n;++i)
const int N=3e3+10;
int n,a[N],b[N],dp[N][N]; //dp[i][j]:1~i,1~j,并且以b[j]为结尾的最长公共上升子序列 

int main()
{
	cin >> n;
	fir(i,n) cin >> a[i];
	fir(i,n) cin >> b[i];
	fir(i,n)
	{
		int mx=0;
		fir(j,n)
		{
			dp[i][j]=dp[i-1][j]; //要把前面的值贴过来 
			if(a[i]>b[j]) mx=max(mx,dp[i-1][j]);
			if(a[i]==b[j]) dp[i][j]=mx+1;
		}
	}
	int ans=0;
	fir(i,n)
		ans=max(ans,dp[n][i]);
	cout << ans << endl; 
}

编辑距离问题

**问题描述: **

有两个字符串A和B,字符串转化的操作有三种,分别是:(1)删除一个字符; (2)插入一个字符; (3)将一个字符改为另一个字符,即替换。 求将字符串A变换为字符串B所用的最少字符操作数。

解法:

// 这道题其实很明显地说了串间转移方法,故本道题采用动态规划来解决。

定义状态:dp[i][j] :表示 a[1~i]b[1~j] 的最小编辑距离。
// 原问题转化为:求 dp[n][m] 的值。// n 表示 A字符串 的长度;m 表示 B字符串 的长度。

定义状态转移方程:
如果 a[i] = b[j],则 c[i][j] = c[i-1][j-1]
如果 a[i] ≠ b[j],则 c[i][j] = max({c[i-1][j-1] + 1, c[i-1][j] + 1, c[i][j-1] + 1})
// 解释:c[i-1][j-1] + 1 表示修改操作的处理;c[i][j-1] + 1表示插入操作的处理;c[i-1][j] + 1 表示删除操作的处理。

代码实现:

#include <bits/stdc++.h>
using namespace std;

const int N = 2005;
string s1 = " ", s2 = " ", t1, t2;
int c[N][N];

int main()
{
	cin >> t1 >> t2;
	s1 += t1;
	s2 += t2;
	int l1 = t1.size(), l2 = t2.size();
	for(int i = 1; i <= l1; i++) //预处理 
		c[i][0] = i;
	for(int i = 1; i <= l2; i++)
		c[0][i] = i;
	for(int i = 1; i <= l1; i++)
	{
		for(int j = 1; j <= l2; j++)
		{
			if(s1[i] == s2[j])
				c[i][j] = c[i - 1][j - 1];
			else
				c[i][j] = min({c[i - 1][j - 1] + 1, c[i][j - 1] + 1, c[i - 1][j] + 1});
		}
	}
	cout << c[l1][l2] << endl;
	return 0;
}

算法复杂度分析:
时间复杂度:O(nm)
// n 表示 字符串 A 的大小;m 表示 字符串 B 的大小。算法需要两层循环,一层遍历字符串A,一层遍历字符串B。

空间复杂度:O(nm)
// 算法需要二维dp数组保留子问题的解。

心得体会:

1.对于一些问题,要学会用递归的角度、思维去认识问题以及解决问题。
2.采用动态规划法,在代码实现时,通常要用到数组去保存子问题的解,通常要用到循环去推进子问题一步步扩展到原问题。

动态规划法的个人体会和思考:

1.采用动态规划解题时,非常关键的一步是要确定好 状态 以及 状态转移方程。
2.动态规划法本质上是避免子问题重复计算,从而降低时间复杂度。为了达到避免子问题重复计算,动态规划法通常先将原问题划分为独立的不同子问题(不同阶段),再通过某种策略逐步将子问题拓展到原问题,从而求解原问题。

数塔(数字三角形)

·从下往上递推,最终得到从顶部开始向下走到底部的路径的最大值。
特点:从一个点出发,线性逐层递推。

状态定义:
f[i][j]表示从最后一行开始,走到位置 (i,j) 的最优解。并且要逐层走下来,不能跳着走。

容易想到 状态转移方程为:
f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];

参考代码:

#include <bits/stdc++.h>
using namespace std;

int r,a[1100][1100],f[1100][1100];

int main()
{
	cin >> r;
	for(int i=1;i<=r;++i)
	{
		for(int j=1;j<=i;++j) 
			cin >> a[i][j];
	}
	for(int i=r;i>=1;--i)
	{
		for(int j=1;j<=i;++j)
			f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];
	}
	cout << f[1][1] << endl;
}

**时间复杂度:**O(N^2)
**空间复杂度:**O(N^2)
注意点:
从第r行开始遍历,f不需要初始化;从r-1行开始遍历,需要先把f初始化一下,即将输入的最后一行的值赋值给f。

数塔问题-同类题型:
HDU-1176 免费馅饼

状态定义:
dp[i][j]:第i秒走到第j位置拿到的馅饼数
状态转移方程:
dp[i][j]=max({dp[i+1][j-1],dp[i+1][j],dp[i+1][j+1]})+a[i][j];
因为它是从(0,5)位置(这一个点)开始走的,然后(按照数塔模型)我们反着推回去,(i,j)应该有(i+1,j-1/j/j+1)推得。

参考代码:

#include <bits/stdc++.h>
using namespace std;

const int N=1e5+10;
int n,x,t,a[N][13],dp[N][13];

int main()
{
	while(cin >> n)
	{
		if(n==0) break;
		int tm=0;
		memset(dp,0,sizeof dp);
		memset(a,0,sizeof a);
		for(int i=1;i<=n;++i){
			cin >> x >> t;
			a[t][x+1]++;
			tm=max(t,tm); //找到最大值
		}
		for(int i=tm;i>=0;--i)
		{
			for(int j=1;j<=11;++j)
			{
				dp[i][j]=max({dp[i+1][j-1],dp[i+1][j],dp[i+1][j+1]})+a[i][j];
			}
		}
		cout << dp[0][6] << endl;	
	}
}

学习一些好的代码写法:
这里在保存时,是写成a[t][x+1]++;而不是a[t][x]++;,并且数组也开得稍微大了些,意在于使得代码写起来更加简洁。在后面状态转移时会涉及 dp[i+1][j-1],如果前面从0开始保存,那么后面需要多写一些条件判断,从1开始保存,使得后面写起来更简洁。

我的另一种一开始的写法:(感觉好像有些奇怪,但是对了)

#include <bits/stdc++.h>
using namespace std;

const int N=1e5+10;
int n,x,T,a[N][13],dp[N][13];

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	while(cin >> n)
	{
		if(n==0) break;
		memset(a,0,sizeof a);
		memset(dp,0,sizeof dp);
		int tm=1;
		for(int i=1;i<=n;++i)
		{
			cin >> x >> T;
			tm=max(tm,T);
			a[T][x+1]++;
		}
		int mx=0;
		for(int i=1;i<=tm;i++)
		{
			for(int j=1;j<=11;++j)
			{
				if(abs(j-6)<=i)
				{
					dp[i][j]=max({dp[i-1][j-1],dp[i-1][j],dp[i-1][j+1]})+a[i][j];
					mx=max(dp[i][j],mx);
				}	
			}
		}
		cout << mx << endl;
	}
}

类似题目:
AtCoder Beginner Contest 266 D - Snuke Panic (1D)

最大字段和问题:

HDU-1024 Max Sum Plus Plus

状态定义:
dp[j]:表示分为i组时,以j结尾的最大值
pre[j]:表示前j个数分为i-1组时的最大值

状态转移方程:
dp[j]=max(dp[j-1],pre[j-1])+s[j];

参考代码:

#include <bits/stdc++.h>
using namespace std;

#define ll long long
const int N=1e6+10;
ll n,m,s[N],dp[N],pre[N];

int main()
{
	while(cin >> m >> n)
	{
		for(int i=1;i<=n;++i) cin >> s[i];
		memset(dp,0,sizeof dp);
		memset(pre,0,sizeof pre);
		ll mx=0;
		for(int i=1;i<=m;++i)
		{
			/*
			dp[j]:表示分为i组时,以j结尾的最大值
			pre[j]:表示前j个数分为i-1组时的最大值 
			*/
			mx=LLONG_MIN;
			for(int j=i;j<=n;++j)
			{
				dp[j]=max(dp[j-1],pre[j-1])+s[j];
				pre[j-1]=mx;
				mx=max(mx,dp[j]);
			}
		}
		cout << mx << endl; //这里不是写 dp[n] //是mx保留了分为i组时的最大值 
	}
}
  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值