【力扣分模块练习】深度回溯

本文深入探讨了回溯算法在解决组合、排列、子集、全排列、单词搜索、分割回文串、复原IP地址等经典问题上的应用。详细解析了每个问题的解题思路,包括如何利用回溯法进行剪枝,以及如何维护访问状态。通过实例代码展示了如何在C++中实现这些算法,帮助读者理解回溯法在信息技术领域的广泛应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文参考:carl大佬–代码随想录的题解

个人算法小抄:
1.组合问题,无序性。如{2,4}和{4,2}是同一种结果。考虑index控制。
2.排序问题,有序性。如{1,2,3}和{3,2,1}是不一样的。考虑维护used数组。
3.分割问题,虚拟出一条分割线。很多时候直接在原数据上操作及撤回。
4.子集问题,当用三部曲模板时(其实就是迭代加递归)。搜集全部经过的节点。 当使用双递归时,收集根节点即可。

77.组合 (组合问题 控制for的下界的startindex)


给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

样例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

总结:
遇到这种,不能颠倒顺序的组合问题(如只能[1,2],不能[2,1])的时候,在递归中传入一个start,作为for横向选择时的脚标,避免走回头路。

坑点:
下一层递归是从i+1开始的,不然会重复选择相同的数字本身。(如[2,2])。 如果记不住,想想[1]的时候咋选[3],我这时候i指向2,下一层肯定是i+1才能是3呀。如果是start加一岂不是又选到2了。

剪枝:
存在一种情况如n = 4,k = 4的时候,如果第一层不选1,从2开始选,那么这条树枝全部都是没有意义的,因为后面的元素全选了都不够满足条件。

因此,可以约束i的上界条件:
1.当前已选择数量为 path.size();
2.需求元素的数量自然就是 k - path.size();
3.所以在Nums数组中,只有在某个脚标之前的选择才是有意义的-- nums.size() - (k - path.size()) + 1。至于为啥要加一,可以把path.size() = 0带进入一个具体的例子如n = 4 ,k = 3中。

class Solution {
public:
	vector<vector<int>> ans;
	vector<int> path;
	vector<vector<int>> combine(int n, int k) {
		vector<int> nums(n);
		for (int i = 0; i < n; i++)
			nums[i] = i + 1;

		backtrack(nums, k, 0);
		return ans;
	}

	void backtrack(vector<int>& nums,int k,int start)
	{
		if (path.size() == k)
		{
			ans.push_back(path);
			return;
		}

		//已选path.size(),需求 k - path.size(),只有n - (需求)+1位置之前的有意义 (之后的元素全选都不够了,直接剪枝)
		for (int i = start; i < nums.size() - (k - path.size()) + 1; ++i)  //剪枝
		{
			int a = nums[i];
			path.push_back(nums[i]);
			backtrack(nums, k, i + 1);//注意下一层要从i+1Kaishi 
			path.pop_back();
		}
	}
};

46. 全排列 (排列问题 维护一个used数组)


给定一个 没有重复 数字的序列,返回其所有可能的全排列。
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

这题可以用交换来改变排列的。撤销操作就是再交换一遍。 用一个level(level)参数来影响循环中的i的值,实现不同的交换对象。但排列问题有更通用的模板

class Solution {
public:
	vector<vector<int>> ans;
	vector<vector<int>> permute(vector<int>& nums) {
		if (nums.empty())
			return ans;
		dfs(nums, 0);
		return ans;
	}

	void dfs(vector<int>& nums, int level)
	{
		if (level == nums.size())
		{
			ans.push_back(nums);
			return;
		}
		for (int i = level; i < nums.size(); ++i)
		{
			swap(nums[i], nums[level]);
			dfs(nums, level+1);
			swap(nums[i], nums[level]);
		}
	}
};

通用套路!排列问题


用一个bool的used数组来记录已访问元素。(这样就可以知道还剩哪些元素,用for遍历的时候跳过那些已经访问过的)。
注意 回溯撤销操作的时候,要把访问数组的操作也撤销

class Solution {
public:
	vector<vector<int>> ans;
	vector<vector<int>> permute(vector<int>& nums) {
		if (nums.empty())
			return ans;
		vector<bool> used(nums.size());
		dfs(nums, used);
		return ans;
	}
	vector<int> path;
	
	void dfs(vector<int>& nums, vector<bool>& used)
	{
		if (path.size() == nums.size())
		{
			ans.push_back(path);
			return;
		}

		for (int i = 0; i < nums.size(); i++)
		{
			if (used[i] == true) continue;  //跳过那些已经访问过的
			used[i] = true;  //标记当前访问
			path.push_back(nums[i]);
			dfs(nums, used);
			used[i] = false;  //撤销访问操作
			path.pop_back();
		}
	}
};

下面是一个基于46题排列问题的剪枝问题,这个剪枝方法也很常用。

47. 全排列 II (排列剪枝 used数组+剪枝)


给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

样例:
输入:
nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]

这题和46的区别就是给定数组中有重复数字需要剪枝去重。
注意首先要对给定数组排序,这样才可以让相同的数挨到一起。

去重核心代码:

当前这个数和前一个数重复了,并且站在这一层观察以往的used情况,发现前一个重复的数竟然没有被用到。说明了啥,说明了这个重复的数字在本层中被用到了,换句话说就是这个重复的数字和“我”占据了同一个位置。那么“我”这条树枝,就要全部被剪枝。

if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { 
    continue;
}

在这里插入图片描述
以上就是树层剪枝。

此外还有一种树枝剪枝。
代码和树层剪枝只有一个true的差别。
在这个问题上,即发现和前一个数字重复,并且当前选择的位置和前一个位置相邻,则剪枝,具体见图。
其实我也不是特别理解,只能理解这个特例。 貌似是最后被剪得只剩下从后往前排列一种情况,就去掉了其他重复。

if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { 
    continue;
}

在这里插入图片描述

从上图可以发现,树层剪枝不仅好用,而且好理解。
全题代码如下:

class Solution {
public:
	vector<vector<int>> ans;
	vector<int> path;
	vector<vector<int>> permuteUnique(vector<int>& nums) {
		if (nums.empty())
			return ans;
		vector<int> used(nums.size());
		sort(nums.begin(), nums.end());
		backtrack(nums, used);
		return ans;
	}

	void backtrack(vector<int>& nums,vector<int>& used)
	{
		if (path.size() == nums.size())
		{
			ans.push_back(path);
			return;
		}

		for (int i = 0; i < nums.size(); i++)
		{
			if (i > 0 && nums[i] == nums[i-1] && used[i-1] == false) //相同nums值,但之前那个重复的没有在上层用过
				continue;					//说明已经在当前层出现了重复的数字排列

            if (used[i] == true) //避免重复选择自己
				continue;
			path.push_back(nums[i]);
			used[i] = true;

			backtrack(nums, used);

			path.pop_back();
			used[i] = false;

		}
	}
};

79. 单词搜索 (回溯维护访问矩阵)


给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

样例:
board =
[
[‘A’,‘B’,‘C’,‘E’],
[‘S’,‘F’,‘C’,‘S’],
[‘A’,‘D’,‘E’,‘E’]
]
给定 word = “ABCCED”, 返回 true
给定 word = “SEE”, 返回 true
给定 word = “ABCB”, 返回 false

坑点:
我做这题主要是一开始加入的条件不明确。实际上判断第一个字符是否是word的第一个字符,可以也通过dfs来判断。这样写法会比较统一。这题主要的难点是跳出条件比较多。而且巧妙的地方是,这道题不需要维护具体的string,而是只需要维护访问矩阵viewed就行,这个矩阵要回溯维护,因为只是一次搜索时不能回头,下一次搜索还是可以用这个数。

自己虽然强行写出来了条件判断嵌入在递归结构中的写法,但是可复制性很差,还是要学习按结构来写的。

class Solution {
public:
	string str;
	int diraction[5] = { -1,0,1,0,-1 };
	bool can_do = false;
	bool exist(vector<vector<char>>& board, string word) {
		int m = board.size(), n = board[0].size();
         vector<vector<bool>> viewed(m,vector<bool>(n,false)); 
		for (int i = 0; i < m; ++i)
		{
			for (int j = 0; j < n; ++j)
			{
				if (board[i][j] == word[0]) //我的写法,提前判断了是否可以开始递归
				{ 
					dfs(board, word,viewed, i, j, 0);
				}
			}
		}
		
		return can_do;
	}

	void dfs(vector<vector<char>> &board,string word, vector<vector<bool>>& viewed,
		int r,int c,int cnt)
	{
		str.push_back(board[r][c]);  //维护了具体数组
		viewed[r][c] = true;
		
		for (int i = 0; i < 4; i++) //往四个方向移动
		{
			int x = r + diraction[i];
			int y = c + diraction[i + 1];

			if (x >= 0 && x < board.size() && y >= 0 && y < board[0].size()
				&&viewed[x][y] == false &&board[x][y] == word[cnt + 1])
			{
				dfs(board, word,viewed, x, y, cnt+1);			
			}
			if (str == word) //终结条件在循环内部
			{
				can_do = true;
				return;
			}
		}
        viewed[r][c] = false; //撤回访问记录
		str.pop_back(); //撤回
	}
};

标答写法:
1.数组越界的跳出条件,返回
2.走了回头路,已经找到了(剪枝),值不等于该等于的,返回。
3.该等于的数已经推进到目标word的最后一位了,说明已经找到了,返回

4.我要做的事情: 记录下我现在正在访问viewed【r】【c】
5.强行循环递归,不管符不符合都进入下一层,条件判断交给一开始那三个if。

6.循环递归结束后,说明当前这个点的四个邻居都搞完了,回溯,把当前这个点的访问记录删除。以便到时候绕路回来还要用到它。

class Solution {
public:
	int diraction[5] = { -1,0,1,0,-1 };
	bool can_do = false;
	bool exist(vector<vector<char>>& board, string word) {
		int m = board.size(), n = board[0].size();
		vector<vector<bool>> viewed(m, vector<bool>(n, false)); //每次开始递归,从新计数
		for (int i = 0; i < m; ++i)
		{
			for (int j = 0; j < n; ++j)
			{	
				dfs(board, word, viewed, i, j, 0,can_do);		
			}
		}

		return can_do;
	}

	void dfs(vector<vector<char>> &board, string word, vector<vector<bool>>& viewed,
		int r, int c, int cnt,bool& can_do)
	{
		if (r < 0 || r >= board.size() || c < 0 || c >= board[0].size())
			return;

		if (viewed[r][c] == true || can_do == true || board[r][c] != word[cnt])
			return;

		if (cnt == word.length() -1)
		{
			can_do = true;
			return;
		}
		
		viewed[r][c] = true; //“我”这一层干的事情,留下访问记录
		
		for (int i = 0; i < 4; i++) //往四个方向移动
		{
			int x = r + diraction[i];
			int y = c + diraction[i + 1];
	
			dfs(board, word, viewed, x, y, cnt + 1,can_do);				
		}

		viewed[r][c] = false; //撤回访问记录
	}
};

131. 分割回文串 (分割问题 模拟切割线->转化为类组合问题)


题意:
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例:
输入: “aab”
输出:
[
[“aa”,“b”],
[“a”,“a”,“b”]
]

用回溯做,算暴力做法,主要难点是要模拟出分割线,以及回溯的跳出条件比较难确定。
在这里插入图片描述

class Solution {
public:
	vector<vector<string>> ans;
	vector<string> path;
	vector<vector<string>> partition(string s) {
		backtrack(s, 0);
		return ans;
	}
	bool IsReverse(string str)
	{
		int i = 0, j = str.length() - 1;

		while (i < j)
		{
			if (str[i] != str[j])
				return false;
			++i;
			--j;
		}
		return true;
	}
	void backtrack(string s,int index)
	{
		if (index == s.length())
		{
			ans.push_back(path);
			return;
		}
		for (int i = index; i < s.length(); i++)
		{
			string tmp = s.substr(index,i - index +1);
			if (IsReverse(tmp))
				path.push_back(tmp);
			else
				continue; //若某部分不符合,进都不会进下一层
			backtrack(s, i + 1);
			path.pop_back();
		}
	}
};

93. 复原IP地址 (分割问题进阶 加入符号


题意:
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效的 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效的 IP 地址。

样例:
输入:s = “101023”
输出:[“1.0.10.23”,“1.0.102.3”,“10.1.0.23”,“10.10.2.3”,“101.0.2.3”]

本题是131分割问题的进阶。难点在于直接在原string上操作并撤销操作。个人觉得这题完全值一个Hard。

要点:
1.用Index控制".“的加入位置。这正说明了此类问题的灵活性。
2.跳出条件要设置为已经加入了3个点”.",因此需要把已经加入的点的数量pointnum传入数组。
3.判断合法性是直接在操作后的原string上,传入头尾指针直接判断区间。

class Solution {
public:
	vector<string> ans;
	vector<string> restoreIpAddresses(string s) {
		backtrack(s, 0, 0);
		return ans;
	}

	void backtrack(string& s,int index,int pointnum)
	{
		if (pointnum == 3)
		{
			if (IsOk(s, index, s.length() - 1))  //单独判断最后一个区间是否合法
				ans.push_back(s);
			return;
		}
		for (int i = index; i < s.length(); ++i)
		{
			if (IsOk(s, index, i))  //利用合法性剪枝
			{

				s.insert(s.begin() + i + 1, '.');
				pointnum++;
				backtrack(s, i+2, pointnum); //加2是因为插入了一个逗号
				s.erase(s.begin() + i + 1);
				pointnum--;
			}
			else
				break; //不合法直接总结横向遍历 退出本层
		}
	}

	bool IsOk(string& s ,int l,int r)
	{
		if (l > r ||  l < r-9) //超出int范围
			return false;
		if (s[l] == '0' && l != r)
			return false;  //先导0

		int num = 0;
		for (int i = l; i <= r; ++i)
		{
			if (s[i] > '9' || s[i] < '0')
				return false;

			num = num * 10 + (s[i] - '0');
		}
		if (num > 255)
			return false;

		return true;
	}
};

78. 子集 类比组合问题

三部曲模板写法,注意收集全部经过的节点
在这里插入图片描述

class Solution {
public:
	vector<vector<int>> ans;
	vector<int> path;
	vector<vector<int>> subsets(vector<int>& nums) {
		backtrack(nums,0);
		return ans;
	}
	void backtrack(vector<int>& nums,int index)
	{
		ans.push_back(path);
		if (index == nums.size())
		{
			return;
		}
		for (int i = index; i < nums.size(); i++)
		{
			path.push_back(nums[i]);
			backtrack(nums, i + 1);
			path.pop_back();
		}
	}
};

双递归写法,横向只有选和不选两种情况。到叶子节点接收全部结果。
在这里插入图片描述

class Solution {
public:
	vector<vector<int>> ans;
	vector<int> path;
	vector<vector<int>> subsets(vector<int>& nums) {
		backtrack(nums,0);
		return ans;
	}
	void backtrack(vector<int>& nums,int index)
	{
		if (index == nums.size())
		{
			ans.push_back(path);
			return;
		}
		path.push_back(nums[index]); //选中index的数
		backtrack(nums, index + 1);
		path.pop_back();
		backtrack(nums, index + 1);
	}
};

491. 递增子序列(子集问题 不排序控制同层重复)


给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。

示例:
输入: [4, 6, 7, 7]
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]

重点:
1.不能用排序后used[i] == false来去重,因为只要排序了,就是改变了原始的递增性。(这个例子给了个递增的就是坑人用的)
2.所以用一个set来记录每个for(每一层)的元素使用情况。
3.剪枝条件:不递增(nums[i] < path.back()) 或 元素已出现在同层中。

4.set在退出一层时不用pop。因为等于说是每个set管理一层for,你退出一层,外面还是有一个之前的set来维护 同层使用情况。

class Solution {
public:
	vector<vector<int>> ans;
	vector<int> path;
	vector<vector<int>> findSubsequences(vector<int>& nums) {
		if (nums.empty())
			return ans;
		
		backtrack(nums, 0);
		return ans;
	}
	void backtrack(vector<int>& nums,int index)
	{
		if(path.size() >= 2)
			ans.push_back(path);
		if (index == nums.size())
		{
			return;
		}
		unordered_set<int> uset;  //记录本层元素使用情况
		for (int i = index; i < nums.size(); i++)
		{
			//剪枝:如果不是递增序列, 或者  已经在同层中(即path中的同位置)重复使用过这个元素
			if (!path.empty() && nums[i] < path.back() || uset.find(nums[i]) != uset.end() )
				continue;

			uset.insert(nums[i]); 
			path.push_back(nums[i]);
			backtrack(nums, i + 1);
			path.pop_back();
			//uset不用pop 因为推出之后回到上一层的元素使用情况
		}
	}
};

332. 重新安排行程(图论 map嵌套表示邻接表)


题意:
给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。

提示:
如果存在多种有效的行程,请你按字符自然排序返回最小的行程组合。例如,行程 [“JFK”, “LGA”] 与 [“JFK”, “LGB”] 相比就更小,排序更靠前
所有的机场都用三个大写字母表示(机场代码)。
假定所有机票至少存在一种合理的行程。
所有的机票必须都用一次 且 只能用一次。

样例:
输入:[[“JFK”,“SFO”],[“JFK”,“ATL”],[“SFO”,“ATL”],[“ATL”,“JFK”],[“ATL”,“SFO”]]
输出:[“JFK”,“ATL”,“JFK”,“SFO”,“ATL”,“SFO”]
解释:另一种有效的行程是 [“JFK”,“SFO”,“ATL”,“JFK”,“ATL”,“SFO”]。但是它自然排序更大更靠后

难点:
一是如何拆环,想到了用邻接表,但是题解的map嵌套非常妙。实际上用一个Index来控制邻接表的指针也可以做。
二是如何返回,这里返回了一个Bool值。这个方法是通用的,当dfs只需要返回一个正确结果时,就需要返回一个返回值;否则如果需要遍历所有节点,就返回void。

class Solution {
public:
	//unordered_map<出发机场,map<到达机场,航班次数>> targets  航班次数用来看有没有飞过
	unordered_map<string, map<string, int>> targets;
	vector<string> ans;
	vector<string> findItinerary(vector<vector<string>>& tickets) {
		targets.clear();
		
		for (vector<string>& vec : tickets)
			targets[vec[0]][vec[1]]++;   //记录映射关系
		ans.push_back("JFK");
		backtrack(tickets);
		return ans;
	}
	
	bool backtrack(vector<vector<string>>& tickets)
	{
		if (ans.size() == tickets.size() + 1)
			return true;

		//map中自动排序了字典序
		for (pair<const string, int>& target : targets[ans[ans.size() - 1]]) //遍历当前这个站的下一站
		{
			if (target.second > 0) //若到达站暂时还没有飞过
			{
				ans.push_back(target.first);
				target.second--;
				if (backtrack(tickets))  //如果下一层找到正确路径,提前结束向上返回
					return true;
				ans.pop_back();
				target.second++;
			}
		}
		return false;
	}
};

51. N 皇后(棋盘问题 二维矩阵按行遍历)


n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

输入:4
输出:[
[".Q…", // 解法 1
“…Q”,
“Q…”,
“…Q.”],

["…Q.", // 解法 2
“Q…”,
“…Q”,
“.Q…”]
]
解释: 4 皇后问题存在两个不同的解法。

难点:
把具体棋盘拜访问题抽象化,以及合法性拜访函数的写法。
在这里插入图片描述

class Solution {
public:
	vector<vector<string>> ans;
	vector<vector<string>> solveNQueens(int n) {
		vector<string> chessboard(n);
		string str;
		for (int i = 0; i < n; i++)
			str += '.';
		for (int i = 0; i < n; i++)
			chessboard[i] = str;
		backtrack(n, 0, chessboard);

		return ans;
	}
private:
	void backtrack(int n,int row,vector<string>& chessboard)
	{
		if (row == n) //检索完了,收集叶子节点结果
		{
			ans.push_back(chessboard);
			return;
		}

		for (int i = 0; i < n; ++i)
		{
			if (IsOk(i, row, chessboard, n)) //如果合法就可以放置
			{
				chessboard[row][i] = 'Q';
				backtrack(n, row + 1, chessboard);
				chessboard[row][i] = '.'; //撤销王后
			}
		}
	}

	bool IsOk(int col, int row, vector<string>& chessboard, int n)
	{
		//检查列
		for (int i = 0; i < row; i++)
			if (chessboard[i][col] == 'Q')
				return false;

		//检查45度对角线(往斜上检查即可,斜下还没处理不用管)
		for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j)
			if (chessboard[i][j] == 'Q')
				return false;
		//135度对角线
		for (int i = row - 1, j = col+1; i >= 0 && j < n; --i, ++j)
			if (chessboard[i][j] == 'Q')
				return false;
		
		return true;
	}
};

37. 解数独(棋盘问题 行列遍历回溯)


和上面的题目类似,由于只要找到一个符合条件的解就立刻返回,所以backtrack函数返回一个Bool值。

唯一难点就在需要二维遍历,再在内部回溯。详见代码注释:

class Solution {
public:
	void solveSudoku(vector<vector<char>>& board) {
		backtrack(board);
	}
private:
	bool backtrack(vector<vector<char>>& board)
	{
		//递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止
		//(填满当然好了,说明找到结果了),所以不需要终止条件!

		for (int i = 0; i < board.size(); ++i) //遍历行
		{
			for (int j = 0; j < board.size(); ++j) //遍历列
			{
				if (board[i][j] != '.') continue;

				for (char k = '1'; k <= '9'; k++)
				{
					if (IsOk(i, j, k,board)) //填入k是否合法
					{
						board[i][j] = k;
						if (backtrack(board)) //如果找到一组合适的立刻返回
							return true;
						board[i][j] = '.';//回溯撤销
					}	
				}
				return false; // 9个数都试完了,都不行,那么就返回false
			}
		}
		return true; //遍历完整个棋盘都没有返回false,说明找到一组合理的
	}
	bool IsOk(int row, int col, char k, vector<vector<char>>& board)
	{
		for (int j = 0; j < board.size(); ++j)  //同行有重复
			if (board[row][j] == k)
				return false;
		
		for (int i = 0; i < board.size(); ++i) //同列重复
			if (board[i][col] == k)
				return false;

		int startRow = (row / 3) * 3; //九宫格重复
		int startCol = (col / 3) * 3;
		for (int i = startRow; i < startRow + 3; ++i)
		{
			for (int j = startCol; j < startCol + 3; ++j)
			{
				if (board[i][j] == k)
					return false;
			}
		}
		return true;
	}
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值