力扣回溯算法专题(一)- 回溯算法理论基础、组合问题 77.组合、216.组合总和Ⅲ、17.电话号码的字母组合、39.组合总和、40.组合总和Ⅱ 思路及C++实现 组合问题总结

回溯算法理论基础

回溯法

定义:回溯法也可以叫做回溯搜索法,是一种搜索的方式。有递归就会有回溯,回溯函数也就是递归函数。

回溯法的效率:回溯的本质是穷举,穷举所有可能,然后选出想要的答案。剪枝操作可以让回溯法高效一些,但回溯法仍然是穷举。

回溯法解决的问题

一般可以解决如下几种问题:
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等

注意:组合是不强调元素顺序的,排列是强调元素顺序。组合无序,排列有序。

回溯法理解

  • 回溯法解决的问题都可以抽象为树形结构。
  • 因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。
  • 递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。

回溯法/递归法模板

  • 回溯函数模板返回值以及参数:
    回溯算法中函数返回值一般为void;
    回溯算法中一般是先写逻辑,然后需要什么参数,就填什么参数。
  • 回溯函数终止条件:一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
  • 回溯搜索的遍历过程:
    for循环,横向遍历,遍历集合区间。一个节点有多少个孩子,这个for循环就执行多少次;
    处理节点;
    递归;
    回溯,撤销处理结果。

回溯法的题目

  • 组合:77.组合、17.电话号码的字母组合、39.组合总和、40.组合总和Ⅱ、216.组合总和Ⅲ
  • 分割:131.分割回文串、93.复原IP地址
  • 子集:78.子集、90.子集Ⅱ
  • 排列:46.全排列、47.全排列Ⅱ
  • 棋盘问题:51.N皇后、37.解数独
  • 其他:491.递增子序列、332.重新安排行程

回溯法伪代码

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

77. 组合

三要素及思路

先写逻辑,再确定递归函数参数:
n,题目中的n。
k,题目中的k,集合个数。
startindex,下一层for循环搜索的起始位置。

终止条件:
k就是树的深度,只取k个元素,path.size() =k就终止。

单层搜索:
path收集每次选取的元素,相当于树型结构里的边。

全局变量
一维数组path来存放符合条件的结果,二维数组result来存放结果集。

代码

class Solution {
public:
    vector<vector<int>> result;//二维数组,存放符合条件结果的集合
    vector<int> path;//一维数组,存放符合条件的一个结果 也是存放路径

    //参数就是树的宽度n,深度k,startindex是为了控制单次递归时从哪里开始遍历,防止出现重复的组合
    //如,一个结果集合是[1, 2] [1, 3] [1, 4];那么下一个结果集合遍历的开始位置就是2开始,即[2, 3] [2, 4]
    void backtracking(int n, int k, int startindex)
    {
        //遍历到树的叶子节点,也就是走过了两个节点,即path的大小为k时,回溯终止
        //相当于 一维数组path大小=k,找到了一个子集大小为k的组合,需要在result中保存path,也就是保存了根节点到叶子节点的路径,然后终止回溯
        if(path.size()==k)
        {
            result.push_back(path);
            return;
        }
        //单层递归
        for(int i = startindex;i<=n;i++)//控制树的横向遍历,i为本次搜索的起始位置
        {
            path.push_back(i);//处理节点
            backtracking(n, k, i+1);//递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
            path.pop_back();//回溯,撤销处理的节点
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
};

剪枝优化

优化过程如下:
已经选择的元素个数:path.size();
还需要的元素个数为: k - path.size();
在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
+1是因为包括起始位置,要的是一个左闭的集合

把上面代码的for(int i = startindex;i<=n;i++)替换成for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)

216.组合总和Ⅲ

三要素及思路

先写逻辑,再确定递归函数参数:
目标和targetsum,题目中的n。
k,题目中的k,集合个数。
sum,每条路径累加和,path里元素总和。
startindex,下一层for循环搜索的起始位置。

终止条件:
k就是树的深度,只取k个元素,path.size() =k就终止。
结合题目,如果此时path里收集到的元素和sum和题目要求的和n,即目标和targetSum相同,就用result收集当前的结果。

单层搜索:
path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。

全局变量
一维数组path来存放符合条件的结果,二维数组result来存放结果集。

代码

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int k, int targetsum, int sum, int startindex)
    {
        //终止条件
        if(path.size()==k)
        {
            if(sum==targetsum) result.push_back(path);//如果累加和与目标和相同,存放结果
            return;//搜索到叶子节点 
        }
        //单层搜索
        //数字只能在1-9之间
        for(int i = startindex; i<=9; i++)
        {
            sum += i;//累加求和
            path.push_back(i);//存放组合
            backtracking(k, targetsum, sum, i+1);//startindex调整为i+1
            sum -= i;//回溯
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k, n, 0, 1);
        return result;
    }
};

剪枝优化

如果第二次取元素值大于n,即目标和,后续操作就没有意义了,直接返回,做剪枝处理if (sum > targetSum) return;,有三种方式:

  • 方式1:遍历前剪枝, 回溯函数开始就剪枝
  • 方式2:调用递归函数开始前剪枝,但要先回溯,再剪枝
  • 方式3:也可以在for循环的控制条件剪枝,i <= 9-(k-path.size())+1

方式1:回溯开始就剪枝

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int k, int targetsum, int sum, int startindex)
    {
        //剪枝1 回溯开始就剪枝
        if(sum>targetsum) return;

        //终止条件
        if(path.size()==k)
        {
            if(sum==targetsum) result.push_back(path);//如果累加和与目标和相同,存放结果
            return;//搜索到叶子节点 
        }
        //单层搜索
        //数字只能在1-9之间
        for(int i = startindex; i<=9; i++)
        {
            sum += i;//累加求和
            path.push_back(i);//存放组合
            backtracking(k, targetsum, sum, i+1);//startindex调整为i+1
            sum -= i;//回溯
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k, n, 0, 1);
        return result;
    }
};

方式2:调用递归函数开始前剪枝,先回溯,再剪枝

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int k, int targetsum, int sum, int startindex)
    {
        //终止条件
        if(path.size()==k)
        {
            if(sum==targetsum) result.push_back(path);//如果累加和与目标和相同,存放结果
            return;//搜索到叶子节点 
        }
        //剪枝2 调用递归前剪枝 要先回溯再剪枝
        for(int i = startindex; i<=9; i++)
        {
            sum += i;//累加求和
            path.push_back(i);//存放组合
            if(sum > targetsum)//剪枝
            {
                sum -= i;//剪枝之前先回溯
                path.pop_back();//剪枝之前先回溯
                return;
            }
            backtracking(k, targetsum, sum, i+1);//startindex调整为i+1
            sum -= i;//回溯
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k, n, 0, 1);
        return result;
    }
};

方式3:for循环控制条件剪枝

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int k, int targetsum, int sum, int startindex)
    {
        //终止条件
        if(path.size()==k)
        {
            if(sum==targetsum) result.push_back(path);//如果累加和与目标和相同,存放结果
            return;//搜索到叶子节点 
        }
        //剪枝3
        for(int i = startindex; i<=9 - (k-path.size()) + 1; i++)
        {
            sum += i;//累加求和
            path.push_back(i);//存放组合
            backtracking(k, targetsum, sum, i+1);//startindex调整为i+1
            sum -= i;//回溯
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k, n, 0, 1);
        return result;
    }
};

17.电话号码的字母组合

三个问题

  1. 数字和字母如何映射——map或二维数组映射
  2. 输入1 * #按键等等异常情况——可以剔除这三种字符
  3. 两个字母两个for循环,三个字符三个for循环,以此类推,怎么解决n个循环——回溯

三要素及思路

先写逻辑,再确定递归函数参数:
digits,题目中的digits,数字串。
index,遍历数字串digits,记录遍历的第几个数字,也表示树的深度

终止条件:
当数字串遍历完就终止,即index==digits.size()。存放当前层的结果,再结束递归

单层搜索:
首先要用index获取数字串的数字
然后找到该数字对应的字母集
最后通过for循环处理这个字母集:s存放每次在字母集选取的元素,然后递归,再回溯

全局变量
一个字符串s收集叶子节点结果
一个字符串数组result存放结果集

代码

  • 二维数组
class Solution {
//私有属性 二维数组 数字与字母映射
private:
    const string letterMap[10] = 
    {
        "", //0
        "", //1
        "abc", //2
        "def", //3
        "ghi", //4
        "jkl", //5
        "mno", //6
        "pqrs", //7
        "tuv", //8
        "wxyz", //9
    };
public:
    string s;//保存符合条件的一个结果
    vector<string> result;//保存结果集

    //参数:1.题目给的字符串digits 引用传入 const修饰;2.当前数字,字符串当前元素
    void backtracking(const string& digits, int index)
    {
        //终止条件 如果字符串遍历完就终止
        if(index==digits.size())
        {
            result.push_back(s);
            return;
        }
        //单层搜索 二维数组
        int digit = digits[index] - '0';//index指向的数字由string转成int
        string letters = letterMap[digit];//获取index指向的数字对应的字母
        for(int i=0; i<letters.size();i++)
        {
            s.push_back(letters[i]);//存放字母
            backtracking(digits, index+1);//递归,index+1表示处理下一个数字
            s.pop_back();//回溯
        }
    }
    vector<string> letterCombinations(string digits) {
        if(digits.size()==0) return result;
        backtracking(digits, 0);
        return result;
    }
};
  • map
class Solution {
//私有属性 map 数字与字母映射
private:
    const unordered_map<char, string> phoneMap{
            {'2', "abc"},
            {'3', "def"},
            {'4', "ghi"},
            {'5', "jkl"},
            {'6', "mno"},
            {'7', "pqrs"},
            {'8', "tuv"},
            {'9', "wxyz"}
    };
public:
    string s;//保存符合条件的一个结果
    vector<string> result;//保存结果集

    //参数:1.题目给的字符串digits 引用传入 const修饰;2.当前数字,字符串当前元素
    void backtracking(const string& digits, int index)
    {
        //终止条件 如果字符串遍历完就终止
        if(index==digits.size())
        {
            result.push_back(s);
            return;
        }
        //单层搜索 map
        char digit = digits[index];//获取当前遍历的数字
        string letters = phoneMap.at(digit);//获取当前遍历数字 对应的 字母集 注意访问方式
        for(int i=0; i<letters.size();i++)
        {
            s.push_back(letters[i]);//存放字母
            backtracking(digits, index+1);//递归,index+1表示处理下一个数字
            s.pop_back();//回溯
        }
    }
    
    vector<string> letterCombinations(string digits) {
        if(digits.size()==0) return result;
        backtracking(digits, 0);
        return result;
    }
};

39.组合总和

三要素及思路

先写逻辑,再确定递归函数参数:
candidates,集合
target,目标值
sum,每条路径累加和,path里元素总和。也可以用target做减法,target=0就说明找到符合的结果了。
startindex,下一层for循环搜索的起始位置。

终止条件:
sum大于target和sum等于target

单层搜索:
path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。

全局变量
一维数组path来存放符合条件的结果,二维数组result来存放结果集。

本题和77.组合、216.组合总和Ⅲ不同的是

  1. 本题元素可以重复,77和216不可以。所以回溯时,本题的startindex从i开始,77和216的startindex从i+1开始。
  2. 组合没有数量限制

代码

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startindex)
    {
        //终止条件
        if(sum > target) return;
        if(sum == target)
        {
            result.push_back(path);//存放结果集
            return;
        }
        //单层搜索
        for(int i=startindex; i<candidates.size(); i++)
        {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i);//元素可以重复选取,不需要i+1
            sum -= candidates[i];//回溯
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

剪枝优化

sum大于target时,还是进入了下一层递归,但其实可以直接返回,不需要进入下一层递归。
因此,sum大于target时再for循环所有范围进行剪枝。对总集合排序,如果下一层的sum,即本层的sum+candidates[i]大于target时,就可以结束本轮for循环的遍历。

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startindex)
    {
        //终止条件
        if(sum > target) return;
        if(sum == target)
        {
            result.push_back(path);//存放结果集
            return;
        }
        //单层搜索
        for(int i=startindex; i<candidates.size() && sum+candidates[i]<=target; i++)//剪枝
        {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i);//元素可以重复选取,不需要i+1
            sum -= candidates[i];//回溯
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end()); // 需要排序 剪枝前需要排序
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

40.组合总和Ⅱ

三要素及思路

和39.组合总和的不同点

  • 区别1:本题candidates 中的每个数字在每个组合中只能使用一次。
  • 区别2:本题数组candidates的元素是有重复的,而39.组合总和是无重复元素的数组candidates

和39.组合总和的共同点:
最后本题和39.组合总和要求一样,解集不能包含重复的组合。

题目难点:
在于区别2中,集合(数组candidates)有重复元素,但还不能有重复的组合。元素在同一个组合内是可以重复的,但两个组合不能相同。
因此,去重操作就是要去重的是同一树层上的使用过,同一树枝上的都是一个组合里的元素,不用去重。树层去重的话,需要对数组排序

先写逻辑,再确定递归函数参数:
candidates,集合
target,目标值
sum,每条路径累加和,path里元素总和。也可以用target做减法,target=0就说明找到符合的结果了。
startindex,下一层for循环搜索的起始位置。
used,bool型数组,用来记录同一树枝上的元素是否使用过,用于集合去重

终止条件:
和39.组合总和一样,sum大于target和sum等于target,同样的,剪枝操作时可以省掉sum大于target条件了。

单层搜索:
path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。

全局变量:一维数组path来存放符合条件的结果,二维数组result来存放结果集。

去重方式1-使用标记数组

[i] == candidates[i - 1]时:

  • used[i - 1] == true,表示同一树枝 candidates[i - 1]使用过,说明取了下一个数,进入了下一层递归,所以是树枝
  • used[i - 1] == false,表示同一树层 candidates[i - 1]使用过,说明当前取的 candidates[i]是从candidates[i - 1] 回溯而来的,所以是同一树层。

去重方式2-使用下标索引: 排序之后,相同元素会挨在一起。去重时,从i的下一个位置取元素,判断前后元素是否相同,相同则跳过

去重方式3-使用set去重: set记录哪些元素使用过,对同一父节点下同一层去重

代码

  • 去重方式1-标记数组used,使用sum
class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(vector<int>& candidates, int target, int sum, int startindex, vector<bool>& used)
    {
        //终止条件
        if(sum==target)
        {
            result.push_back(path);
            return;
        }
        //单层搜索 去重
        for(int i=startindex; i<candidates.size() && sum+candidates[i]<=target; i++)
        {
            //判断当前值在同一树层是否使用过 同一树层元素不能重复,candidates[i] == candidates[i - 1]时,有
            // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
            // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
            //candidates[i]==candidates[i-1]看是否为同一元素, used[i-1]看元素是在树枝还是树层
            if(i > 0 && candidates[i]==candidates[i-1] && used[i-1]==false) continue;

            //累加和 存入 树枝判断 递归 回溯
            sum += candidates[i];
            path.push_back(candidates[i]);
            used[i] = true;//树枝
            backtracking(candidates, target, sum, i+1, used);//区别于39,i+1,每个数字在每个组合中只能使用一次
            used[i] = false;
            sum -= candidates[i];
            path.pop_back();
        }

    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<bool> used(candidates.size(), false);//bool型数组 判断元素在树层还是树枝
        // 先candidates排序,让其相同的元素都挨在一起。
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0, used);
        return result;
    }
};
  • 去重方式2-索startindex索引下标,使用sum
class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    // 索引下标 不使用数组
    void backtracking(vector<int>& candidates, int target, int sum, int startindex)
    {
        //终止条件
        if(sum==target)
        {
            result.push_back(path);
            return;
        }
        //单层搜索
        for(int i=startindex; i<candidates.size() && sum+candidates[i] <= target; i++)//剪枝
        {
            //去重 从i下一个位置取元素 前后元素相同则跳过
            if(i>startindex && candidates[i]==candidates[i-1]) continue;

            //操作
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i+1);//每个数字在每个组合中只能使用一次
            sum -= candidates[i];
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target)
    {
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0);
        return result;
    }
};
  • 去重方式2-startindex索引下标,不使用sum
class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    //不使用sum
    void backtracking(vector<int>& candidates, int target, int startindex)
    {
        //终止条件
        if(target==0)
        {
            result.push_back(path);
            return;
        }
        //单层搜索
        for(int i=startindex; i<candidates.size() && target-candidates[i] >= 0; i++)//剪枝
        {
            //去重 从i下一个位置取元素 前后元素相同则跳过
            if(i>startindex && candidates[i]==candidates[i-1]) continue;

            //操作
            target -= candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, i+1);//每个数字在每个组合中只能使用一次
            target += candidates[i];
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target)
    {
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0);
        return result;
    }
};
  • 去重方式3-set去重,不使用sum
class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    //3.set去重+不使用sum
    void backtracking(vector<int>& candidates, int target, int startindex)
    {
        if(target==0)
        {
            result.push_back(path);
            return;
        }
        unordered_set<int> uset;
        for(int i=startindex; i<candidates.size() && target-candidates[i]>=0; i++)
        {
            if(uset.find(candidates[i]) != uset.end()) continue;
            uset.insert(candidates[i]);
            target -= candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, i+1);
            target += candidates[i];
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target)
    {
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

剪枝优化和39题的一样

总结

1. 回溯法的理论基础

  • 回溯法的定义:回溯搜索法,一种搜索的方式
  • 回溯法解决的问题:组合、切割、子集、排列、棋盘,注意组合无序,排列有序
  • 回溯法的理解:本质是穷举,回溯问题可以抽象为一棵高度有限的树(N叉树),在集合中递归查找子集。树的宽度就是集合大小,树的深度就是递归的深度。
  • 递归法模板以及伪代码:
//先写逻辑再确定参数
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

2. 组合问题

  • 77.组合:

    • 回溯三要素
    • 用递归控制for循环嵌套的数量,把回溯问题抽象为树形结构,直观看搜索过程:for循环横向遍历,递归纵向遍历,回溯不断调整结果集
    • 剪枝精髓是for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够 题目要求的k个元素了,就没有必要搜索了
  • 216.组合总和Ⅲ:

    • 三种剪枝方式
    • 一个是已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉
    • 还有一个是对for循环选择的起始范围的剪枝
  • 17.电话号码的字母组合:

    • 和前面两道题不一样的是,这道题的每一个数字代表的是不同集合,也就是求不同集合之间的组合。77和216都是求同一个集合中的组合。
    • 注意输入异常的情况,例如输入1 * #按键
    • 使用二维数组或者map建立数字与字母的映射
    • 对当前遍历数字的获取和对应字母集的获取
  • 39.组合总和:

    • 组合没有数量要求
    • 元素可以重复选
  • 40.组合总和Ⅱ:

    • 先排序,再去重
    • 多了一个bool类型的数组used,用来判断元素是在树层还是树枝。即当candidates[i] == candidates[i - 1]时,判断candidates[i]是在树层还是树枝上:
    • used[i - 1] == true,说明同一树枝 candidates[i - 1]使用过,树枝可以重复
    • used[i - 1] == false,说明同一树层 candidates[i - 1]使用过,同一树层元素不能重复,要去重
    • 三种去重方式,使用标记数组去重,使用下标索引去重,使用set去重
    • 使用sum和使用target两种方式找到符合条件的结果,适用于39、216题
  • 对于组合问题,什么时候需要startIndex?

    • 如果是一个集合来求组合的话,就需要startIndex,例如:77.组合,216.组合总和Ⅲ,39.组合总和,40.组合总和Ⅱ
    • 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值