力扣刷题系列——回溯算法I

回溯算法思想及经典例题(一)

回溯法

可以提前看看公众号文章:https://mp.weixin.qq.com/s/g5uvxi1lyxmWC4LtP0Bdlw (从二叉树遍历到回溯算法,包含例题:二叉树路径和等于给定目标值的路径(力扣113)、集合的所有子集合列表(力扣78)

1、概念

      回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。

      回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

      许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

2、基本思想

      在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。

      若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。

      而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

     回溯法中,首先需要明确下面三个概念:
    (一)约束函数:约束函数是根据题意定出的。通过描述合法解的一般特征用于去除不合法的解,从而避免继续搜索出这个不合法解的剩余部分。因此,约束函数是对于任何状态空间树上的节点都有效、等价的。
    (二)状态空间树:刚刚已经提到,状态空间树是一个对所有解的图形描述。树上的每个子节点的解都只有一个部分与父节点不同。
    (三)扩展节点、活结点、死结点:所谓扩展节点,就是当前正在求出它的子节点的节点,在深度优先搜索中,只允许有一个扩展节点。活结点就是通过与约束函数的对照,节点本身和其父节点均满足约束函数要求的节点;死结点反之。由此很容易知道死结点是不必求出其子节点的(没有意义)。

3、用回溯法解题的一般步骤:

      首先,要通过读题完成下面三个步骤:
(1)描述解的形式,定义一个解空间,它包含问题的所有解。
(2)构造状态空间树。
(3)构造约束函数(用于杀死节点)。
 
      然后就要通过深度优先搜索思想完成回溯,完整过程如下:
(1)设置初始化的方案(给变量赋初值,读入已知数据等)。
(2)变换方式去试探,若全部试完则转(7)。
(3)判断此法是否成功(通过约束函数),不成功则转(2)。
(4)试探成功则前进一步再试探。
(5)正确方案还未找到则转(2)。
(6)已找到一种方案则记录并打印。
(7)退回一步(回溯),若未退到头则转(2)。
(8)已退到头则结束或打印无解。

4、算法框架

(1)问题框架

    设问题的解是一个n维向量(a1,a2,………,an),约束条件是ai(i=1,2,3,…..,n)之间满足某种条件,记为f(ai)。

(2)非递归回溯框架

int a[n],i;
 初始化数组a[];
 i = 1;
 while (i>0(有路可走)   and  (未达到目标))  // 还未回溯到头
 {
     if(i > n)                                              // 搜索到叶结点
     {   
           搜索到一个解,输出;
     }
     else                                                   // 处理第i个元素
     { 
           a[i]第一个可能的值;
           while(a[i]在不满足约束条件且在搜索空间内)
           {
               a[i]下一个可能的值;
           }
           if(a[i]在搜索空间内)
          {
               标识占用的资源;
               i = i+1;                              // 扩展下一个结点
          }
          else 
         {
               清理所占的状态空间;            // 回溯
               i = i –1; 
          }
 }

(3)递归的算法框架

         回溯法是对解空间的深度优先搜索,在一般情况下使用递归函数来实现回溯法比较简单,其中i为搜索的深度,框架如下:

 int a[n];
 try(int i)
 {
     if(i>n)
        输出结果;
      else
     {
        for(j = 下界; j <= 上界; j=j+1)  // 枚举i所有可能的路径
        {
            if(fun(j))                 // 满足限界函数和约束条件
              {
                 a[i] = j;
               ...                         // 其他操作
                 try(i+1);
               回溯前的清理工作(如a[i]置空值等);
               }
          }
      }
 }
回溯法有“通用解题法”之称。用它可以系统地搜索问题的所有解。回溯法是一个既带有系统性又带有跳跃性的搜索算法。

      在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。 而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

例题讲解

(力扣46)给定一个不重复的数字集合,返回其所有可能的全排列。例如:

  • 输入:[1, 2, 3]

  • 输出:

    [
      [1,2,3],
      [1,3,2],
      [2,1,3],
      [2,3,1],
      [3,1,2],
      [3,2,1]
    ]

代码实现:

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> lists = new ArrayList<>();
        int[] arr = new int[nums.length];
        backTrack(lists,nums,new ArrayList(),arr);
        return lists;
    }
    public void backTrack(List<List<Integer>> lists,int[] nums,ArrayList<Integer> list,int[] arr){
        if(list.size()==nums.length){
            lists.add(new ArrayList(list));
            return;
        }
        for(int i=0;i<nums.length;i++){
            if(arr[i]==1)   continue;
            arr[i] = 1;
            list.add(nums[i]);
            backTrack(lists, nums, list, arr);
            arr[i] = 0;
            list.remove(list.size()-1);
        }
    }
}

下面以经典的排列(permutation)和组合(combination)问题为例,讲讲求解回溯法问题的要点:候选集合

内容将会包含:

  • 回溯法的”选什么“问题与候选集合

  • 全排列、排列、组合问题的回溯法解法

  • 回溯法问题中,如何维护候选集合

  • 回溯法问题中,如何处理失效元素

回溯法的重点:“选什么”

我们说过,回溯法实际上就是在一棵决策树上做遍历的过程。那么,求解回溯法题目时,我们首先要思考所有决策路径的形状。例如,子集问题的决策树如下图所示:

                                                                                    子集问题的决策树

决策树形状主要取决于每个结点处可能的分支,换句话说,就是在每次做决策时,我们 “可以选什么”、 “有什么可选的”

对于子集问题而言,这个“选什么”的问题非常简单,每次只有一个元素可选,要么选、要么不选。不过,对于更多的回溯法题目,“选什么”的问题并不好回答。这时候,我们就需要分析问题的候选集合,以及候选集合的变化,以此得到解题的思路。

全排列问题:如何维护候选集合

让我们拿经典的全排列问题来讲解回溯法问题的候选集合概念。

在全排列问题中,决策树的分支数量并不固定。我们一共做 n 次决策,第 i 次决策会选择排列的第 i 个数。选择第一个数时,全部的 n 个数都可供挑选。而由于已选的数不可以重复选择,越往后可供选择的数越少。以 n=3 为例,决策树的形状如下图所示:

                                                                                     全排列问题的决策树

如果从候选集合的角度来思考,在进行第一次选择时,全部的 3 个数都可以选择,候选集合的大小为 3。在第二次选择时,候选集合的大小就只有 2 了;第三次选择时,候选集合只剩一个元素。可以看到,全排列问题候选集合的变化规律是:每做一次选择,候选集合就少一个元素,直到候选集合选完为止。我们可以在上面的决策树的每个结点旁画上候选集合的元素,这样看得更清晰。

                                                                         全排列问题有候选集合的决策树

可以看到,已选集合候选集合是补集的关系,它们加起来就是全部的元素。而在回溯法的选择与撤销选择的过程中,已选集合和候选集合是此消彼长的关系。

如何根据这样的思路写出代码呢?当然可以用 HashSet 这样的数据结构来表示候选集合。但如果你这么去写的话,会发现代码写起来比较啰嗦,而且 set 结构的“遍历-删除”操作不太好写。在这里,我不展示使用 set 结构的代码。大家只要明白一条要点:在一般情况下,候选集合使用数组表示即可。 候选集合上需要做的操作并不是很多,使用数组简单又高效。

在子集问题中,我们定义了变量 k,表示当前要对第 k 个元素做决策。实际上,变量 k 就是候选集合的边界,指针 k 之后的元素都是候选元素,而 k 之前都是无效元素,不可以再选了。

                                                                                用数组表示候选集合

而每次决策完之后将 k 加一,就是将第 k 个元素移出了候选集合。

                                                                                  将第 k 个元素移出候选集合

在全排列问题中,我们要处理的情况更难一些。每次做决策时,候选集合中的所有元素都可以选择,也就是有可能删除候选集合中间的元素,这样数组中会出现“空洞”。这种情况该怎么处理呢?我们可以使用一个巧妙的方法,先将要删除的元素与第 k 个元素交换,再将 k 加一,过程如下方动图所示:

不知道你有没有注意到,上图中候选集合之外的元素画成了蓝色,这些实际上就是已选集合。前面分析过,已选集合与候选集合是互补的。将蓝色部分看成已选集合的话,我们从候选集合中删除的元素,正好加入了已选集合中。也就是说,我们可以只用一个数组同时表示已选集合和候选集合!

理解了图中的关系之后,题解代码就呼之欲出了。我们只需使用一个 current 数组,左半边表示已选元素,右半边表示候选元素。指针 k 不仅是候选元素的开始位置,还是已选元素的结束位置。我们可以得到一份非常简洁的题解代码:

public List<List<Integer>> permute(List<Integer> nums) {
    List<Integer> current = new ArrayList<>(nums);
    List<List<Integer>> res = new ArrayList<>();
    backtrack(current, 0, res);
    return res;
}

// current[0..k) 是已选集合, current[k..N) 是候选集合
void backtrack(List<Integer> current, int k, List<List<Integer>> res) {
    if (k == current.size()) {
        res.add(new ArrayList<>(current));
        return;
    }
    // 从候选集合中选择
    for (int i = k; i < current.size(); i++) {
        // 选择数字 current[i]
        Collections.swap(current, k, i);
        // 将 k 加一
        backtrack(current, k+1, res);
        // 撤销选择
        Collections.swap(current, k, i);
    }
}

注意写在递归函数上方的注释。在写回溯法问题的代码时,你需要时刻清楚什么是已选集合,什么是候选集合。注释中的条件叫做“不变式”。一方面,我们在函数中可以参考变量 k 的含义,另一方面,我们在做递归调用的时候,要保证这个条件始终成立。特别注意代码中递归调用传入的参数是 k+1 ,即删除一个候选元素,而不是传入 i+1

n 中取 k 的排列

                                                                                   n 中取 k 的排列的决策树

题解代码如下所示,只需要修改递归结束的条件即可。

public List<List<Integer>> permute(List<Integer> nums, int k) {
    List<Integer> current = new ArrayList<>(nums);
    List<List<Integer>> res = new ArrayList<>();
    backtrack(k, current, 0, res);
    return res;
}

// current[0..m) 是已选集合, current[m..N) 是候选集合
void backtrack(int k, List<Integer> current, int m, List<List<Integer>> res) {
    // 当已选集合达到 k 个元素时,收集结果并停止选择
    if (m == k) {
        res.add(new ArrayList<>(current.subList(0, k)));
        return;
    }
    // 从候选集合中选择
    for (int i = m; i < current.size(); i++) {
        // 选择数字 current[i]
        Collections.swap(current, m, i);
        backtrack(k, current, m+1, res);
        // 撤销选择
        Collections.swap(current, m, i);
    }
}

注意这里 k 是题目的输入,所以原先我们代码里的变量 k 重命名成了 m。此外,就是递归函数开头的 if 语句条件发生了变化,当已选集合达到 k 个元素时,就收集结果停止递归。

组合问题:失效元素

public List<List<Integer>> combine(List<Integer> nums, int k) {
    Deque<Integer> current = new ArrayDeque<>();
    List<List<Integer>> res = new ArrayList<>();
    backtrack(k, nums, 0, current, res);
    return res;
}

// current 是已选集合, nums[m..N) 是候选集合
void backtrack(int k, List<Integer> nums, int m, Deque<Integer> current, List<List<Integer>> res) {
    // 当已选集合达到 k 个元素时,收集结果并停止选择
    if (current.size() == k) {
        res.add(new ArrayList<>(current));
        return;
    }
    // 从候选集合中选择
    for (int i = m; i < nums.size(); i++) {
        // 选择数字 nums[i]
        current.addLast(nums.get(i));
        // 元素 nums[m..i) 均失效
        backtrack(k, nums, i+1, current, res);
        // 撤销选择
        current.removeLast();
    }
}

由于已选集合与候选集合并非互补,这里用单独的数组存储已选元素,这一点上与子集问题类似。

组合问题与子集问题的关系

也许是排列 & 组合的 CP 感太重,所以我们在思考组合问题的解法的时候会自然地从排列问题上迁移。其实,组合问题和子集问题有很密切的联系。

由子集问题求解组合问题

组合问题可以看成是子集问题的特殊情况。从 n 中取 k 个数的组合,实际上就是求 n 个元素的所有大小为 k 的子集。也就是说,组合问题的结果是子集问题的一部分。我们可以在子集问题的决策树的基础上,当已选集合大小为 k 的时候就不再递归,就可以得到组合问题的决策树。

                                                                    在子集问题决策树基础上得到的组合问题决策树

public List<List<Integer>> subsets(List<Integer> nums) {
    Deque<Integer> current = new ArrayDeque<>();
    List<List<Integer>> res = new ArrayList<>();
    backtrack(nums, 0, current, res);
    return res;
}

// current 是已选集合, nums[m..N) 是候选集合
void backtrack(List<Integer> nums, int m, Deque<Integer> current, List<List<Integer>> res) {
    // 收集决策树上每一个结点的结果
    res.add(new ArrayList<>(current));
    if (m == nums.size()) {
	    // 当候选集合为空时,停止递归
        return;
    }
    // 从候选集合中选择
    for (int i = m; i < nums.size(); i++) {
        // 选择数字 nums[i]
        current.addLast(nums.get(i));
        // 元素 nums[m..i) 均失效
        backtrack(nums, i+1, current, res);
        // 撤销选择
        current.removeLast();
    }
}

可以看到,每次做决策都会增加一个已选元素。当递归到第 k 层时,计算的就是大小为 k 的子集。不过,这样写出的子集问题解法没有原解法易懂,我还是更推荐原解法。

经典例题

1.分割回文串

(力扣131)给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回 s 所有可能的分割方案。

示例:

输入: "aab"
输出:
[
  ["aa","b"],
  ["a","a","b"]
]

代码实现:

//https://leetcode-cn.com/problems/palindrome-partitioning/solution/hui-su-you-hua-jia-liao-dong-tai-gui-hua-by-liweiw/
class Solution {
    public List<List<String>> partition(String s) {
        List<List<String>> lists = new ArrayList<>();
        if(s==null) return lists;
        int len = s.length();
        LinkedList<String> list = new LinkedList<>();
        backtracking(s,0,len,list,lists);
        return lists;
    }
    public void backtracking(String s,int start,int end,LinkedList<String> list,List<List<String>> lists){
        if(start == end){
            lists.add(new ArrayList(list));
            return;
        }
        for(int i=start;i<end;i++){
            if(!isTrue(s,start,i)){
                continue;
            }
            list.addLast(s.substring(start,i+1));
            backtracking(s,i+1,end,list,lists);
            list.removeLast();
        }
    }
    public boolean isTrue(String s,int start,int end){
        while(start<end){
            if(s.charAt(start)!=s.charAt(end)){
                return false;
            }
            start++;
            end--;
        }
        return true;
    }
}

2.组合总和

(力扣216)找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

  • 所有数字都是正整数。
  • 解集不能包含重复的组合。 

示例 1:

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

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]

代码实现:

class Solution {

    List<List<Integer>> ans = new ArrayList<>();
    //尽量不要使用遗留类Stack,Deque完成可以实现Stack功能。
    Deque<Integer> path = new ArrayDeque<>();

    /**
     * 整体思路
     *
     * @param k k个数
     * @param n 组合成n(累加和为n)
     *          组合数范围1~9,且不重复
     * @return
     */
    public List<List<Integer>> combinationSum3(int k, int n) {
        dfs(k, n, 1);
        return ans;
    }

    /**
     * @param k     k个数
     * @param n     组合成n(累加和为n)
     * @param start 组合数范围start~9,且不重复
     */
    private void dfs(int k, int n, int start) {
        if (k < 0 || n < 0) return;   // 剪枝

        // 刚好k个数、刚好递减成0(说明这k个数累加和刚好为n)
        if (k == 0 && n == 0) {
            if (!path.isEmpty())
                ans.add(new ArrayList<>(path));
            return;
        }

        // 从start开始,到9的范围,然后就是递归分支(选、不选) 记得恢复状态
        for (int i = start; i < 10; i++) {
            path.push(i);
            dfs(k - 1, n - i, ++start);
            path.pop();
        }
    }
}

3.递增子序列

(力扣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]]
说明:

  • 给定数组的长度不会超过15。
  • 数组中的整数范围是 [-100,100]。
  • 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。

代码实现:

class Solution {
    List<Integer> temp = new ArrayList<Integer>();
    List<List<Integer>> ans = new ArrayList<List<Integer>>();

    public List<List<Integer>> findSubsequences(int[] nums) {
        dfs(0, Integer.MIN_VALUE, nums);
        return ans;
    }

    public void dfs(int cur, int last, int[] nums) {
        if (cur == nums.length) {
            if (temp.size() >= 2) {
                ans.add(new ArrayList<Integer>(temp));
            }
            return;
        }
        if (nums[cur] >= last) {
            temp.add(nums[cur]);
            dfs(cur + 1, nums[cur], nums);
            temp.remove(temp.size() - 1);
        }
        if (nums[cur] != last) {
            dfs(cur + 1, last, nums);
        }
    }
}

4.幂集

(力扣1698,面试题08.04)幂集。编写一种方法,返回某集合的所有子集。集合中不包含重复的元素。

说明:解集不能包含重复的子集。

示例:

 输入: nums = [1,2,3]
 输出:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]

代码实现:

class Solution {
     public List<List<Integer>> subsets(int[] nums) {
        //结果集合
        List<List<Integer>> list = new ArrayList();
        //回溯方法
        backtrack(list,new ArrayList<>(),nums,0);
        return list;
    }
    public void backtrack(List<List<Integer>>list,List<Integer>tempList,int []nums,int start){
        list.add(new ArrayList<>(tempList));
        for(int i = start;i<nums.length;i++) {
            tempList.add(nums[i]);
            backtrack(list, tempList, nums, i + 1);
            tempList.remove(tempList.size() - 1);
        }
    }
}
//https://leetcode-cn.com/problems/power-set-lcci/solution/javahui-su-jie-jue-zi-ji-wen-ti-by-ctrl-cv/

5.有重复字符串的排列组合

(力扣1702,面试题08.08)有重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合。

示例1:

 输入:S = "qqe"
 输出:["eqq","qeq","qqe"]
示例2:

 输入:S = "ab"
 输出:["ab", "ba"]
提示:

  • 字符都是英文字母。
  • 字符串长度在[1, 9]之间。

代码实现:

import java.util.List;
import java.util.Arrays;
class Solution {
    public String[] permutation(String S) {
        List<String> res = new ArrayList<String>();
        int len = S.length();
        if (len == 0) return new String[0];
        boolean[] used = new boolean[len];
        char[] sChar = S.toCharArray();

        StringBuilder path = new StringBuilder(len); 

        // 排序是为了后面的剪枝
        Arrays.sort(sChar);

        dfs(res, sChar, len, path, 0, used);
        return res.toArray(new String[0]);
    }
    
    /**
    * @param res 结果集
    * @param sChar 输入字符数组
    * @param len 字符数组长度
    * @param path 根结点到任意结点的路径
    * @param depth 当前树的深度
    * @param used 使用标记数组
    */
    private void dfs(List<String> res
                    , char[] sChar
                    , int len
                    , StringBuilder path
                    , int depth
                    , boolean[] used) {
        // 到达叶子结点
        if (depth == len) {
            res.add(path.toString());
            return;
        }

        for (int i = 0; i < len; i++) {
            if (!used[i]) {

                // 根据已排序字符数组, 剪枝
                if (i > 0 && sChar[i] == sChar[i-1] && !used[i-1]) {
                    continue;
                }

                path.append(sChar[i]);
                used[i] = true; // 标记选择
                dfs(res, sChar, len, path, depth+1, used);
                path.deleteCharAt(depth);
                used[i] = false; // 撤销选择
            }
        }
    }
}

以上内容整合了几方资源,仅作为个人日后复习查阅,侵删。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,关于力扣刷题C++常用操作,我可以给你一些常见的操作和技巧: 1. 使用 STL 容器和算法库:STL(Standard Template Library)是 C++ 标准库中的一个重要组成部分,包含了许多常用的容器和算法。在力扣刷题中,使用 STL 可以大大提高代码的效率和可读性。例如,vector 可以用来存储动态数组,sort 可以用来排序等等。 2. 使用 auto 关键字:auto 关键字可以自动推导变量类型,可以减少代码量和提高可读性。例如,auto x = 1; 可以自动推导出 x 的类型为 int。 3. 使用 lambda 表达式:lambda 表达式是 C++11 中引入的一种匿名函数,可以方便地定义一些简单的函数对象。在力扣刷题中,使用 lambda 表达式可以简化代码,例如在 sort 函数中自定义比较函数。 4. 使用位运算:位运算是一种高效的运算方式,在力扣刷题中经常会用到。例如,左移运算符 << 可以用来计算 2 的幂次方,右移运算符 >> 可以用来除以 2 等等。 5. 使用递归:递归是一种常见的算法思想,在力扣刷题中也经常会用到。例如,二叉树的遍历、链表的反转等等。 6. 使用 STL 中的 priority_queue:priority_queue 是 STL 中的一个容器,可以用来实现堆。在力扣刷题中,使用 priority_queue 可以方便地实现一些需要维护最大值或最小值的算法。 7. 使用 STL 中的 unordered_map:unordered_map 是 STL 中的一个容器,可以用来实现哈希表。在力扣刷题中,使用 unordered_map 可以方便地实现一些需要快速查找和插入的算法。 8. 使用 STL 中的 string:string 是 STL 中的一个容器,可以用来存储字符串。在力扣刷题中,使用 string 可以方便地处理字符串相关的问题。 9. 注意边界条件:在力扣刷题中,边界条件往往是解决问题的关键。需要仔细分析题目,考虑各种边界情况,避免出现错误。 10. 注意时间复杂度:在力扣刷题中,时间复杂度往往是评判代码优劣的重要指标。需要仔细分析算法的时间复杂度,并尽可能优化代码。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值