常见的算法策略-(递归&贪心&分治&动态规划)

4.常见算法策略

4.1 递归

编写递归程序三部曲:

  1. 定义函数功能;

  1. 找出递归的终止条件;

  2. 递推函数的等价关系式。(汉诺塔, 遍历)

举例:求连续n个数的和

第一步:定义函数功能

//求连续n个数的和
int sum(int n)
{
    
}

第二步:找出递归的终止条件

//求连续n个数的和
int sum(int n)
{
    if (n == 0)
    {
        return 0; 
    }
}

第三步:找出递推函数的等价关系式

//求连续n个数的和
int sum(int n)
{
    if (n == 0)
    {
        return 0; 
    }
    return n + sum(n - 1);
}

4.1.1 求第n个斐波那契数

斐波那契数列(Fibonacci Sequence),又称黄金分割数列,因为数学家莱昂纳多斐波那契以兔子繁殖问题为列引入,故称为“兔子数列”,指的是这么一个数列:1 1 2 3 5 8 13 21 34 55 ......在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)

斐波那契数列指的是这样一个数列:

这个数列从第3项开始,每一项都等于前两项之和。

#include <iostream>
using namespace std;

//求第3个斐波那契数列
int fibonacci(int n)
{
	if (n == 0)
	{
		return 0;
	}
	if (n == 1)
	{
		return 1;
	}
	return fibonacci(n - 1) + fibonacci(n - 2);

}

4.1.2 小青蛙跳台阶

给定一个n,代表一共有n级台阶,一只青蛙一开始在第0级,每次可以向上跳一级台阶,也可以向上跳两级台阶,求该青蛙跳到第n级台阶一共有多少多少种跳发?

思路分析:如果台阶只有一级台阶的话,那么青蛙只有一种跳法;如果有两级台阶的话,有两种跳法,一种是一次跳一级,另一种是一次跳两级;如果有三级台阶的话,青蛙第一次可以跳一级,剩下的两级可以按照上面提到的跳两级台阶的跳法,若青蛙在跳三级的时候,第一次二级的话,剩下的一级只能按一级台阶的跳法来跳。以此类推,n级台阶与三级台阶的跳法思路一样。

//小青蛙跳台阶问题,求跳到第n个台阶有多少种跳法
int frog_jump(int n)
{
	if (n == 1)
	{
		return 1;
	}
	if (n == 2)
	{
		return 2;
	}
	return frog_jump(n - 1) + frog_jump(n - 2);

}

4.2 贪心策略

贪心策略的思想就是通过局部最优来达到全局最优。

要想选出全局最优解是非常苦难的事情,要证明每一步最优达到全局最优,需要严格的数学证明,否则不能说明为全局最优。

很多问题表面上看起来用贪心策略可以找到最优解,实际上却把最优解给漏掉了。

例子1:钱币支付问题

假设1元、2元、5元、10元、20元、50元、100元的纸币分别有c0、c1、c2、c3、c4、c5、c6张。现在要用这些纸币来支付k元,最少用多少张纸币?

很明确,用贪心算法,每一步尽可能用面值最大的纸币支付。

#include <iostream>
using namespace std;

const int N = 7;
int Value[N] = { 1,2,5,10,20,50,100 };
int Count[N] = { 3,0,2,1,0,3,5 };

//int solve(int money)
//{
//	if (money == 0)
//	{
//		return 0;
//	}
//	for (int i = N - 1; i >= 0; i--)
//	{
//		if (money >= Value[i] && Count[i] > 0)
//		{
//			Count[i]--;
//			int ans = solve(money - Value[i]);
//			if (ans != -1)
//			{
//				return ans + 1;
//			}
//			Count[i]++;
//		}
//	}
//	return -1;
//
//}

int solve(int money)
{
	int num = 0;
	for (int i = N - 1; i >= 0; i--)
	{
		int c = min(money / Value[i], Count[i]);
		money -= c * Value[i];
		num += c;
	}
	if (money > 0)
	{
		return -1;
	}
	return num;
}


int main()
{
	int money;
	cout << "请输入花费总额: ";
	cin >> money;
	int ans = solve(money);
	if (ans != -1)
	{
		cout << "最少需要支付" << ans << "张纸币" << endl;
	}
	else
	{
		cout << "没有找到支付方法" << endl;
	}
	return 0;
}

例子2:西红柿首付的烦恼

王多鱼获得一笔奖金,要求购买最少得商品把钱花光,则没有零钱剩下,否则奖金被没收。

输入:

一个整数k:商品的种类(每个种类商品个数不限);

第i类商品的价值a[i];

一个整数m:奖金总额

输出:

最少得商品数量

举例:

输入: 7

商品价值: 1 2 5 10 20 50 100

奖金总额: 288

输出 : 8

#include <iostream>
using namespace std;

int main()
{
	int k, m, n, cnt = 0, a[100];
	cout << "商品的种类数量: " << endl;
	cin >> k;
	cout << "从大到小输出商品的价值 " << endl;
	for (int i = 1; i <= k; i++)
	{
		cin >> a[i];
	}
	cout << "输入奖金的数量" << endl;
	cin >> m;
	for (int i = k; i >= 1; i--)
	{
		if (m >= a[i])
		{
			n = m / a[i];
			m = m - n * a[i];
			cnt += n;
			cout<<a[i]<<"元的商品"<<n<<"个"<<"剩余奖金"<<m<<endl;
			if (m == 0)
			{
				break;
			}
		}
	}
	cout <<"最少得商品数量为:"<< cnt << endl;

	return 0;
}

4.3 分治策略

分治(Devide and Conquer),分而治之,就是把一个复杂的问题分成两个或者多个相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的合并(维基百科)。

分治策略的步骤

分解:将原问题分解为若干个规模较小、相互独立且与原问题形式相同的子问题。

解决:递归地求解各个子问题。如果子问题的规模足够小,则直接求解。

合并:将子问题的解合并为原问题的解。

4.3.1 归并排序、快速排序

1.归并排序:

分解:将待排序的数组分成两个子数组,每个子数组的规模为原数组的一半。

解决:对两个子数组分别进行归并排序,这是通过递归调用归并排序算法实现的。

合并:将两个已排序的子数组合并成一个有序的数组。

2.快速排序:

分解:选择一个基准元素,将数组分成两个子数组,一个子数组中的元素都小于等于基准元素,另一个子数组中的元素都大于等于基准元素。

解决:对两个子数组分别进行快速排序。

合并:由于子数组都是在原数组的基础上划分的,所以不需要显式地合并操作。

4.3.2 二分查找

二分查找针对的是一个有序的数据集合,每次查找时通过将待查找的元素与中间位置的元素进行比较,来确定待查找元素在集合中的位置范围,然后在缩小后的范围内继续进行查找,直到找到目标元素或者确定目标元素不存在。

具体步骤

1. 首先确定查找区间的上下界,初始时,下界为数组的第一个元素的下标,上界为数组的最后一个元素的下标。

2. 计算中间位置的下标,公式为 mid = (low + high) / 2(这里的除法为整数除法)。

3. 将待查找元素与中间位置的元素进行比较:

        如果相等,则查找成功,返回中间位置的下标。

        如果待查找元素小于中间位置的元素,则说明目标元素在中间位置的左侧,将上界更新为 mid - 1,继续在左侧区间进行查找。

        如果待查找元素大于中间位置的元素,则说明目标元素在中间位置的右侧,将下界更新为 mid + 1,继续在右侧区间进行查找。

4. 重复步骤 2 和 3,直到找到目标元素或者下界大于上界(说明目标元素不存在)。

4.3.3 两个大数相乘

算法步骤:

接收两个以字符串形式表示的大整数 A和B 。

将大整数A分为两个部分A1和A2:使的其中 n 为大整数的长度;

同样,将B分为 B1和B2:即

计算 C1 = A1 * B1; C2 = A2 * B2; C3 = (A1+A2)*(B1+B2)

#include <iostream>
using namespace std;


//返回数字长度
int getNumLength(int num)
{
	int length = 0;
	while (num > 0)
	{
		num /= 10;
		length++;
	}
	return length;
}

//用分治法求两个大数相乘
long long multiply(int num1, int num2)
{
	int a_len = getNumLength(num1);
	int b_len = getNumLength(num2);

	int n = a_len > b_len ? a_len / 2 : b_len / 2;;
	//A和B只要有一个数字是个位数,直接计算返回
	if (num1 <= 9 || num2 <= 9)
	{
		return num1 * num2;
	}

	int a1 = num1 / pow(10, n);
	int a2 = num1 % (int)pow(10, n);
	int b1 = num2 / pow(10, n);
	int b2 = num2 % (int)pow(10, n);

	int n0 = multiply(a1, b1);
	int n1 = multiply(a2, b2);
	int n2 = multiply(a1 + a2, b1 + b2) - n0 - n1;

	return n0 * pow(10, 2 * n) + n2 * pow(10, n) + n1;
}
void test_multiply()
{
	int big1 = 123456;
	int big2 = 654321;

	long long result = multiply(big1, big2);
	cout << "result:" << result << endl;
}

4.3.4 字符串的全排列

输入一个字符串abc,打印出其全排列的结果:abc,acb;bac,bca;cab,cba.

以字符串 “abc” 为例:

  1. 选择字符 “a” 作为固定字符,子字符串为 “bc”。
  2. 求子字符串 “bc” 的全排列,得到 “bc” 和 “cb”。
  3. 将 “a” 依次插入到 “bc” 和 “cb” 的不同位置:
    • 插入到 “bc” 中,得到 “abc”、“bac”、“bca”。
    • 插入到 “cb” 中,得到 “acb”、“cab”、“cba”
//用分治策略,求字符串的全排列
void fullpai(string str, int start, int end)
{
	int len = str.length();
	if (start == end)
	{
		cout << str << endl;
		return;
	}
	for (int i = start; i <= end; i++)
	{
		if (i != start)
		{
			swap(str[i], str[start]);//交换i指向的元素和最左边的元素
		}
		fullpai(str, start + 1, end);
	}
}

void test_fullpai()
{
	string str1 = "abc";
	fullpai(str1, 0, str1.length() - 1);
}

4.3.5 找出假的硬币

一堆硬币中有且仅有一枚假币,假币和外观和真币一模一样,但是重量比真币轻。用分治的思想,找出这枚假币。

分治思路:

  1. 首先将硬币分成相等的两部分(如果硬币总数是奇数,就把多出来的那一枚先放在一边)。
  2. 分别称这两部分硬币的重量。
  3. 如果两部分重量相等,那么假币就在之前单独放在一边的那一枚(如果有的话)或者在还没有称重的硬币中,然后对这部分硬币继续使用分治方法进行查找。
  4. 如果两部分重量不相等,那么假币就在较轻的那一部分中,对较轻的这部分硬币继续使用分治方法进行查找。
//用分治思想找出假币
int findFakeCoin(int a[], int start, int end)
{
	int sum_left = 0, sum_right = 0;
	int mid = (start + end) / 2;

	if((end - start +1) % 2 == 0) //硬币为偶数
	{
		for (int i = start; i <= mid; i++)
		{
			sum_left += a[i];//累加左边的硬币
			sum_right += a[mid + 1 + i - start];//累加右边的硬币
		}
		if (sum_left < sum_right)//假币在左半部分
		{
			findFakeCoin(a, start, mid);
		}
		else
		{
			findFakeCoin(a, mid + 1, end);
		}
	}
	else //硬币为奇数
	{
		for (int i = start; i < mid; i++)
		{
			sum_left += a[i];
            sum_right += a[mid + 1 + i - start];
		}
		if (sum_left == sum_right)
		{
			return mid;
		}
		else if (sum_left < sum_right)
		{
			findFakeCoin(a, start, mid - 1);
		}
		else
		{
			findFakeCoin(a, mid + 1, end);
		}
	}
}

void test_findFakeCoin()
{
	int coins[21] = { 0 };
	for (int i = 0; i < 21; i++)
	{
		coins[i] = 7;
	}
	coins[8] = 6;
	int length = sizeof(coins) / sizeof(int);
	cout <<"假币的索引为: "<<findFakeCoin(coins, 0, length - 1) << endl;
}

4.3.6 分治和递归的区别和联系

递归和分治是两个不同维度的概念。

分治是一种算法策略,其思想就是将原问题拆分成多个无重复的子问题,当子问题解决后合并子问题的解就能得到原问题的解,分治思想可以使用递归来实现,也可以不用递归来实现。而大多数分治问题都是用递归来实现的,因为递归代码清晰、直观、容易理解。

递归从狭义角度来说,是程序的一种实现方式,即调用自身,可以用递归解决分治问题,也可以用来解决其他问题。

4.4 动态规划

动态规划(Dynamic programming, 简称DP),是一种在数学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划其实就是给定一个问题,我们把它拆分成一个个子问题,直到子问题可以直接解决,然后呢,把子问题答案保存起来,以减少重复计算,再根据子问题的答案反推,得出原问题解的一种方法。

动态规划的最核心的思想,就是在于拆分问题,记住过往,减少重复计算。

A:1+1+1+1+1+1+1+1=?

B:8

A:1+1+1+1+1+1+1+1+1=?

B:9

A:你为什么这么快就得出了答案 ?

B:只要在上次计算的基础上再加就行了,不用重复计算。

4.4.1 求第n个斐波那契数的优化

#include <iostream>
using  namespace std;


//求第3个斐波那契数列
int fibonacci(int n)
{
	if (n == 0)
	{
		return 0;
	}
	if (n == 1)
	{
		return 1;
	}
	return fibonacci(n - 1) + fibonacci(n - 2);

}
//求第n个斐波那契数列,使用动态规划
int fibonacci_dp(int n)
{
	//定义状态数组f,其中f[i]表示第I个斐波那契数
	int f[1024] = { 0, 1 };//初始化边界值
	for (int i = 2; i <= n; i++)
	{
		f[i] = f[i - 1] + f[i - 2];//状态转移方程
	}
	return f[n];//返回最终结果,时间复杂度为O(n)
}

4.4.2 小青蛙跳台阶问题

求解动态规划题目的一般步骤为:

1.设置一个状态数组,并且初始化为边界值;

2.寻找状态转移方程(解决动态规划问题的核心和难点)

3.根据实际需要返回最终结果

//小青蛙跳台阶问题,求跳到第n个台阶有多少种跳法
int frog_jump(int n)
{
	if (n == 1)
	{
		return 1;
	}
	if (n == 2)
	{
		return 2;
	}
	return frog_jump(n - 1) + frog_jump(n - 2);
}

//小青蛙跳台阶问题,求调到第n个台阶有多少种跳法,使用动态规划
int frog_jump_dp(int n)
{
	//定义状态数组f,其中f[i]表示小青蛙跳到第i个台阶的跳法总数
	int f[1024] = { 0, 1, 2 };//初始化边界值
	for (int i = 3; i <= n; i++)
	{
		f[i] = f[i - 1] + f[i - 2];//状态转移方程
	}
	return f[n];//返回最终结果,时间复杂度为O(n)
}

4.4.3 用最小花费爬楼梯

给一个整数数组cost,其中cost[i]表示从楼梯的第i个台阶向上爬需要支付的费用。一旦支付了此费用,既可以选择向上爬一个或者两个台阶。可以选择从下标为0或者下标为1的台阶开始爬楼梯。请计算爬到楼顶需要的最小花费额。

示例1:

输入:cost = {10, 15, 20};
输出:15
解释:从下表位1的台阶开始爬,支付15,向上爬两个台阶,即可到达楼梯顶部

示例2:

输入:cost = {1, 100, 1, 1, 1, 100, 1, 1, 100, 1}
输出:6
解释:从下标为0的台阶开始爬
支付1,向上爬两个台阶,到达下标为2的台阶
支付1,向上爬两个台阶,到达下标为4的台阶
支付1,向上爬两个台阶,到达下标为6的台阶
支付1,向上爬一个台阶,到达下标为7的台阶
支付1,向上爬;两个台阶,到达下标为9的台阶
支付1,向上爬一个台阶,即可到达楼梯顶部。
最小花费为6

解题思路:

假设n个阶梯分别对应数组下标0到n-1,楼梯顶部对应下标n,求到达n的最小花费。

设置状态数组dp,其中dp[i]表示到达下标i的最小花费,因为可以选择0或者1作为初始台阶,因此dp[0]=do[1]=0,。

当2<=i<=n时,可以从下标为i-1的台阶开始爬,花费cost[i-1]向上爬一级台阶到达下标i,或者从下标为i-2的台阶开始爬,花费cost[i-2]向上爬两级台阶到达下标。为了使总花费最小,dp[i]应该取上述两项的最小值,因此状态转移方程为:

dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i - 2])

具体实现代码:

//最小花费楼梯
int minCost(int cost[], int n)
{
	//设置一个状态数组dp,其中dp[i]爬到第i个台阶的最小花费
	int dp[1024] = { 0, 0, };//初始化边界值
	for (int i = 2; i <= n; i++)
	{
		dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);//状态转移方程
	}
	return dp[n];//返回最终结果,时间复杂度为O(n)
}

void test_minCost()
{
	int cost1[] = { 10, 15, 20 };
	cout <<"爬到楼梯顶部的最小花费为:"<< minCost(cost1, 3) << endl;
	int cost2[] = { 1, 100, 1, 1, 1, 100, 1, 1, 100, 1 };
	cout <<"爬到楼梯顶部的最小花费为:"<< minCost(cost2, 10) << endl;
}

4.4.4 求最大子数组和

给定整数数组nums,求其最大数组 (子数组最少包含一个元素)的和。

子数组是数组中一个连续部分。

示例:

输入:nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
输出:6
解释:子数组[4,-1,2,1]的和是最大的,为6。

使用动态规划来求解,算法思路:

设置状态数组dp,dp[i],以下标i指向的元素结尾的所有子数组的最大和。

以第i个整数组结尾的子数组分为两种情况:

和第i-1个整数结尾的子数组和相连;

和第i-1个整数结尾的子数组不相连,单独以第i个数组作为子数组;

状态转移方程:

dp[i] = max(dp[i - 1]+ nums[i],nums[i]) = max(dp[i - 1], 0 )+ nums[i]

具体实现:

//最大子数组的和,用暴力法
int max_subarry(int a[], int len)
{
	int max = a[0];
	for (int i = 0; i < len; i++)
	{
		for (int j = i; j < len; j++)
		{
			int sum = 0;
			for (int k = i; k <= j; k++)
			{
				sum += a[k];
			}
			if (sum > max)
			{
				max = sum;
			}
		}
	}
	return max;
}
//最大子数组的和,用动态规划
int max_subarry_dp(int a[], int len)
{
	//定义状态数组dp,其中dp[i]表示以第i个元素结尾的最大子数组的和
	int dp[512];//定义状态数组
	dp[0] = a[0];//初始化边界值

	int maxValue = dp[0];
	for (int i = 1; i < len; i++)
	{
		dp[i] = max(dp[i - 1],0)+ a[i];//状态转移方程
		maxValue = max(maxValue, dp[i]);//更新最大值
	}

	return maxValue;//返回最终结果,时间复杂度为O(n)
}
void test_max_subarry()
{
	int nums[] = { -2, 1, -3, 4, -1, 2, 1, -5, 4 };
	int length = sizeof(nums) / sizeof(nums[0]);
	cout << "最大子数组的和为:" << max_subarry(nums, length) << endl;
	cout << "最大子数组的和为:" << max_subarry_dp(nums, length) << endl;
}

4.4.5 最长回文字符串

给定一个字符串s,找出s中最长的回文字符串

回文字符串:如果一个字符串的逆序和原始字符串相同,则称改字符串为回文字符串。

示例:

输入:s = "bcalevelast"
输出:"alevela"
解释:alevela就是原始字符串中最长的那个回文字符串

如果字符串长度为1,那必然是回文字符串;如果长度为2或者3,只需要最左边和最右边相等,那么该字符串就是回文字符串。

设dp[i] [j] 表示是s[i]到s[j]所表示的字符串是否是回文字符串,如果是回文字符串,则dp[i] [j]=1,如果不是,则dp[i] [j] = 0。根据s[i]是否等于s[j],分为两种情况:

1.若s[i]==s[j],那么只要s[i+1]到s[j-1]是回文字符串,那么s[i]到s[j]就是回文字符串;如果s[i+1]到s[j-1]不是回文字符串,那么s[i]到s[j]就不是回文字符串;

2.若s[i] != s[j],那么s[i]到s[j]一定不是回文字符串。

状态转移方程为:

dp[i][j] = dp[i + 1][j - 1],s[i] == s[j]
			0, s[i] != s[j]

具体实现:

//动态规划,求最长回文字符串
string maxHuiwen(string s)
{
	int len = s.size();
	int start = 0;//最长回文字符串的起始位置
	int max_length_huiwen = 1;//最长回文字符串的长度

	int dp[50][50] = { 0 };//定义状态数组,其中dp[i][j]表示字符串s[i...j]是否为回文串

	for (int j = 1; j < len; j++)
	{
		for (int i = 0; i < j; i++)
		{
			if (s[i] == s[j])
			{
				if (j - i < 3)
				{
					dp[i][j] = 1;
				}
				else
				{
					dp[i][j] = dp[i + 1][j - 1];
				}
			}
			if (dp[i][j] == 1 && (j - i + 1 > max_length_huiwen))
			{
				max_length_huiwen = j - i + 1;
				start = i;
			}
		}
	}

	return s.substr(start, max_length_huiwen);
}

void test_maxHuiwen()
{
	string str1 = "bcalevelast";
	cout << "最长回文字符串为:" << maxHuiwen(str1) << endl;
}

4.4.6 背包问题

背包问题可以分为三类:01背包(每个元素最多取一次)、完全背包(每个元素可以取多次)以及分组背包(可以有多个背包)。

01背包问题:有N件物品,每件物品都有各自的体积和价值,现有一个容量为V的背包,每件物品最多只能装入一次,如何让背包里装入的物品价值最大?

第i件物品的体积为vi,价值为wi。

viwi物品\背包012345
000000000
121022222
242024666
343024668
454024668

解题思路:

定义状态数组dp,其中dp[i] [j]表示;只考虑前i个物品,背包容量为j的情况下,装入物品的最大价值。

状态转移方式:

dp[i] [j] = max(dp[i -1] [j], dp[i-1] [j-vi] + wi)

其中dp[i-1] [j] 表示,不装第i个物品的情况下,只考虑装前i-1个物品的最大价值。

dp[i-1] [j - vi] + wi 表示装入第i个物品,那么背包总容量就要减去第i个物品的体积,剩下的容量再去考虑装前i-1个物品的最大价值,再加上当前装入的第i件物品的价值wi。

具体实现:

//01背包问题
int maxValueofBag()
{
	int V = 5;//背包容量
	int N = 4;//物品个数
	int v[5] = { 0, 1, 2, 3,4 };//物品体积
	int w[5] = { 0, 2, 4, 4 , 5 };//物品价值

	//定义状态数组dp,dp[i][j]表示背包容量为j时,装入前i个物品的最大价值
	int dp[10][10] = { 0 };
	//初始化状态数组
	//商品个数为0的情况下;不同背包容量现爱装入的物品最大价值
	for (int j = 0; j <= V; j++)
	{
		dp[0][j] = 0;
	}
	//背包容量为0的情况下,分别考虑装入不同个数物品时的最大价值
	for (int i = 0; i <= N; i++)
	{
		dp[i][0] = 0;
	}

	//装物品的思路:不能装时,直接继承上一个状态;可以装时,比较装与不装的价值,取较大值
	for (int i = 1; i <= N; i++)//外层循环,遍历物品价值
	{
		for (int j = 1; j <= V; j++)//内层循环遍历背包容量
		{
			if (j < v[i])//当前背包容量小于当前物品的体积,装不进去,只能不装
			{
				dp[i][j] = dp[i - 1][j];//状态转移方程
			}
			else
			{
				//能装进去,考虑要不要装,如果不装,用上一个状态dp[i-1][j]
				// 如果装,装上第i个物品最大价值为dp[i-1][j-v[i]]+w[i]
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
			}
		}
	}
	
	//有四件物品,背包容量为5,所以dp[4][5]就是最大价值
	return dp[4][5];//返回最终结果
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值