算法学习——技巧小结7(回溯:排列、组合、子集)

引言

你是否曾遇到过这样的问题:面对一个复杂的决策过程,每一步都有多种选择,你需要找出所有满足条件的解决方案?比如,如何列出数字1, 2, 3的所有可能排列?或者,如何从一个集合中找出所有和为特定值的子集?这类问题看似千变万化,但它们背后都隐藏着一种强大而优雅的解决思路——回溯算法

回溯算法是解决“枚举所有可能性”问题的利器,它本质上是一种采用深度优先搜索策略的暴力尝试法。但与纯粹的暴力枚举不同,回溯算法融入了“试错”与“剪枝”的思想:它像一位聪明的探险家,在决策树的丛林中探索每一条路径;一旦发现某条路不可能通向目的地,便果断折返,尝试下一个岔路口。这种“选择-验证-撤销”的核心步骤,使得它在许多场景下比盲目枚举高效得多。

在本篇文章中,我们将从最经典的排列、组合、子集问题入手,由浅入深地揭开回溯算法的神秘面纱。你将看到,一个清晰易懂的回溯模板如何成为解决这些问题的“万能钥匙”。更重要的是,我们将一起探索这个基础模板如何灵活变通,应用于更复杂的场景。

一、回溯框架

核心框架

def backtrack(选项表):
    if 满足结束条件:
        保存结果
        return
    
    for 选项 in 选项表:
        做选择
        下一层回溯
        撤销选择

具体还可根据选择表是否含重复元素,以及能否重复选择同一个元素,将核心框架,引申出三种子框架。

第一类: 无重复元素、不可重复选择

# 组合/子集问题回溯算法框架
def backtrack(start: int):
    for i in range(start, len(nums)):
        track.append(nums[i])	# 做选择
        backtrack(i + 1)  # 选项表从i+1开始,因为已选择的不能再选
        track.pop()  #  撤销选择

# 排列问题回溯算法框架
def backtrack(cnt):
    for i in range(len(nums)):
        # 剪枝
        if used[i]:
            continue
            
        # 做选择
        used[i] = True
        track.append(nums[i])

        backtrack(cnt+1)	# cnt表示目前已经选择了几个,进入下一次需要加1
        
        # 撤销选择
        track.pop()
        used[i] = False

第二类:有重复元素,不可重复选择

# 组合/子集问题回溯算法框架
nums.sort()
def backtrack(start: int):
    for i in range(start, len(nums)):
        # 跳过值相同的相邻树枝
        if i > start and nums[i] == nums[i - 1]:
            continue
        
        track.append(nums[i])  # 做选择
        backtrack(i + 1)  # 注意参数
        track.pop()  # 撤销选择

# 排列问题回溯算法框架
nums.sort()
def backtrack(cnt):
    for i in range(len(nums)):
        # 剪枝
        if used[i]:
            continue
            
        # 重复选项x是首次出现,可以选也可以不选
        # 若重复选项x'并非首次出现,那我们就需看其前面的那个x是否被选择:
        # 1.如果x已被选择,则x'可以选也可以不选
        # 2.如果x未被选择,则x'不可选,需要跳过
        if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
            continue
            
        # 做选择
        used[i] = True
        track.append(nums[i])

        backtrack(cnt+1)	# cnt表示目前已经选择了几个,进入下一次需要加1
        
        # 撤销选择
        track.pop()
        used[i] = False

与第一类不同,第二类是有重复元素的,比如 [ 1 , 2 , 2 ′ , 2 ′ ′ , 3 ] [1, 2, 2',2'',3] [1,2,2,2′′,3]

若我们要解决它的组合问题,例如三个数之和为6的组合有哪些,就会发现类似 [ 1 , 2 , 3 ] [1, 2,3] [1,2,3] [ 1 , 2 ′ , 3 ] [1, 2',3] [1,2,3]是重复的,因此我们需要在进行选取之前先排序,然后在每一层回溯中,遇到重复元素时,只在该元素首次出现时选择,后面出现时跳过即可。

若我们需要找出它的全排列时可发现,如 [ 1 , 2 , 2 ′ , 2 ′ ′ , 3 ] [1, 2, 2' ,2'',3] [1,2,2,2′′,3]就和 [ 1 , 2 ′ , 2 , 2 ′ ′ , 3 ] [ 1,2',2,2'',3] [1,2,2,2′′,3] [ 1 , 2 ′ , 2 ′ ′ , 2 , 3 ] [ 1,2',2'',2,3] [1,2,2′′,2,3] [ 1 , 2 ′ ′ , 2 ′ , 2 , 3 ] [ 1,2'',2',2,3] [1,2′′,2,2,3] [ 1 , 2 ′ ′ , 2 , 2 ′ , 3 ] [ 1,2'',2,2',3] [1,2′′,2,2,3] [ 1 , 2 , 2 ′ ′ , 2 ′ , 3 ] [ 1,2,2'',2',3] [1,2,2′′,2,3]会产生重复。为了避免这种重复情况,我们需要先将选项表排序,使得其中的重复元素都相邻放置,然后再做选择时,确保重复元素的相对位置不变。

如上例子中, [ 1 , 2 ′ , 2 , 2 ′ ′ , 3 ] [ 1,2',2,2'',3] [1,2,2,2′′,3] [ 1 , 2 ′ , 2 ′ ′ , 2 , 3 ] [ 1,2',2'',2,3] [1,2,2′′,2,3] [ 1 , 2 ′ ′ , 2 ′ , 2 , 3 ] [ 1,2'',2',2,3] [1,2′′,2,2,3] [ 1 , 2 ′ ′ , 2 , 2 ′ , 3 ] [ 1,2'',2,2',3] [1,2′′,2,2,3] [ 1 , 2 , 2 ′ ′ , 2 ′ , 3 ] [ 1,2,2'',2',3] [1,2,2′′,2,3]中重复元的 2 2 2的相对位置发生了变化,不应该选择,
这样,就仅存在 [ 1 , 2 , 2 ′ , 2 ′ ′ , 3 ] [1, 2, 2' ,2'',3] [1,2,2,2′′,3],就不会出现重复。

第三类:无重复元素,可重复选择

# 组合/子集问题回溯算法框架
def backtrack(start):
    # 回溯算法标准框架
    for i in range(start, len(nums)):
        track.append(nums[i])   # 做选择
        backtrack(i)  # 可以重复选择,所以下一层依旧从i开始
        track.pop()  # 撤销选择


# 排列问题回溯算法框架
def backtrack(cnt):
    for i in range(len(nums)):   
        track.append(nums[i])  # 做选择
        backtrack(cnt+1)  # cnt表示目前已经选择了几个,进入下一次需要加1
        track.pop()  # 撤销选择

二、基本题型

类型1:无重复、不可复选

78. 子集

在这里插入图片描述

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = []  # 存储所有子集结果
        track = []  # 记录当前递归路径(当前正在构建的子集)
        n = len(nums)  # 输入集合的长度

        def backtrack(start: int) -> None:
            """
            回溯递归核心函数

            通过递归遍历所有可能的子集组合,每次递归调用都代表一个新的决策点。

            :param start: int 当前可选的起始索引
            :return: None
            """
            # 每个节点都是一个子集,直接加入结果
            # 注意:这里不需要终止条件,因为循环会自动结束
            res.append(track.copy())

            # 遍历从start开始的所有元素
            for i in range(start, n):
                track.append(nums[i])  # 做出选择,将当前元素加入子集
                backtrack(i + 1)  # 递归进入下一层,i+1确保不重复使用同一元素
                track.pop()  # 撤销选择,回溯

        backtrack(0)  # 从索引0开始回溯
        return res

46. 全排列

在这里插入图片描述

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        """
        生成输入数组的所有可能排列(使用回溯算法)

        该方法采用回溯算法递归地构建所有可能的排列组合。通过维护一个已使用标记数组来避免重复选择元素,
        当当前路径长度等于输入数组长度时,将该路径加入结果列表。

        :param nums: List[int] 输入整数数组,元素可以重复
        :return: List[List[int]] 返回所有可能的排列组合,每个组合都是一个整数列表
        """
        n = len(nums)  # 输入数组的长度
        res = []  # 存储所有排列结果
        track = []  # 记录当前递归路径(当前正在构建的排列)
        used = [False] * n  # 标记数组,记录元素是否已被使用

        def backtrack(cnt) -> None:
            """
            回溯递归函数,用于生成所有排列

            通过深度优先搜索遍历所有可能路径,使用标记数组避免重复选择元素。
            当路径长度等于输入数组长度时,将当前路径加入结果列表。

            :return: None
            """
            # 终止条件:当前路径长度等于数组长度
            if cnt == n:
                # 注意需要使用copy(),否则后续修改会影响已存储的结果
                res.append(track.copy())
                return

            # 遍历所有可能的选择
            for i in range(n):
                # 跳过已使用的元素
                if used[i]:
                    continue

                # 做出选择
                used[i] = True  # 标记当前元素已使用
                track.append(nums[i])  # 将当前元素加入路径

                # 递归进入下一层决策
                backtrack(cnt+1)

                # 撤销选择(回溯)
                track.pop()  # 移除最后添加的元素
                used[i] = False  # 取消标记

        # 从空路径开始回溯
        backtrack(0)
        return res

77. 组合

在这里插入图片描述

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        def backtrack(cnt: int, start: int) -> None:
            """
            回溯递归核心函数

            通过递归遍历所有可能的组合方式,使用start参数避免重复组合。
            
            :param cnt: int 当前已选择的元素个数
            :param start: int 当前可选的起始数字
            :return: None
            """
            # 终止条件:已选元素个数等于k
            if cnt == k:
                # 必须使用copy(),否则后续修改会影响已存储的结果
                res.append(track.copy())
                return

            # 遍历从start到n的所有数字
            for i in range(start, n + 1):
                track.append(i)  # 做出选择,将当前数字加入组合
                # 递归进入下一层:
                # cnt+1 表示已选数字增加1
                # i+1 确保下一个数字比当前大,保持升序避免重复
                backtrack(cnt + 1, i + 1)
                track.pop()  # 撤销选择,回溯

        res = []  # 存储所有组合结果
        track = []  # 记录当前递归路径(当前正在构建的组合)
        backtrack(0, 1)  # 从已选0个元素,起始数字1开始回溯
        return res

216. 组合总和 III

在这里插入图片描述

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        # 提前终止条件:1-9中k个数的最大和为45(9+8+...+ (9-k+1))
        if n > 45:
            return []

        nums = list(range(1, 10))  # 候选数字1-9
        m = len(nums)  # 候选数字长度(固定为9)
        res = []  # 存储所有符合条件的组合
        track = []  # 记录当前递归路径(当前尝试的组合)

        def backtrack(start: int, sum_val: int) -> None:
            """
            回溯递归核心函数

            通过递归遍历所有可能的组合方式,使用剪枝条件优化搜索过程。

            :param start: int 当前可选的起始索引
            :param sum_val: int 当前路径的数字和
            :return: None
            """
            # 终止条件:组合长度等于k且和等于n
            if len(track) == k and sum_val == n:
                res.append(track.copy())  # 保存当前组合
                return

            # 遍历从start开始的所有候选数字
            for i in range(start, m):
                # 剪枝条件1:当前和加上数字后超过目标值,直接终止循环
                if sum_val + nums[i] > n:
                    break

                track.append(nums[i])  # 做出选择,将当前数字加入组合
                # 递归进入下一层:
                # i+1 确保每个数字只用一次
                # sum_val+nums[i] 更新当前和
                backtrack(i + 1, sum_val + nums[i])
                track.pop()  # 撤销选择,回溯

        backtrack(0, 0)  # 从索引0,初始和0开始回溯
        return res

类型2:有重复、不可复选

47. 全排列 II

在这里插入图片描述

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        nums.sort()  # 先排序使相同元素相邻,便于后续剪枝
        n = len(nums)
        used = [False] * n  # 标记数组元素是否被使用过
        res = []  # 存储所有排列结果
        track = []  # 记录当前路径(当前排列)

        def backtrack(cnt: int) -> None:
            """
            回溯递归函数,用于生成所有唯一排列
            
            通过深度优先搜索遍历所有可能路径,使用剪枝条件避免重复排列。
            当路径长度等于输入数组长度时,将当前路径加入结果列表。
            
            :param cnt: int 当前路径长度(已选元素个数)
            :return: None
            """
            # 终止条件:当前路径长度等于数组长度
            if cnt == n:
                res.append(track.copy())  # 添加当前路径的副本到结果
                return

            # 遍历所有可选元素
            for i in range(n):
                # 跳过已使用的元素
                if used[i]:
                    continue

                # 剪枝条件:当前元素与前一个相同且前一个未被使用
                # 说明同一树层已经使用过相同元素,跳过以避免重复
                if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
                    continue

                # 选择当前元素
                used[i] = True
                track.append(nums[i])

                # 递归进入下一层决策树
                backtrack(cnt + 1)

                # 撤销选择(回溯)
                track.pop()
                used[i] = False

        backtrack(0)  # 从路径长度0开始回溯
        return res

90. 子集 II

在这里插入图片描述

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        nums.sort()  # 先排序使相同元素相邻,便于后续剪枝
        n = len(nums)
        res = []  # 存储所有子集结果
        track = []  # 记录当前递归路径(当前正在构建的子集)

        def backtrack(start: int) -> None:
            """
            回溯递归核心函数

            通过递归生成所有子集,使用start参数控制遍历起始位置,通过剪枝条件避免重复子集。

            :param start: int 当前可选的起始索引
            :return: None
            """
            # 每个节点都是一个子集,直接加入结果
            res.append(track.copy())

            # 遍历从start开始的所有元素
            for i in range(start, n):
                # 剪枝条件:跳过同一树层使用过的相同元素
                if i > start and nums[i] == nums[i - 1]:
                    continue

                track.append(nums[i])  # 做出选择,将当前元素加入子集
                backtrack(i + 1)  # 递归进入下一层,注意i+1避免重复使用同一元素
                track.pop()  # 撤销选择,回溯

        backtrack(0)  # 从索引0开始回溯
        return res

40. 组合总和 II

在这里插入图片描述
类型:有重复、不可复选

class Solution:
    def combinationSum2(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()  # 关键步骤:排序使相同数字相邻,便于后续剪枝
        n = len(nums)
        res = []  # 存储所有符合条件的组合
        track = []  # 记录当前递归路径(当前尝试的组合)

        def backtrack(start: int, sum_val: int) -> None:
            """
            回溯递归核心函数

            通过递归遍历所有可能的组合方式,使用剪枝条件优化搜索过程。

            :param start: int 当前可选的起始索引
            :param sum_val: int 当前路径的数字和
            :return: None
            """
            # 终止条件:当前和等于目标值
            if sum_val == target:
                res.append(track.copy())  # 保存当前组合
                return

            # 遍历从start开始的所有候选数字
            for i in range(start, n):
                # 剪枝条件1:跳过同一树层使用过的相同数字
                if i > start and nums[i] == nums[i - 1]:
                    continue

                # 剪枝条件2:当前和加上数字后超过目标值,直接终止循环
                # (因为数组已排序,后续数字只会更大)
                if sum_val + nums[i] > target:
                    break

                track.append(nums[i])  # 做出选择,将当前数字加入组合
                # 递归进入下一层:
                # i+1 确保每个数字只用一次
                # sum_val+nums[i] 更新当前和
                backtrack(i + 1, sum_val + nums[i])
                track.pop()  # 撤销选择,回溯

        backtrack(0, 0)  # 从索引0,初始和0开始回溯
        return res

类型3:无重复,可复选

39. 组合总和

在这里插入图片描述

class Solution:
    def combinationSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()  # 关键步骤:排序便于后续剪枝操作
        n = len(nums)
        res = []  # 存储所有符合条件的组合
        track = []  # 记录当前递归路径(当前尝试的组合)

        def backtrack(start: int, sum_val: int) -> None:
            """
            回溯递归核心函数

            通过递归遍历所有可能的组合方式,使用剪枝条件优化搜索过程。

            :param start: int 当前可选的起始索引
            :param sum_val: int 当前路径的数字和
            :return: None
            """
            # 终止条件:当前和等于目标值
            if sum_val == target:
                res.append(track.copy())  # 保存当前组合
                return

            # 遍历从start开始的所有候选数字
            for i in range(start, n):
                # 剪枝条件:当前和加上数字后超过目标值,直接终止循环
                # (因为数组已排序,后续数字只会更大)
                if sum_val + nums[i] > target:
                    break

                track.append(nums[i])  # 做出选择,将当前数字加入组合
                # 递归进入下一层:
                # 传递i而不是i+1,允许重复使用当前数字
                # sum_val+nums[i] 更新当前和
                backtrack(i, sum_val + nums[i])
                track.pop()  # 撤销选择,回溯

        backtrack(0, 0)  # 从索引0,初始和0开始回溯
        return res

三、变体题型

140. 单词拆分 II

在这里插入图片描述
139. 单词拆分中,需要判断 s s s是否能拆分为 w o r d D i c t wordDict wordDict,我们只用关心能否、有无、对错,最值,而不关心到底有几种“能”的情况,这类型的题一般用动态规划,即根据递推公式一步一步布林布林地从头计算到尾,返回 d p dp dp表的最后一个结果即可。

而本题和139. 单词拆分不一样,该题更关心的是将 s s s拆分成 w o r d D i c t wordDict wordDict中的单词,总共有多少种拆分方式,而这用到则应该是回溯,因为你回溯中的路径 t r a c k track track正好可以用来存放不同的可能。

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
        """
        将字符串分割为字典中的单词组合(回溯算法实现)

        该方法通过回溯算法找出所有可能的单词分割方式,使得分割后的每个单词都存在于给定的字典中。
        使用记忆化剪枝优化算法效率,避免重复计算。

        :param s: str 待分割的字符串
        :param wordDict: List[str] 单词字典列表
        :return: List[str] 返回所有有效的单词分割方案,每个方案是用空格分隔的字符串
        """
        word_set = set(wordDict)  # 转换为集合提高查找效率
        max_len = max(len(word) for word in wordDict) if wordDict else 0  # 字典中最长单词长度
        n = len(s)  # 字符串长度
        res = []  # 存储所有有效的分割方案
        track = []  # 记录当前递归路径(当前分割的单词序列)

        def backtrack(idx: int) -> None:
            """
            回溯递归核心函数

            通过递归尝试所有可能的分割方式,使用最大单词长度剪枝优化搜索过程。

            :param idx: int 当前处理的字符串起始索引
            :return: None
            """
            # 终止条件:已处理完整个字符串
            if idx == n:
                res.append(" ".join(track))  # 将当前单词序列用空格连接成字符串
                return

            # 尝试所有可能的单词长度(从1到最大单词长度)
            for word_len in range(1, max_len + 1):
                # 剪枝条件1:确保不越界
                if idx + word_len > n:
                    break	# 由于单词长度递增,后续长度只会更大,直接终止循环

                # 剪枝条件2:当前子串必须在字典中
                word = s[idx:idx + word_len]
                if word in word_set:
                    track.append(word)  # 做出选择,将当前单词加入序列
                    backtrack(idx + word_len)  # 递归处理剩余字符串
                    track.pop()  # 撤销选择,回溯

        backtrack(0)  # 从字符串起始位置开始回溯
        return res

17. 电话号码的字母组合

在这里插入图片描述

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        # 边界条件处理:空输入直接返回空列表
        if not digits:
            return []

        # 数字到字母的映射字典(九宫格键盘布局)
        digit_to_letters: Dict[str, str] = {
            "2": "abc",
            "3": "def",
            "4": "ghi",
            "5": "jkl",
            "6": "mno",
            "7": "pqrs",
            "8": "tuv",
            "9": "wxyz"
        }

        n = len(digits)  # 输入数字字符串的长度
        res = []  # 存储所有字母组合结果
        track = []  # 记录当前递归路径(当前正在构建的字母组合)

        def backtrack(idx: int) -> None:
            """
            回溯递归核心函数

            通过递归生成所有可能的字母组合,每个数字对应多个字母选择。

            :param idx: int 当前处理的数字索引
            :return: None
            """
            # 终止条件:已处理完所有数字
            if idx == n:
                res.append("".join(track))  # 将当前字母组合转为字符串
                return

            current_digit = digits[idx]  # 当前处理的数字
            # 遍历当前数字对应的所有字母
            for letter in digit_to_letters[current_digit]:
                track.append(letter)  # 做出选择,添加当前字母
                backtrack(idx + 1)  # 递归处理下一个数字
                track.pop()  # 撤销选择,回溯

        backtrack(0)  # 从第一个数字开始回溯
        return res

437. 路径总和 III

在这里插入图片描述
这是一道综合性很强的题,需要我们在二叉树的结构上使用前缀和枚举右维护左以及回溯等技巧。

  • 首先是要通过二叉树的前序遍历来获取每个节点的值;
  • 然后利用枚举右维护左的思想,来维护一个前缀和字典,记录从根节点到当前节点这一路径上出现的路径和;
  • 最后是运用回溯的思想,在我们遍历完当前节点的左右子树后,退出当前节点之前,撤销刚进入时向前缀和字典添加的记录。
class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
        """
        统计二叉树中和等于目标值的路径数量(前缀和算法实现)

        该方法使用深度优先搜索结合前缀和技巧,高效统计二叉树中路径和等于目标值的所有路径数量。
        通过维护前缀和字典,可以在O(n)时间内解决问题。

        :param root: Optional[TreeNode] 二叉树的根节点
        :param targetSum: int 需要匹配的目标和值
        :return: int 返回满足条件的路径数量
        """
        prefix_sum_count = defaultdict(int)  # 前缀和计数器
        prefix_sum_count[0] = 1  # 初始化前缀和为0的计数为1
        result = 0  # 存储满足条件的路径总数

        def dfs(node: Optional[TreeNode], current_sum: int) -> None:
            """
            深度优先搜索递归函数

            遍历二叉树的同时维护前缀和计数,统计满足条件的路径数量。

            :param node: Optional[TreeNode] 当前访问的树节点
            :param current_sum: int 从根节点到当前节点的路径和
            :return: None
            """
            nonlocal result
            if node is None:
                return

            # 计算当前路径和
            current_sum += node.val

            # 查找满足 current_sum - targetSum 的前缀和数量
            result += prefix_sum_count[current_sum - targetSum]

            # 做选择:更新当前前缀和的计数
            prefix_sum_count[current_sum] += 1

            # 递归遍历左右子树
            dfs(node.left, current_sum)
            dfs(node.right, current_sum)

            # 撤销选择:回溯时恢复前缀和计数(重要)
            prefix_sum_count[current_sum] -= 1

        dfs(root, 0)  # 从根节点开始遍历,初始前缀和为0
        return result

131. 分割回文串

在这里插入图片描述

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        n = len(s)  # 字符串长度
        res = []  # 存储所有回文分割方案
        track = []  # 记录当前递归路径(当前分割的回文子串列表)

        def backtrack(start: int) -> None:
            """
            回溯递归核心函数

            从指定位置开始寻找所有可能的回文子串分割方案。

            :param start: int 当前处理的起始索引
            :return: None
            """
            # 终止条件:已处理完整个字符串
            if start == n:
                res.append(track.copy())  # 保存当前分割方案
                return

            # 尝试所有可能的结束位置
            for end in range(start, n):
                # 检查当前子串是否为回文
                substring = s[start:end + 1]
                if substring == substring[::-1]:  # 回文判断
                    track.append(substring)  # 做出选择,添加回文子串
                    backtrack(end + 1)  # 递归处理剩余字符串
                    track.pop()  # 撤销选择,回溯

        backtrack(0)  # 从字符串起始位置开始回溯
        return res
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值