LeetCode刷题,按照我自己的理解写了题解和解题思路

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rliESD14-1666144405400)(assets/1665804772939.png)]
LeetCode:98,257,494,332,133,17,112,113,46,207,210
实现的方式不一定是最好的,但都是按照我自己的理解去写的,仅供参考,如果有更好的解法不妨发出来一起研究!!!

98.验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

    1. 节点的左子树只包含 小于 当前节点的数。
    1. 节点的右子树只包含 大于 当前节点的数。
    1. 所有左子树和右子树自身必须也是二叉搜索树

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/validate-binary-search-tree

解题思路

需要验证该二叉搜索树,就需要将该树遍历一遍,可以使用深度优先的思想,二叉树的遍历有三种方式,符合其中的前序和中序遍历,这里使用的方式为前序

具体思路:

  1. 当前节点如果满足1、2两点定义,那么该节点符合二叉搜索树
  2. 因为又子树,又不好直接拿到其子树,所以考虑使用递归
  3. 子树的节点需要满足两点
    • 左子树节点小于当前节点的父节点的值
    • 右子树节点大于当前节点的父节点的值
  4. 题目给的方法只有一个参数,明显是不够的,因此需要创建一个新的方法
  5. 该方法需要传入当前需要验证的节点以及该节点所被允许的最大和最小值
  6. 当所有节点遍历过一遍,没有出现不符合的情况,则该树是一颗二叉搜索树

通关代码

	public boolean isValidBST(TreeNode root) {
        return isValid(root,Long.MIN_VALUE,Long.MAX_VALUE);
    }
    boolean isValid(TreeNode root,long left,long right){
        if(root==null) {
            return true;
        }
        if(root.val <= left || root.val >= right) {
            return false;
        }
        return isValid(root.left,left,root.val) && isValid(root.right,root.val,right);
    }

注意点:根节点所被允许的值是随机的,也就是-∞到+∞

257. 二叉树的所有路径

题目描述:

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点。

解题思路

返回所有的路径,可以使用深度优先算法

具体思路:

​ 使用深度优先算法遍历整棵树,当遇到左右子节点为null的节点,说明当前是一个完整的路径。

代码实现

时间复杂度O(n2)

	private List<String> result;

    public List<String> binaryTreePaths(TreeNode root) {
        result = new ArrayList();
        if(root==null) {
            return result;
        }
        dfs(root,"");
        return result;
    }

    void dfs(TreeNode root, String path){
        if(root==null) {
            // 此时这是一个完整的路径
            result.add(path);
            return;
        }
        path = path.length() == 0 ? root.val+ : path + "->" + root.val;
        if(root.right==null && root.left==null) {
            result.add(path);
        } else {
            if(root.left!=null) {
                dfs(root.left,path);
            }
            if(root.right!=null) {
                dfs(root.right,path);
            }
        }
    }

494. 目标和

题目描述:

给你一个整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :

  • 例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/target-sum

解题思路

分析:每次添加符号都可以选择 + 或者 - 号,因此每次都可以将问题拆分为两个子问题。当nums中的数据都被使用过之后,再比较最后的总和,如果最终结果==taget,那么就是其中一种表达式。不难想象,符号的添加情况好比一颗树二叉树,每条路径都是一种符号添加的方式,要从其中选取结果等于target的方式,可以考虑使用深度优先的思想来实现

具体思路:

  1. 输入nums以及target
  2. 输出最终结果==target的总方式
  3. 需要传入参数nums,i(当前到哪了),sum(当前总和),target(目标值)
  4. 当添加的符号数量等于nums.length时,比较和结果==target?
  5. 递归调用,分别添加 “-” 与 "+"符号进行计算并递归

代码实现

时间复杂度(2n

	int total;
    public int findTargetSumWays(int[] nums, int target) {
        this.total=0;
        findTargetSum(nums,0,0,target);
        return total;
    }

    void findTargetSum(int[] nums, int index,int sum,int target){
        if(nums.length==index) {
            if(sum==target) {
                total++;
            }
            return;
        }
        findTargetSum(nums,index+1,sum+nums[index],target);
        findTargetSum(nums,index+1,sum-nums[index],target);
    }

332. 重新安排行程

给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

  • 例如,行程 [“JFK”, “LGA”] 与 [“JFK”, “LGB”] 相比就更小,排序更靠前。

假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/reconstruct-itinerary

解题思路

分析:根据题意,需要对所有ticket进行遍历,然后以JFK为起点,将所有的ticket都使用且只使用一次。并且当有多种行程都可以选择时,选择字典排序小的ticket。

所有的ticket画在一起,类似于一张图,将该图的所有边都走且只走一遍,并且按照字典顺序选择,最后即为所求组合。

此处选择的算法思想类似于图的深度优先算法,以JFK为起点进行遍历。因为一定存在至少一种合理的行程,所以该图必定是一个连通图。即从JFK开始,必定能把所有的节点全部走完。

具体思路:

  1. 输入tickets,考虑到需要将其连接在一起形成一个图。如果使用邻接表,那么取出数据就会比较麻烦,因此可以考虑使用map集合。以from为key,to为value,但是to可以有多个,又考虑到字典顺序问题,可以使用PriorityQueue<>。其中的存储的数据默认就是按照字典顺序存储

  2. 输出最后的result,即最后的组合,使用List集合存储

  3. 根据输入构建的图,进行以JFK为起点的dfs

  4. dfs中,将以JFK为起点的所有目的队列地取出,即从map取出

  5. 遍历所有目地的的队列,并且以这些目的地作为新的起点,进行dfs,直到所有节点遍历完成。每遍历一个节点,就将该节点从队列中中移除

  6. 将from添加到result中,最后到的节点必定是先添加到result的,最后呈现的是

    JFK->…->最终地点,这一点类似于拓扑排序,最后访问的必定是最先结束的,最先访问的是最后结束的。

  7. 返回结果

实现代码

	LinkedList<String> result;
    Map<String,PriorityQueue<String>> map;
    public List<String> findItinerary(List<List<String>> tickets) {
        this.result = new LinkedList();
        this.map = new HashMap();
        for(List<String> ticket : tickets) {
            String from = ticket.get(0);
            String to = ticket.get(1);

            PriorityQueue<String> destinations = map.get(from);
            if(destinations==null) {
                destinations = new PriorityQueue();
                map.put(from,destinations);
            }
            destinations.add(to);
        }
        dfs("JFK");
        return result;
    }

    void dfs(String from) {
        PriorityQueue<String> destinations =  map.get(from);
        while(destinations!=null && !destinations.isEmpty()){
            dfs(destinations.remove());
        }
        result.addFirst(from);
    }

133. 克隆图

题目描述:

给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。

图中的每个节点都包含它的值 valint) 和其邻居的列表(list[Node])。

测试用例格式:

简单起见,每个节点的值都和它的索引相同。例如,第一个节点值为 1(val = 1),第二个节点值为 2(val = 2),以此类推。该图在测试用例中使用邻接列表表示。

邻接列表 是用于表示有限图的无序列表的集合。每个列表都描述了图中节点的邻居集。

给定节点将始终是图中的第一个节点(值为 1)。你必须将 给定节点的拷贝 作为对克隆图的引用返回。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/clone-graph

解题思路

分析:输入的是连通图中的一个节点,返回一个拷贝后连通图的节点。

拷贝当前节点后,需要将当前节点的邻居节点也拷贝,同样,邻居节点的邻居也需要拷贝。可以考虑使用深度优先遍历,遍历到当前节点的邻居的邻居的…最后一个邻居,然后返回当前节点,再由上层递归添加到上层递归的当前节点。

具体思路:

  1. 输入一个node节点,如果该节点为空,直接返回null。
  2. 创建一个map集合,用于存储已经拷贝过的节点
  3. 将该节点进行深度优先遍历
  4. 如果该节点已经拷贝,直接返回map中该节点的拷贝即可
  5. 遍历该节点的邻居集合,将源节点邻居拷贝,并赋予拷贝节点,按照分析中的操作,重复调用dfs遍历算法
  6. 返回输入的节点的拷贝

代码实现

	public Node cloneGraph(Node node) {
        if(node==null) {
            return null;
        }
        Map<Node,Node> map = new HashMap();
        return dfs(node, map);
    }

    Node dfs(Node node,Map<Node,Node> map) {
        if(map.containsKey(node)) {
            return map.get(node);
        }
        Node clone = new Node(node.val, new ArrayList());
        map.put(node,clone);
        for(Node neighbor:node.neighbors) {
            clone.neighbors.add(dfs(neighbor,map));
        }
        return clone;
    }

17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/letter-combinations-of-a-phone-number

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OEpU3UrX-1666144405402)(assets/1665841448000.png)]

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

解题思路

digits的范围2-9,因此字母组合为2-9对应字母的组合

如果输入“23”,那么就先从abc中选择一个字母,再从def中选择一个进行组合,直到所有将所有字母进行组合。即有全排列那种感觉,2中任意字母和3中任意字母进行的全排列。

输入多少的数字,每种组合就有多少个字母。

具体思路

  1. 输入digits,创建一个keyMap,里面是2-9对应的字母映射
  2. 创建一个List集合result,用于存储结果
  3. 如digits.length=0,return 空集合
  4. 进行dfs遍历,每次从keyMap集合中读取对应的字母
  5. 当前组合长度为digits长度,则向result中添加当前组合

代码实现

String[] keyMap = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    List<String> result;
    public List<String> letterCombinations(String digits) {
        result = new ArrayList();
        if(digits.length()<=0) {
            return result;
        }
        dfs("",0,digits);
        return result;
    }
    void dfs(String combination,int index, String digits){
        if(digits.length()==index) {
            result.add(combination);
            return;
        }
        for(char c : keyMap[digits.charAt(index)-'2'].toCharArray()) {
            dfs(combination+c,index+1,digits);
        }
    }

112. 路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。

叶子节点 是指没有子节点的节点。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/path-sum

输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/path-sum

解题思路

遍历整棵数,使用深度优先遍历,对路径上所有节点的值进行相加,当遇到叶子节点,判断最后相加的targetSum是否是目标值,如果是,那么直接return true,如果不是,那么继续遍历。

代码实现

    public boolean hasPathSum(TreeNode root, int targetSum) {
        return dfs(root,0,targetSum);
    }

    boolean dfs(TreeNode node, int curSum, int targetSum) {
        if(node==null) {
            return false;
        }
        if(node.left==null && node.right==null) {
            return targetSum==curSum+node.val;
        }
        return dfs(node.left,curSum+node.val,targetSum) || dfs(node.right,curSum+node.val,targetSum);
    }

113. 路径总和 II

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/path-sum-ii

解题思路

遍历整棵二叉树,并将每个节点的值相加,当该节点为叶子节点时,比较最后这一条路径上所有节点相加的值是否为目标值targetSum,如果是,则将该路径添加到result结果集中

具体思路:

  1. 输入根节点,targetSum
  2. 创建变量result,用于存储结果,创建变量curList,用于存储当前路径上的所有节点的值,最后符合条件的需要添加到result中
  3. 从根节点开始进行深度优先遍历
  4. 当遍历到某一叶子节点,判断当前从根节点到该叶子节点的总和是否等于targetSum,如果是,那么将当前路径pathList添加到result中,需要注意的是,需要new ArrayList(pathList),将这个结果添加到result中,因为这是引用传递,如果直接传入pathList,那么后续的递归修改了pahtList,那么之前的递归也会被影响。
  5. 如果不是叶子节点,则分别对其左右子节点进行遍历。
  6. 最后将当前递归添加到pathList中的root.val移除,以免影响下一轮的递归

代码实现

    List<List<Integer>> result;
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        result = new ArrayList();
        List<Integer> curList = new ArrayList();
        path(root,curList,0,targetSum);
        return result;
    }

    void path(TreeNode root,List<Integer> pathList, int curSum, int targetSum) {
        if(root==null) {
            return;
        }
        pathList.add(root.val);
        if(root.left==null && root.right == null && curSum+root.val==targetSum) {
            result.add(new ArrayList(pathList));
        }
        path(root.left,pathList,curSum+root.val,targetSum);
        path(root.right,pathList,curSum+root.val,targetSum);
        // 回溯,将之前添加的root.val移除,防止影响下一次递归
        pathList.remove(pathList.size()-1);
    }

46. 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/permutations/

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

解题思路

每次可以选择其中的一位进行排列,如果第一次选1,那么第二次只能选2或者3。第三次只有一个选择,因此不需要进行选择。即如果已选数量为数组长度-1,那么可以直接结束当前排列。

如何知道当前选择的数据有哪些?进行交换,如果选择第二位,那么就将第二位交换到第一位,选择的第二位是谁,谁就交换到第二位。以此类推,直到nums.length-1位,将本次排列结果添加到result中。

每次交换并且使用之后,都需要将数组还原,即在同一次遍历中要进行两次交换,中间进行排列的选择。

具体思路:

DFS+回溯

  1. 输入nums数组
  2. 创建并调用dfs(int[] nums,int index)方法,index为 当前排列到了第几位
  3. 如果当前index==nums.length-1,将当前路径集合添加到result中。
  4. 否则进行排列,从index开始循环,即当前排列到了第几位,将nums[index]与nums[i]进行交换。i表示应该选取下标为多少的数据作为第 index 位
    List<List<Integer>> result;
    public List<List<Integer>> permute(int[] nums) {
        result = new ArrayList();
        dfs(nums,0);
        return result;
    }
    void dfs(int[] nums, int index) {
        if(index==nums.length-1) {
            List<Integer> permutes = new ArrayList();
            for(int num : nums) {
                permutes.add(num);
            }
            result.add(permutes);
        } else {
            for(int i = index;i<nums.length;i++) {
                swap(nums,i,index);
                dfs(nums,index+1);
                swap(nums,i,index);
            }
        }
    }
    void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i]=nums[j];
        nums[j] = temp;
        
    }

207. 课程表

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/course-schedule

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

解题思路

如果不存在循环学习的课程,即0->1->0,学习0之前要学1,学习1之前要学习0,那么就可以按照一定顺序完成所有课程。即可以对所有课程进行拓扑排序,如果不能进行拓扑排序,那么就不能完成所有课程。

课程的序号为0->numCourses-1,等于已经知道了每个课程的名称

将所有课程构成一个图,对该图进行拓扑排序。或者可以记录下所有课程的入度,以及其下一门可以学习的课程。

具体思路:

  1. 创建一个Course类,记录学习当前课程需要前置学习的课程的数量inDegree,即入度,并且记录下学了这门课之后可以学习哪些课程 next
  2. 创建一个List集合,用于保存所有课程,并且初始化课程入度为0
  3. 遍历prerequisites,该数组中存放课程之间的前后关系,prerequisite[0]的学习前提是学习了prerequisite[1]。将prerequisite[0]的入度+1,并且将prerequisite[0]添加到prerequisite[1]的next,即下一个可以学习的课程集合
  4. 遍历courses集合,将所有入度为0的课程添加到队列中
  5. 如果该队列不为空,那么就遍历队列中课程的next,将next集合中所有的课程入度-1,如果其中存在入度-1之后==0的情况,那么将其添加到队列中,表示该课程可以学习了
  6. 检查是否存在入度不为0的课程,如果有,那么就存在循环的情况,即0->1->0,学习0之前要学1,学习1之前要学习0这种情况,表示无法完成全部课程,return false

代码实现

	class Course {
        int inDegree;
        // 下一个可以学的课程
        List<Course> next;
        Course(int inDegree) {
            this.inDegree = inDegree;
            this.next = new ArrayList();
        }
    }
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        List<Course> courses = new ArrayList();
        for(int i = 0;i<numCourses;i++) {
            courses.add(new Course(0));
        }
        for(int[] prerequisite:prerequisites) {
            // 当前课程入度+1,  学习prerequisite[0]之前必须要先学prerequisite[1],因此就是1->0,0课程的入度+1
            courses.get(prerequisite[0]).inDegree++;
            // [1]接下来可以学的课程
            courses.get(prerequisite[1]).next.add(courses.get(prerequisite[0]));
        }
        // 将所有入度为0的课程添加到队列中
        Queue<Course> q = new LinkedList();
        for(Course course: courses) {
            if(course.inDegree==0) {
                q.add(course);
            }
        }
        // 遍历q,将入度为0的course出队,并将其接下来可以学习的课程添加到队列中,前提是该课程入度为0
        while(!q.isEmpty()){
            Course course = q.poll();
            for(Course nextCourse:course.next) {
                nextCourse.inDegree--;
                if(nextCourse.inDegree==0) {
                    q.add(nextCourse);
                }
            }
        }
        // 遍历查看是否有入度不为0的课程,如果有,那么说明无法全部学习,即存在环
        for(int i = 0;i < numCourses; i++) {
            if(courses.get(i).inDegree!=0) {
                return false;
            }
        }
        return true;
    }

210. 课程表 II

题目描述:

现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。

例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/course-schedule-ii

输入:numCourses = 2, prerequisites = [[1,0]]
输出:[0,1]
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

解题思路

与序号为207的题目一样,同点是207不需要返回课程学习的顺序,只需要返回是否能全部学习即可。这里可以定义一个int[] result数组,用于存储最后的结果,其中的值为每个课程的序号。

实现代码

	int[] result;
    class Course {
        // 课程入度,入度为0即可学习
        int inDegree;
        int index;
        List<Course> next;
        Course(int inDegree,int index){
            this.inDegree = inDegree;
            this.index = index;
            this.next = new ArrayList();
        }
    }
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        result = new int[numCourses];
        int resultNum = 0;
        List<Course> courses = new ArrayList();
        // 初始化课程信息
        for(int i = 0;i<numCourses;i++) {
            courses.add(new Course(0,i));
        }
        // 遍历prerequisites
        for(int[] prerequisite : prerequisites) {
            // [0]课程需要学习[1]课程后才可学习
            courses.get(prerequisite[0]).inDegree++;
            // 将[0]课程加入[1]课程学习之后可以学习的课程
            courses.get(prerequisite[1]).next.add(courses.get(prerequisite[0]));
        }
        // 学习入度为0的课程,并且将该课程之后可学习课程入度-1,当入度为0,添加到可学习队列中
        Queue<Course> queue = new LinkedList();
        for(Course course : courses){
            if(course.inDegree==0){
                queue.add(course);
            }
        }
        // 开始学习课程
        while(queue.size()!=0){
            Course course = queue.poll();
            result[resultNum++] = course.index;
            for(Course nextCourse : course.next) {
                nextCourse.inDegree--;
                if(nextCourse.inDegree==0){
                    queue.add(nextCourse);
                }
            }
        }
        // 如果存在入度不为0的课程,那么说明无法全部完成,返回一个空集合
        for(int i =0;i<numCourses;i++){
            if(courses.get(i).inDegree!=0) {
                return new int[0];
            }
        }
        return result;
    }
     queue.add(course);
        }
    }
    // 开始学习课程
    while(queue.size()!=0){
        Course course = queue.poll();
        result[resultNum++] = course.index;
        for(Course nextCourse : course.next) {
            nextCourse.inDegree--;
            if(nextCourse.inDegree==0){
                queue.add(nextCourse);
            }
        }
    }
    // 如果存在入度不为0的课程,那么说明无法全部完成,返回一个空集合
    for(int i =0;i<numCourses;i++){
        if(courses.get(i).inDegree!=0) {
            return new int[0];
        }
    }
    return result;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值