【动态规划】用01背包分享算法思考

目录

文章介绍

1)阅读前言

2)主要内容

提升对动态规划的算法理解.

本题思路与方案?   

本题其他解法的考虑

提升动态规划理解

1)工作原理和思想  

2)算法特征和使用条件

最优子结构:

重叠子问题:

无后效性:

3)dp[]数组存在的必要和作用

4)递推公式的推导技巧

尝试并数学归纳

直击问题本质联想已知模型

解决问题的6步骤

01背包问题原题

1)题目原题

2)懒人快读:        

3)变量约定

理论应用解决01背包

1)将问题进行抽象,提取出实体对象,确定实体之间的关系

2)初步确认是否可以使用动态规划解决

3)数学归纳或者模型匹配,从规律中挖掘逻辑关系

4)定义dp[]数组与下标的含义

5)初始化dp[]数组

6)确定遍历顺序

7)代码实现与纠错

其他解法的考虑

为什么贪心算法不行?

为什么暴力回溯不好?

本题原方案的优化

1)二维数组方案可改进的地方


文章介绍

1)阅读前言

作者与读者交流的方式从来都是缺少信任的,一句话让你有认真阅读本文章的理由...

       互联网从不缺少解决问题的方法论,而如何建立正确有效的认识论却是鲜少.      

文章内容将帮助你构建对动态规划更底层的认识,要认识到一时的量变是不足以引起质变的,请看以下主要内容部分,是否有你寻找的答案 若能帮助你,还请给个肯定的赞~ 

本文的红色字体是最高级别的提醒色,请重视红色字体...

2)主要内容

提升对动态规划的算法理解.
  1. 算法的工作原理和思想
  2. 算法的特征以及使用条件
  3. dp[]数组的作用
  4. 递推公式的推导技巧
本题思路与方案  
  1. 将问题进行抽象,提取出实体对象,确定实体之间的关系
  2. 初步确认是否可以使用动态规划解决
  3. 数学归纳或者模型匹配,从规律中挖掘逻辑关系
  4. 定义dp[]数组与下标的含义
  5. dp[]数组初始化
  6. 确定遍历顺序
  7. 代码实现与纠错
本题其他解法的考虑
  • 本题为什么动态规划能行?
  • 本题为什么贪心算法不行?
  • 本题为什么暴力回溯不好?

 文章的编排也是按照上述问题的顺序,至于为什么不立刻开始01背包问题的解答,我的意图是想让你带着新的眼光和思想重新思考旧的问题...

一篇文章若能解决需求性问题这是作用,若能带来些许思考问题的启发这是更高的价值和意义


提升动态规划理解

1)工作原理和思想  

        ( 问题特征+问题前提+采用思想+核心措施+最终效果 )

       对于具有最优子结构重叠子问题特征的问题  ,  在确保无后效性的前提下  ,  采用将问题变成同性质的更小规模的思想  ,  根据递归公式通过递归分解问题并存储中间结果  ,  以更低的时间复杂度高效地构建出全局最优解。

        蓝色字体是关键概念,在下方有解释,如果你对上述总结没有清晰的概念,请一定要明确...         这三个特征条件将会是考虑问题是否能用动态规划解决的关键,并在后面会将理论代入到01背包问题中,从真实存在的实体中感受抽象特征.

  • 最优子结构: 问题的最优解可以由其子问题的最优解组合而成。即上一个局部最优解,能推导出下一层局部最优,直到得到全局最优解.
  • 重叠子问题:在求解过程中,相同的子问题会被多次求解。即可能需要获取多次子问题的解,作为当前选择的参考因素.
  • 无后效性:一旦子问题的状态确定,它不会受到后续决策的影响。即后状态不会影响前状态的结果 .        

2)算法特征和使用条件

如果没有科学的认识论也能解决当下的问题,说明当下的问题还不足以让你认真对待,而非无用

使用动态规划一定需要满足上述的三个特征:最优子结构,重叠子问题,无后效性.在实际编程中,有两种情况下你才会用到这三个特征作为你的尺子,即初看题目时思考是否满足使用条件已得出递推公式需要检验公式是否正确.因此在后面的论述中,会分别从两个角度介绍每一个特征.为了更好的理解概念,将结合"爬楼梯"作为分析对象,现给出递推公式:dp[i]=dp[i-1]+dp[i-2];由于下方的论述内容阅读门槛对初次接触动态规划的朋友们不友好,当你进入感悟阶段会发觉价值所在,收藏不迷路~

(我用更贴近正常人的思维活动的思路来写这一部分的解释,多次的琢磨修改,请认真阅读,关乎到你是否能够正确判断分辨出,你的任务是否符合使用动态规划的条件,是否能使用动态规划求解问题)

最优子结构
        初看题目时

        能不能通过局部最优得到全局最优.(详细的说是,能不能将最终问题或者等价的最终问题,能不能被你合理的 划分成更小规模的 同性质的子问题.如果能则满足此特征).

        得出递推公式时

        检查公式是否能实现 非初始阶段的每一个状态 都可以计算出最优解,若能计算出每个子问题最优解,才能构建出全局最优解,如爬楼梯的当前状态是可以通过前两个状态的最优解的和构建出当前状态的最优解,因此爬楼梯问题是满足最优子结构的,递推公式也可能是正确的,我们接着往下探讨...


重叠子问题
        初看题目时

        从反面角度说,如果在求解 非初始阶段的任意一个当前状态的最优解时,如果不需要利用前面的字问题最优解,就能得出结果的话,说明不具有重叠子问题特性.那么你的问题不需要使用动态规划,也许是贪心算法.

        得出递推公式时

        检查是否会将子问题的最优解纳入公式中作为参数,自行检验是否具有重叠子问题结构.(具体解释是:在第i个选择最优解的求解过程中,可能会出现多次利用前i-1个选择的最优解 作为本次选择的递推公式参数,从而构建出第i次选择的最优解.)


无后效性
        初看题目时

        动态规划的每一个状态的最优解一旦确定就不会发生改变,每一个状态内部细节中,唯一确定一个结果;每一个状态后续的更大状态求解中,不会反过来影响到前面的结果.后续子问题在考虑最优解时直接使用前面确定的状态的最优解即可,无需重新计算,因为结果只有一个,怎么算都是同一个,内部环境外部环境如何再变,最终得到的结果都是同一个.(举个反例,假设"爬楼梯"可以在每一个状态下,可以选择上楼梯或下楼梯,每次移动可以一格或两格,那么此时,每一个状态都不是唯一确定的了,违反 特性的规则)

        得出递推公式时

        直接从公式中检验,如dp[i]=dp[i-1]+dp[i-2];第i个状态的最优解计算过程,不会影响任何之前状态的最优解的值,即后续子问题不会影响前面子问题的最优解值;  对于当前状态的最优解值,是只有一个唯一确定的值,因为当前状态的内部结构细节是静态的,不会发生改变,是唯一确定的,因此任何一次计算都是得出同一个结果.

(读到这,如果还是不懂,作者的建议是,可以在解题中明确和强化此概念,也可以去贪心算法章节看题目为什么不能用动态规划,从正面和反面强化和检验你理解的概念,欢迎留言私信帮助,点赞收藏下~)

3)dp[]数组存在的必要和作用

论述一件事情的重要性,可以从正面肯定其价值,也可以从反面假设来凸显其存在的必要性

   正面肯定价值 :   可以存储子问题的解,实现动态规划记忆化计算和搜索,避免重叠问题的计算.同时为动态规划其中两个思想的实现提供了结构支撑.

   反面假设凸显 :   假设dp[]数组不存在,那么之前讲的思想将无从实现,任何一次状态的计算都会重复的递归的计算子问题的结果,动态规划性能会降级为原始的递归性能,因此没有dp[]数组存储每一个状态的最优解结果,这不能算是动态规划算法.而也许是贪心算法,原始的递归,原始的迭代.

4)递推公式的推导技巧

这是掌握动态规划算法的核心之一,这是我自己的感悟,是根本不可能通过这么小的内容体量向你讲清楚,因此,我将在后续单独出一篇文章,专门分享如何构建递归公式,也如此篇文章更侧重对认识论的教学.简单的问题能够通过简单的尝试+数学归纳就能得出递推公式;复杂的问题可以尝试理解分析问题的本质是什么,与你见过的经典模型匹配,具体问题具体分析.

尝试并数学归纳

方法描述: 观察题目变量的变化,挖掘前后最优解可能存在的数学关系公式,注意区分初始值阶段与正常递推阶段,所隐藏的关系,不要混淆两者内部的规律.最直观的方式是自己画图

"爬楼梯"原题: 假设有一个楼梯,每次可以爬1步或2步,求爬到第n阶楼梯的方法数。

  • 基本情况
  1. 爬到第1阶 有1种方法(直接踏上第1阶),
  2. 爬到第2阶 有2种方法(直接踏上第2阶   或 从第1阶前进1步)。
  3. 爬到第3阶 有3种方法(从第1阶前进2步 或 从第2阶前进1步)。
  4. 爬到第4阶 有5种方法(从第2阶前进2步 或 从第3阶前进1步)。
  • 观察模式:将规律分为初始阶段与正常阶段,明确条件:可以选择往上前进1/2步.因此得出第3阶走法=第2阶走法+第1阶走法   第4阶走法=第3阶走法+第2阶走法 ...可以开始归纳为递推公式了
  • 递推公式dp[n] = dp[n - 1] + dp[n - 2],其中dp[n]表示爬到第n阶的方法数。

通过数学归纳法,我们可以证明这个递推公式对于所有n > 2都成立。

直击问题本质联想已知模型

技巧描述: 这种方法要求我们深入理解问题的本质,识别问题的关键特征,然后将其与已知的动态规划模型进行匹配。

"分割等和子集"原题:给定一个整数数组和一个目标和,判断是否可以将数组划分为两个子集,使得这两个子集的和相等。

  • 问题本质:问题可以转化为寻找一个子集A,子集A的和等于目标和的一半,因为两个子集的和等于整个数组的和。
  • 联想模型目标子集 <=> 背包 = 容器    可选数组元素 <=> 可选物品 = 可选对象
  • 递推公式:定义dp[m]为等于目标和一半的子集的可能性,dp[i][j]表示考虑数组中前i个数,且和为j时的划分方法数。递推公式为:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]],其中nums是输入数组。

通过分析问题的本质,我们可以将问题转化为一个已知的动态规划模型,并推导出相应的递推公式。使用这个方法的前提是你对经典问题理解的足够深刻

如果你是按顺序看到这里的,对动态规划产生了自己感悟的朋友,多少会感受到你和我的感悟在碰撞之间重整;对于初学者小白,最好的选择是当你开始产生感悟的时候回来看我的话;接下来就开始真正的对01背包问题进行分析,给个鼓励的赞,更新更多思想干货~

解决问题的6步骤

总结大牛的经验,结合自己的思考,我觉得高效完整的流程在下方给出,后续以01背包问题作为对象,向你展示我是如何用这样的步骤分析问题和解决问题的:

  1. 将问题进行抽象,提取出实体对象,确定实体之间的关系
  2. 初步确认是否可以使用动态规划解决
  3. 数学归纳或者模型匹配,从规律中挖掘逻辑关系
  4. 定义dp[]数组与下标的含义,确定递推公式
  5. 确定遍历顺序
  6. 代码实现与纠错

01背包问题原题

1)题目原题

2)懒人快读:        

(已有条件)    -    有一个容量为V的背包,N件不同重量和价值的物品。                        

(进行操作   -    放入第i件物品耗费的空间是vi,得到的价值是wi

(提出问题   -    求解将哪些物品装入背包可使价值总和最大?        

 先回顾01背包问题,我尝试使用数据库分析需求建立模型的方式,对问题做简单的抽象.                   提取出两个实体一个关系 : 物品(重量,价值)   背包(容量)   物品-背包状态(i物品是否被背包选择)    

3)变量约定

便于后续文章解决,对变量的描述做出统一约定:

  • N:物品的总数
  • W:背包的容量
  • vi:第i件物品的体积
  • wi:第i件物品的价值


理论应用解决01背包

1)将问题进行抽象,提取出实体对象,确定实体之间的关系

理清题中变量实体的属性,与实体之间的关系,画出下方结构图:

2)初步确认是否可以使用动态规划解决

        明确总问题是当背包容量为V时,背包能装的物品最大总价值是多少?一个问题是否能使用动态规划解决,在文章上方有"算法特征和使用条件"详细说明,关键是看问题是否符合三特征:最优子结构,重叠子问题,无后效性.

最优子结构

        (思路:先存在合法子问题,再看是否能通过局部最优得到全局最优)      

        原问题是当背包容量为V时,背包能装的物品最大总价值是多少?现在将问题缩小规模检查其合法性,==>当背包容量为V-1,V-2,V-3,...,2,1时,背包能装的物品最大总价值是多少?问题依然成立.则存在合法子问题;

        总问题能不能由子问题得到呢?答案是可以的,因为当好做好前i-1次选择,每次都做到最好,那么我第i次选择将建立在前i-1次选择的结果上,我第i次也能做到最好,则能够得到通过局部最优得到全局最优.

重叠子问题

        (思路:先存在合法子问题,再看求解第i个子问题是否会利用前i-1个子问题的解)

        原问题是当背包容量为V时,背包能装的物品最大总价值是多少?现在将问题缩小规模检查其合法性,==>当背包容量为V-1,V-2,V-3,...,2,1时,背包能装的物品最大总价值是多少?问题依然成立.则存在合法子问题;        

        求解第i个问题时,一定要利用上前i-1个问题的解,才能算是满足重叠子问题.

无后效性

        (思路:每个问题的解只能是有唯一确定的值,前面状态的改变不会影响到后面的问题)

        假设i<j,第i个问题的最优解一旦确定,就不会再有二义性;而如果第i个问题的解是变化不确定的,由于值会变化,因此第j个问题的解受到第i个问题的影响,所以也是不确定的,违反了无后效性的规则.回到背包问题,一旦确地第i次选择,那么第j次选择不会受到第i次选择的影响.

        建议:如你所见,实际编程过程中不需要如此大费周章的论述,只要想不到可能存在违反规则的行为,那就先初步判定该题能用动态规划.

3)数学归纳或者模型匹配,从规律中挖掘逻辑关系

        假设从未见过01背包问题,那么该如何全部重新开始思考呢,本问题用"从规律中挖掘逻辑关系",对于每个物品i,我们有两种选择:放入背包不放入背包

  • 如果选择不放入背包,那么问题就变成了考虑前i−1个物品,容量为j时,如何得到最优解.
  • 如果选择放入背包,那么问题就变成了考虑前i−1个物品,容量为j−wi​作为前提条件,同时加上这个物品的价值

4)定义dp[]数组与下标的含义

思考方向:

设计合适的dp[]数组和下标的定义,一般有两个方向,一个是从总问题问题导向性思考需要做什么,第二个是从逻辑关系入手,为实现局部最优推出全局最优的目标努力.当然这两个方向思考都是对的,一般来说都能定义出正确的有效的dp[]数组.

设计一维/二维数组?

由于物品(重量,价值)有两个属性,且背包能装最大的物品价值由每一个被选择转入的物品决定,即:多个被选择的物品的价值=背包容量的最大价值,所以我们在每个阶段的任务就是:

不超出限定容量情况下,对已有物品进行选择,使得当前状态价值最大.其中包含了容量,物品是否选择,最大价值.

一维数组是无法承载每一个变量所代表的意义,因此初步我们使用二维数组,(至于为什么能优化成一维数组,这个后面谈)

确定下标的含义:

动态规模的dp[][]数组的作用是记录每个子问题状态下的最优解,那你得先实现dp[][]数组能够代表任意一个子问题的装填呀,因此这是我们思考的方向.

总问题(全部物品可选,全部背包容量)如何全局最优 

子问题(部分物品可选,部分背包容量)如何局部最优

不难发现,不管是总问题还是子问题,每一个问题的状态都需要用可选物品范围,背包容量的多少才能表示完整,因此dp[][]数组的下标含义就分配给这两者.理论上,下标的分配顺序会影响到子问题的遍历顺序,两者之间要对应好才不会出现错误,遍历顺序的问题我们稍后讲,按照经典做法我们将dp[i][j]定义为i为可选物品范围,j为背包容量,dp[i][j]值为最优选择的价值.

从上述的分析能应该能感受到,dp[]数组的定义都是有理有据的,只有明确了产生的原理,遇到创新难题时才能迅速的抓住问题的本质,并定义出正确的dp[]数组.

到目前为止,逻辑关系已经得出,dp[]数组已经定义好,那最简单的一步就是,用dp[]数组代替逻辑关系,即:dp[i][j]=max(dp[i-1][j],dp[i-1][j-wi]+vi)即在选择装入与不选择装入两种结果种取最大价值的一方.

5)初始化dp[]数组

        x方向为背包容量对应j,y方向为可选物品范围对应i.初始化的根本任务是手动解决递推公式不能处理的数据或状态, 因此如下方所示,当背包容量为0时,什么都装不了,因此第一列初始为0;当可选范围没有物品时,什么都选不了,因此第一行初始为0;剩下的部分递推公式已经可以完成了,致辞初始化完成,而不是无盲目的全部初始化为0和1.

        如果需要进行初始化的地方值都为0,可不用另外赋值为0,如Java在创建Int类型数组时默认帮你初始化为0,如C++可以调用构造方法统一初始化为0;

横j01020304050
(0,0)000000
(10,60)0
(20,100)0
(30,120)0
(50,240)0

6)确定遍历顺序

        (正面推理肯定+反面证明不可或缺)

        遍历顺序实际上是需要认真推理的,也许只能顺序或逆序遍历,也许是顺序逆序都行,具体方案还得结合你的递推公式决定,如01背包递推公式dp[i][j]=max(dp[i-1][j],dp[i-1][j-wi]+vi)

        如果采用顺序遍历,本质上是从下到上(即从较小的物品编号到较大的物品编号)和从左到右(即从较小的容量到较大的容量)进行状态转移的,因此采用顺序遍历;而且不管是先遍历i还是j都是成功的,因为两种方式都能保证,每个dp[i][j]计算的时候,需要使用的dp[i−1][j] 和 dp[i−1][j−wi​]都已经实现计算好并存储在dp[][]中.

        如果采用逆序遍历,当我们计算dp[i][j] 时,需要使用 dp[i−1][j] 和 dp[i−1][j−wi​] 的值。但因为我们是逆序遍历,可能还没有计算出这些状态的值,这会导致无法正确进行状态转移,因此逆序遍历对于本题是不可行的.

        综上所述,得出结论:对于01背包问题,只能顺序遍历,且先遍历i还是j都是可以的.而逆序遍历由于所需要的值还未计算出来,所以无法确定当前的值,因此是不可行的.

7)代码实现与纠错

dp[]数组具体的值已经在下方代码运行结果图中给出,可以根据递推公式,尝试自己手推,看自己的理论基础是否足以解决这道题目.

public class Knapsack01 {

    public static int knapsack01(int W, int[] weights, int[] values, int n) {
        //创建int数组的同时已经完成初始化为0
        int[][] dp = new int[n + 1][W + 1];

        //初始化数据(实际上在上面创建dp[][]数组时已经默认初始为0)
        for(int i=0;i<n;i++){
            dp[0][i]=0;
            dp[i][0]=0;
        }

        //使用递归公式计算
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= W; j++) {
                if (weights[i - 1] <= j) {    //在选和不选之间比较哪个价值收益最大
                    dp[i][j] = Math.max(dp[i - 1][j], values[i - 1] + dp[i - 1][j -weights[i - 1]]);
                }
                else {    //当前物品太大,超出最大容量,不能放入
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        return dp[n][W];
    }

    public static void main(String[] args) {
        int n = 4; // 物品数量
        int W = 5; // 背包容量
        int[] values = {6, 10, 12, 20}; // 物品价值
        int[] weights = {1, 2, 3, 5}; // 物品重量

        System.out.println("最大价值为 = " + knapsack01(W, weights, values, n));
    }
}


其他解法的考虑

为什么贪心算法不行?

  1. 局部最优不等于全局最优:贪心算法在每一步都做出在当前状态下看起来最优的选择,即选择单位重量价值最高的物品。然而,这种局部最优选择并不保证能导致全局最优解。因为在后续步骤中,贪心选择可能会阻止我们选择其他物品的组合,这些组合可能会带来更大的总价值。

  2. 没有回溯:贪心算法没有回溯过程,它只根据当前情况做出决定,而不考虑之前的状态。在01背包问题中,我们需要考虑所有可能的物品组合,这通常需要回溯到之前的状态来评估不同选择的影响。

  3. 不能处理冲突:在01背包问题中,有时候需要在多个价值较低但总重量较小的物品和少数价值较高但重量较大的物品之间做出选择。贪心算法可能会选择后者,因为它的单位价值更高,但这可能会导致背包空间的浪费,从而无法装入更多的物品。

为什么暴力回溯不好?

  1. 效率问题:暴力回溯算法会尝试所有可能的物品组合,这是一个指数时间复杂度的解法。对于规模较大的问题,即物品数量较多或背包容量较大时,这种方法会非常慢,甚至不可行。

  2. 重复计算:在回溯过程中,很多子问题会被重复计算多次。例如,考虑前i个物品和考虑前i-1个物品的很多状态可能会重复出现,但暴力回溯算法并没有利用这些重复状态来减少计算量。

  3. 空间复杂度:虽然回溯算法的空间复杂度相对较低,因为它只需要存储到当前深度的状态,但在大规模问题中,递归调用栈可能会变得非常大,导致空间效率不高。

public class KnapsackBruteForce {

    // 用于记录最大价值
    static int maxVal = 0;

    // 回溯函数
    public static void knapsackBruteForce(int[] weights, int[] values, int W, int idx, int currentWeight, int currentValue) {
        // 已经尝试了所有物品,检查当前价值是否是最大的
        if (idx == weights.length) {
            if (currentValue > maxVal && currentWeight <= W) {
                maxVal = currentValue;
            }
            return;
        }

        // 选择物品idx
        knapsackBruteForce(weights, values, W, idx + 1, currentWeight + weights[idx], currentValue + values[idx]);

        // 不选择物品idx
        knapsackBruteForce(weights, values, W, idx + 1, currentWeight, currentValue);
    }

    // 主函数
    public static int knapsack(int W, int[] weights, int[] values, int n) {
        maxVal = 0;
        knapsackBruteForce(weights, values, W, 0, 0, 0);
        return maxVal;
    }

    public static void main(String[] args) {
        int W = 50; // 背包容量
        int[] weights = {10, 20, 30, 50}; // 物品重量
        int[] values = {60, 100, 120,200}; // 物品价值
        int n = weights.length;

        System.out.println("最大价值为 = " + knapsack(W, weights, values, n));
    }
}

本题原方案的优化

先回顾二维数组方案的递推公式:dp[i][j]=max(dp[i-1][j],dp[i-1][j-wi]+vi)

1)二维数组方案可改进的地方

        观察二维数组方案递推公式,dp[i][j]的计算依赖于i-1行的结果,不管是哪个状态都是如此,也就是说,第i行的问题只需要参考第i-1行的结果,看下方图所示你就懂什么回事了.

066666

但是实际上不需要两行数组,我们只需要在求解第i行问题时,将第i-1行结果复制给第i行,直接利用,覆盖更新dp[i-1]的值即可, 如此我们只需要一个一维数组即可完整的实现二维数组的效果,这就是一维滚动数组,但是呢方案已经改变,那么我们就需要思考原本的方案是否还使用新的方案.

具体实现与代码,我认为已经有很棒的博主介绍过,这是他的文章:热爱编程的林兮,在他那里相信你会有收获的,同样的事情我就不展开做了.


        至此,本文完美结束!本文更多的是我在学习过程中得到的启发与感悟,更重要的是思想认识论的部分,而不是仅仅完成一道01背包,01背包在动态规划中之所以经典,不是因为难,而是变种多,解决问题的思想很好的体现的动态规划的思想.

        我在构思文章段落之间的安排,内容出现的顺序,语言组织的逻辑,抽象概念的论述,思考问题的思想时,对我来说也是一种锻炼,但是很遗憾这篇文章的阅读门槛对于初学者是不友好的,等你在实际编程中出现一闪而过的感悟时,是看这篇文章最好的时候...

后续继续分享对算法的思考,教你更多认识层面的思想.如果你认可我的文章,给个鼓励的赞吧

点个关注持续更新哦~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值