LeetCode - 深度广度优先

六. 搜索算法

1. 深度优先搜索

例题 695 求最大岛屿的面积。

  • 即求最大的连通区域的大小;
  • 主函数遍历矩阵,判断当前元素是否需要递归判断连通岛屿的面积;
  • 辅助函数用于递归计算岛屿面积;
  • 注意遍历过的地方需要将 1 置为 0 ,防止重复计算。
	//辅
	public int searchGrid(int[][] grid, int i, int j) {
        if(i >= grid.length || i < 0 || j >= grid[0].length || j < 0) {
            return 0;
        }
        if (grid[i][j] == 1) {
            //这里置成0,防止下次重新遍历到
            grid[i][j] = 0;
            return 1 + searchGrid(grid, i+1, j) + searchGrid(grid, i, j+1) + searchGrid(grid, i-1, j) + searchGrid(grid, i, j-1);
        }
        return 0;
    }
    //主
    public int maxAreaOfIsland(int[][] grid) {
        int row = grid.length;
        int col = grid[0].length; 
        int area = 0;
        int maxArea = 0;
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (grid[i][j] == 1) {
                    area = searchGrid(grid, i, j);
                    maxArea = maxArea < area ? area : maxArea; 
                }
            }
        }
        return maxArea;
    }

例题547。计算省份的数量

  • 即给出一个是否直接相连的二维矩阵,找出其中有多少个岛屿;
  • 深度优先搜索,主函数遍历每一个城市,判断该城市是否被访问过;
  • 辅函数以当前城市为基准,遍历所有城市判断是否有直接相连的城市,找到直接相连的城市,再以该城市为基准进行递归;
  • 需要维护一个当前城市是否被访问过的一维数组,防止重复访问。
/**
    这个递归函数从进入到退出刚好访问一个省会
     */
    public void search(int[][] isConnected, boolean[] isVisited, int i) {
        if (i < 0 || i >= isConnected.length) {
            return;
        }
        //以当前城市为基准,重新遍历所有城市看是否能访问
        for (int j = 0; j < isConnected.length; j++) {
            if (isConnected[i][j] == 1 && !isVisited[j]) {
                isVisited[j] = true;
                search(isConnected, isVisited, j);
            } 
        }
    }
    public int findCircleNum(int[][] isConnected) {
        /**
        深度优先搜索,从一个城市开始,遍历所有与之直接相连的城市,遍历过的做个标记防止重复遍历
         */
        int sum = 0;
        boolean[] isVisited = new boolean[isConnected.length];
        for (int i = 0; i < isConnected.length; i++) {
            if (!isVisited[i]) {
                sum += 1;
                isVisited[i] = true;
                search(isConnected, isVisited, i);
            }
        }
        return sum;
    }

例题417。太平洋大西洋水流问题,找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。

  • 按照题目的意思顺流判断是否能到达太平洋和大西洋,需要考虑的条件很复杂;
  • 可以考虑倒流,从四边往内倒流,只要能访问即表示可流到;
  • 这种方式的思路是只要当前值可连通,如果四周的值更大,那肯定更满足要求;
  • 上边和左边一定能流到太平洋,下边和右边一定能流到大西洋。
	public static void search(int[][] heights, int i, int j, boolean[][] canReach) {
        if (i < 0 || j >= heights[0].length || canReach[i][j]) {
            return;
        }
        //可访问到即可达,这也是倒流的好处
        canReach[i][j] = true;
        if (i >= 1 && heights[i][j] <= heights[i-1][j]) {
            search(heights,i-1, j, canReach);
        }
        if (i < heights.length-1 && heights[i][j] <= heights[i+1][j]) {
            search(heights,i+1, j, canReach);
        }
        if (j >= 1 && heights[i][j] <= heights[i][j-1]) {
            search(heights,i, j-1, canReach);
        }
        if (j < heights[0].length-1 && heights[i][j] <= heights[i][j+1]) {
            search(heights,i, j+1, canReach);
        }


    }
    
    public List<List<Integer>> pacificAtlantic(int[][] heights) {
        /**
        按照题目的意思顺流判断是否能到达太平洋和大西洋,需要考虑的条件很复杂;
        可以考虑倒流,从四边往内倒流,只要能访问即表示可流到。
        上边和左边表示能流到太平洋,下边和右边表示能流到大西洋
         */
        List<List<Integer>> retList = new ArrayList<>();
        int m = heights.length;
        int n = heights[0].length;
        boolean[][] canReachP = new boolean[m][n];
        boolean[][] canReachA = new boolean[m][n];
        for (int i = 0; i < m; i++) {
            search(heights, i, 0, canReachP);
            search(heights, i, n-1, canReachA);
        }
        for (int j = 0; j < n; j++) {
            search(heights, 0, j, canReachP);
            search(heights, m-1, j, canReachA);
        }
        for (int i = 0; i < m; i ++) {
            for (int j = 0; j < n; j++) {
                if(canReachP[i][j] && canReachA[i][j]) {
                    retList.add(Arrays.asList(i, j));
                }
            }
        }
         return retList;
    }

2. 回溯法

例题46. 全排列

  • 回溯法在递归前修改状态,在回溯时将状态还原;
  • 求全排列时,一个数组的全排列等于选定一个数,加剩余数的全排列;
  • 如 [1, 2, 3] 的全排列等于选定 1 + [2, 3] 的全排列,及选定 2 + [1, 3] 的全排列,及选定 3 + [1, 2] 的全排列;而 [2, 3] 的全排列等于选定 2 加 [3] 的全排列,及选定 3 加 [2] 的全排列,以此类推。
	public void swap(List<Integer> nums, int i, int j) {
        int tmp = nums.get(i);
        nums.set(i, nums.get(j));
        nums.set(j, tmp);
    }
    /**
    从start开始的nums的全排列
     */
    public void sort(List<Integer> nums, List<List<Integer>> retList, int start) {
        if (start == nums.size() - 1) {
            retList.add(new ArrayList<Integer>(nums));
        }
        for (int i = start; i < nums.size(); i++) {
            // 对于当前位置 start,可以与后续每一个位置交换,得到一种排列
            swap(nums, i, start);
            // 第一位选定后(i),继续处理后一个位置
            sort(nums, retList, start + 1);
            //恢复原样
            swap(nums, i, start);
        }
    }
    public List<List<Integer>> permute(int[] nums) {
        List<Integer> newNums = new ArrayList<>();
        for (int i = 0; i < nums.length; i++) {
            newNums.add(nums[i]);
        }
        List<List<Integer>> retList = new ArrayList<>();
        sort(newNums, retList, 0);
        return retList;
    }

还可以不采用原数组交换的方式,而是用一个新的列表来存储当前的全排列数组,这样更好理解,但是需要多占用空间

public static void sort(int[] nums, List<Integer> tmpList, boolean[] isVisited, List<List<Integer>> retList) {
        if (tmpList.size() == nums.length) {
            retList.add(new ArrayList<Integer>(tmpList));
            return;
        }
        //注意这里从0开始
        for (int i = 0; i < nums.length; i++) {
            //被使用的不能再用
            if (isVisited[i]) {
                continue;
            }
            isVisited[i] = true;
            tmpList.add(nums[i]);
            sort(nums, tmpList, isVisited, retList);
            //回溯恢复
            isVisited[i] = false;
            tmpList.remove(Integer.valueOf(nums[i]));
        }
    }
    public static List<List<Integer>> permute(int[] nums) {
        boolean[] isVisited = new boolean[nums.length];
        List<List<Integer>> retList = new ArrayList<>();
        //利用一个tmpList存储当前的全排列数组
        List<Integer> tmpList = new ArrayList<>();
        sort(nums, tmpList, isVisited, retList);
        return retList;
    }

如果全排列的数组中含重复数字,则得想办法进行剪枝:

public void sort(int[] nums, List<Integer> tmpList, boolean[] isVisited, List<List<Integer>> retList) {
        if (tmpList.size() == nums.length) {
            retList.add(new ArrayList<Integer>(tmpList));
            return;
        }
        //注意这里从0开始
        for (int i = 0; i < nums.length; i++) {
            //被使用的不能再用
            if (isVisited[i]) {
                continue;
            }
            //含重复数字只需添加这一步剪枝
            //!isVisited[i-1]是因如果前一个相同数字没被使用,那么这个数字就会在后面再次被选中造成重复
            if (i > 0 && nums[i] == nums[i-1] && !isVisited[i-1]) {
                continue;
            }
            isVisited[i] = true;
            tmpList.add(nums[i]);
            sort(nums, tmpList, isVisited, retList);
            //回溯恢复
            isVisited[i] = false;
            //删除最后一个
            tmpList.remove(tmpList.size() - 1);
        }
    }
    public List<List<Integer>> permuteUnique(int[] nums) {
        boolean[] isVisited = new boolean[nums.length];
        List<List<Integer>> retList = new ArrayList<>();
        //利用一个tmpList存储当前的全排列数组
        List<Integer> tmpList = new ArrayList<>();
        //含重复数字需要排序将重复的数字排列在一起
        Arrays.sort(nums);
        sort(nums, tmpList, isVisited, retList);
        return retList;
    }

例题77. 组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

  • 排列回溯的是交换的位置,而组合回溯的是否把当前的数字加入结果中;
  • 排列递归时传入的 start 是 start+1,而组合传入的是 i+1,因为组合时永远是在后一位寻找数字。
/**
    组合与排列类似,只是在回溯的时候是将添加的元素恢复
     */
    public void myComb(List<Integer> tmpList, List<List<Integer>> retList, int start, int n, int k) {
        if (k == 0) {
            retList.add(new ArrayList(tmpList));
            return;
        }
        //注意这里i <= n-k+1, 因为超过n-k+1的话,tmpList总长度永远小于k
        for (int i = start; i <= n-k+1; i ++) {
            tmpList.add(i);
            //这里的start是i+1,而不是排列中的start+1
            myComb(tmpList, retList, i+1, n, k-1);
            tmpList.remove(tmpList.size()-1);
        }
    }
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> retList = new ArrayList<>();
        myComb(new ArrayList<>(), retList, 1, n, k);
        return retList;
    }

例题40. 含重复数字的组合题。给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用 一次 。解集不能包含重复的组合。

  • 组合和全排列一样,如果含有重复数字,第一步要做的就是给数组排序,使得相同元素在一起;
  • 第二步是剪枝,i>0 && candidates[i] == candidates[i-1] && !isUsed[i-1]。
class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        boolean[] isUsed = new boolean[candidates.length];
        List<List<Integer>> retList = new ArrayList<>();
        List<Integer> list = new ArrayList<>();
        dfs(candidates, target, list, retList, isUsed, 0);
        return retList;
    }

    public void dfs(int[] candidates, int target, List<Integer> list, List<List<Integer>> retList, boolean[] isUsed, int start) {
        if (target == 0) {
            retList.add(new ArrayList<>(list));
            return;
        }
        if (target < 0) {
            return;
        }
        for (int i = start; i < candidates.length; i++) {
            if (isUsed[i]) {
                continue;
            }
        	//和全排列一样的剪枝方法
            if (i>0 && candidates[i] == candidates[i-1] && !isUsed[i-1]) {
                continue;
            }
            list.add(candidates[i]);
            isUsed[i] = true;
            dfs(candidates, target - candidates[i], list, retList, isUsed, i+1);
            isUsed[i] = false;
            list.remove(list.size() - 1);
        }
    }
}

排列组合小结:

  • 排列题:如果不含重复数字,可以通过对原数组进行位置交换得到全排列,注意此时要传入 start,dfs 时是 start+1;也可以另外开辟空间存储单个排列,并建立 isUsed 判断当前元素是否使用过,此时则无需传入 start,for 循环中 i = 0。如果含重复数字,则一定要使用方法二另外开辟空间,并且需要对数组排序和剪枝。
  • 组合题:组合题必须另外开辟空间存储单个组合,并且必须传入 start,for 循环中 i = start,因为组合时不能往回获取元素,dfs 时是 i + 1;如果数组中的元素可重复选取,那 dfs 时是 i。由于一定要传入start,则 isUsed 无需建立。但如果是含重复数字的组合题,则需要建立 isUsed 用于剪枝。

例题79. 单词搜索。在矩阵中搜索指定的单词,存在返回 true,否则返回 false。

  • 单词搜索回溯的是当前字母是否被使用;
public boolean search(char[][] board, char[] arr, int k, int n, int i, int j, boolean[][] isUsed) {
		//搜索到所有字母才算成功
        if (k == n) {
            return true;
        }
        if (i >= board.length || j >= board[0].length || i < 0 || j < 0) {
            return false;
        }
        if (arr[k] == board[i][j] && !isUsed[i][j]) {
            isUsed[i][j] = true;
            //选择了一个字母后,再在其周围搜索
            if(search(board, arr, k+1, n, i+1, j, isUsed) 
            || search(board, arr, k+1, n, i-1, j, isUsed) 
            || search(board, arr, k+1, n, i, j+1, isUsed) 
            || search(board, arr, k+1, n, i, j-1, isUsed)) {
                return true;
            } else {
            	//如果后续不满足要求,当前字母回退重新搜索
                isUsed[i][j] = false;
                return false;
            }
        }
        return false;

    }
    public boolean exist(char[][] board, String word) {
        char[] arr = word.toCharArray();
        // 存放字母是否被使用
        boolean[][] isUsed = new boolean[board.length][board[0].length];
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[0].length; j++) {
                isUsed = new boolean[board.length][board[0].length];
                if (search(board, arr, 0, word.length(), i, j, isUsed)) {
                    return true;
                }
            }
        }
        return false;
         
    }

例题 51. N 皇后。将 n 个皇后放置在 n×n 的棋盘上,并且每一行、列、左斜、右斜最多只有一个皇后。

  • N 皇后问题有隐藏条件,即每一行都只能放置一个皇后,因此我们遍历时只需一行一行进行;
  • 回溯时回溯的是当前行里,该列是否放置皇后;
  • 需要建立方法判断当前是否能放置皇后。
/**
    判断当前是否能放置皇后
     */
    public boolean canPlace( int row, int col, int n, char[][] board) {
        // 检查列是否有皇后冲突
        for (int i = 0; i < row; i++) {
            if (board[i][col] == 'Q') {
                return false;
            }
        }
        // 检查右上方是否有皇后冲突
        for (int i = row - 1, j = col + 1; i >=0 && j < n; i--, j++) {
            if (board[i][j] == 'Q') {
                return false;
            }
        }
        // 检查左上方是否有皇后冲突
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (board[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }

    public void search(int k, int n, List<List<String>> retList, char[][] board, int i) {
        if (k == n) {
            List<String> tmpList = new ArrayList<>();
            for (char[] c : board) {
                String tmp = String.valueOf(c);
                tmpList.add(tmp);
            }
            retList.add(tmpList);
            return;
        }
        if (i < 0 || i >= n) {
            return;
        }
        //针对第i行的每一列
        for (int p = 0; p < n; p++) {
            if (canPlace(i, p, n, board)) {
                //放置
                board[i][p] = 'Q';
                search(k+1, n, retList, board, i+1);
                //撤销放置
                board[i][p] = '.';   
            } 
        }
    }
    public List<List<String>> solveNQueens(int n) {
        //初始化board数组
        char[][] board = new char[n][n];
        for (char[] c : board) {
            Arrays.fill(c, '.');
        }
        List<List<String>> retList = new ArrayList<>();
        //由于每行只能放一个,因此只需要对j进行遍历
        search(0, n, retList, board, 0);
        return retList;
    }

3. 广度优先算法

广度优先不同与深度优先搜索,它是一层层进行遍历的,因此需要用先入先出的队列进行遍历。由于是按层次进行遍历,广度优先搜索时按照“广”的方向进行遍历的,也常常用来处理最短路径等问题。最短路径就是当前层所在的层数

例题 934. 最短的桥。给定一个二维 0-1 矩阵,其中 1 表示陆地,0 表示海洋,每个位置与上下左右相连。已知矩阵中有且只有两个岛屿,求最少要填海造陆多少个位置才可以将两个岛屿相连。

  • 实际上是求两个岛屿间的最短距离;
  • 先深度优先寻找到其中一个岛屿的所有坐标,再对该岛屿的每个坐标进行广度优先一层一层遍历,每遍历完一层,距离加一,直到找到第二个岛屿;
  • 遍历过的位置要进行记录,防止重复遍历;记得需要明确层与层间的边界,才能正确增加距离。
/**
    递归寻找第一个岛屿坐标
     */
    public void dfs(int i, int j, int[][] grid, Queue<int[]> retList, boolean[][] isVisited) {
        if (i < 0 || j < 0 || i >= grid.length || j>= grid[0].length) {
            return; 
        }
        if (grid[i][j] == 1) {
            grid[i][j] = 2;
            isVisited[i][j] = true;
            retList.add(new int[]{i, j});
            dfs(i+1, j, grid, retList, isVisited);
            dfs(i, j+1, grid, retList, isVisited);
            dfs(i-1, j, grid, retList, isVisited);
            dfs(i, j-1, grid, retList, isVisited);
        }

    }
    public int shortestBridge(int[][] grid) {
        /**
        先通过深度优先找到其中一个岛屿的所有坐标,并将值改成2,
        再通过广度优先一层一层寻找其与另一个岛屿的距离
         */
        Queue<int[]> landList = new LinkedList<>();
        boolean[][] isVisited = new boolean[grid.length][grid[0].length];
        //寻找第一个岛屿坐标
        boolean flag = true;
        for (int i = 0; i < grid.length && flag; i++) {
            for (int j = 0; j < grid[0].length && flag; j++) {
                if (grid[i][j] == 1) {
                    dfs(i, j, grid, landList, isVisited);
                    flag = false;//用于退出循环
                }
            }
        }
        //开始广度优先搜索
        int dis = 0;
        int[][] direc = new int[][]{{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
        while (!landList.isEmpty()) {
            //记录当前层的长度
            int size = landList.size();
            for (int k = 0; k < size; k ++) {
                int[] land = landList.poll();
                for (int i  = 0; i < 4; i++) {
                    int p = land[0] + direc[i][0];
                    int q = land[1] + direc[i][1];
                    if (p >= 0 && p < grid.length && q >= 0 && q < grid[0].length && !isVisited[p][q]) {
                        isVisited[p][q] = true;
                        //找到另一个岛屿直接返回
                        if (grid[p][q] == 1) {
                            return dis;
                        }
                        landList.add(new int[]{p, q});
                    }
                }
            }
            //搜索完当前层,到下一层长度需加一
            dis ++;
        }
        return Integer.MAX_VALUE;
    }

例题126.单词接龙 。按字典 wordList 完成从单词 beginWord 到单词 endWord 转化,一个表示此过程的 转换序列 是形式上像 beginWord -> s1 -> s2 -> … -> sk 这样的单词序列。

  • 广度优先建图获取可达关系,深度优先+回溯寻找所有可能的解;
  • 把每个字符串当作一个节点,如果某个字符串可以由另一个字符串通过变换一个字符得到,则这两个字符串可连接;通过广度优先为每个字符串确定所在的层级,用一个map存储当前字符串所在的层级,一个map存储当前字符串可达哪些下层字符串(注意同层或上层的字符串不能被加入到可达的下层字符串中)。
  • 将可达关系送入到深度优先算法中,递归+回溯寻找所有可能的解。
/**
     寻找当前字符串的可达字符串
     */
    public static Set<String> getWords(String beginWord, List<String> dict, Map<String, Integer> map) {
        Set<String> retSet = new HashSet<>();
        //当前将字符串的每一位进行替换,看是否在字典中
        for (int i = 0; i < beginWord.length(); i++) {
            char[] word = beginWord.toCharArray();
            for (char c = 'a'; c <= 'z'; c++) {
                word[i] = c;
                String tmp = String.valueOf(word);
                if (dict.contains(tmp)) {
                    //避免平级字符串被加入到可达字符串中
                    if (map.get(tmp) == null || map.get(tmp) > map.get(beginWord)) {
                        retSet.add(tmp);
                        map.put(tmp, map.get(beginWord) + 1);
                    }
                }
            }
        }
        return retSet;
    }
    /**
    dfs回溯寻找所有可能的解
     */
    public static void dfs(Map<String, Set<String>> wordMap, String beginWord, String endWord, List<String> tmpList, List<List<String>> retList) {
        if (endWord.equals(beginWord)) {
            retList.add(new ArrayList(tmpList));
            return;
        }
        Set<String> wordSet = wordMap.get(beginWord);
        for (String word : wordSet) {
            tmpList.add(word);
            dfs(wordMap, word, endWord, tmpList, retList);
            tmpList.remove(word);
        }

    }
    public static List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
        if (!wordList.contains(endWord)) {
            return new ArrayList<>();
        }
        Queue<String> queue = new LinkedList<>();
        queue.add(beginWord);
        //存储当前字符串所在的层级
        Map<String, Integer> map = new HashMap<>();
        //存储从一个字符串可以到后续哪些字符串
        Map<String, Set<String>> wordMap = new HashMap<>();
        int dis = 0;
        while(!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0 ; i < size; i++) {
                String word = queue.poll();
                //存储当前字符串所在层数
                map.putIfAbsent(word, dis);
                Set<String> words = getWords(word, wordList, map);
                wordMap.put(word, words);
                //当一个字符串有多个父级时,避免该字符串被重复加入队列
                queue.addAll(words.stream().filter(w -> !queue.contains(w)).collect(Collectors.toSet()));
            }
            dis ++;
        }
        List<List<String>> retList = new ArrayList<>();
        List<String> tmpList = new ArrayList<>();
        tmpList.add(beginWord);
        if (map.get(endWord) != null) {
            dfs(wordMap, beginWord, endWord, tmpList, retList);
        }
        return retList;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Leetcode 高频考题整理确实是很有帮助的,以下是一些常见的 Leetcode 高频考题整理: 1. 数组和字符串问题: - 两数之和 (Two Sum) - 三数之和 (Three Sum) - 最长回文子串 (Longest Palindromic Substring) - 盛最多水的容器 (Container With Most Water) - 下一个排列 (Next Permutation) 2. 链表问题: - 反转链表 (Reverse Linked List) - 删除链表中的倒数第N个节点 (Remove Nth Node From End of List) - 合并两个有序链表 (Merge Two Sorted Lists) - 链表中环的检测 (Linked List Cycle) - 环形链表的起始点 (Linked List Cycle II) 3. 树和图问题: - 二叉树的遍历 (Binary Tree Traversal) - 二叉树的最大深度 (Maximum Depth of Binary Tree) - 二叉树的最小深度 (Minimum Depth of Binary Tree) - 图的深度优先搜索 (Depth First Search) - 图的广度优先搜索 (Breadth First Search) 4. 动态规划问题: - 爬楼梯 (Climbing Stairs) - 最大子序和 (Maximum Subarray) - 打家劫舍 (House Robber) - 不同路径 (Unique Paths) - 最长递增子序列 (Longest Increasing Subsequence) 5. 排序和搜索问题: - 快速排序 (Quick Sort) - 归并排序 (Merge Sort) - 二分查找 (Binary Search) - 搜索旋转排序数组 (Search in Rotated Sorted Array) - 寻找峰值 (Find Peak Element) 这只是一些常见的 Leetcode 高频考题整理,还有很多其他题目也值得关注。通过刷题和整理高频题目,可以提高对算法和数据结构的理解和应用能力。希望对你有所帮助!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值