代码随想录算法day31 | 动态规划算法part04 | 1049. 最后一块石头的重量 II,494. 目标和,474.一和零

1049.最后一块石头的重量II

本题就和 416. 分割等和子集 很像

力扣题目链接(opens new window)

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;

如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。

最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。

示例:

  • 输入:[2,7,4,1,8,1]
  • 输出:1

解释:

  • 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
  • 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
  • 组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
  • 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

提示:

  • 1 <= stones.length <= 30
  • 1 <= stones[i] <= 1000

如果对背包问题不都熟悉先看一下这一篇博文:背包问题​​​​

本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了

是不是感觉和上篇博文讲解的 416.分割等和子集 非常像了。

本题物品的重量为 stones[i],物品的价值也为 stones[i]。对应着01背包里的物品重量 weight[i] 和 物品价值 value[i]。

接下来进行动规五步曲:

  • 确定dp数组以及下标的含义

dp[j] 表示容量(这里说容量更形象,其实就是重量)为 j 的背包,最多可以背最大重量为 dp[j]

可以回忆一下01背包中,dp[j] 的含义,容量为 j 的背包,最多可以装的价值为 dp[j]。

相对于 01背包,本题中,石头的重量是 stones[i],石头的价值也是 stones[i] ,可以 “最多可以装的价值为 dp[j]” == “最多可以背的重量为 dp[j]”

  • 确定递推公式

01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

本题则是:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);

一些同学可能看到这dp[j - stones[i]] + stones[i]中 又有- stones[i] 又有+stones[i],看着有点晕乎。

大家可以再去看 dp[j]的含义。

  • dp数组如何初始化

既然 dp[j] 中的 j 表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。

因为提示中给出1 <= stones.length <= 30,1 <= stones[i] <= 1000,所以最大重量就是30 * 1000 。

而我们要求的target其实只是最大重量的一半,所以dp数组开到15000大小就可以了。

当然也可以把石头遍历一遍,计算出石头总重量然后除2,得到dp数组的大小。

我这里就直接用15000了。

接下来就是如何初始化dp[j]呢,因为重量都不会是负数,所以dp[j]都初始化为0就可以了,这样在递归公式dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);中dp[j]才不会初始值所覆盖。

代码为:

int[] dp = new int[1500];
Arrays.fill(dp, 0);
  • 确定遍历顺序

如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

讲解博文:代码随想录算法day30 | 动态规划算法part03 | 01背包问题 二维 ,01背包问题 一维,416. 分割等和子集-CSDN博客

代码如下: 

for (int i = 0; i < stones.length; i++) { // 遍历物品
    for (int j = target; j >= stones[i]; j--) { // 遍历背包
        dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
    }
}

  • 举例推导dp数组

举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下:

1049.最后一块石头的重量II

最后 dp[target] 里是容量为target的背包所能背的最大重量。

那么分成两堆石头,一堆石头的总重量是 dp[target],另一堆就是 sum - dp[target]。

在计算 target 的时候,target = sum / 2 因为是向下取整,所以 sum - dp[target] 一定是大于等于dp[target]的

那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。

以上分析完毕,Java 代码如下:

class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for (int i : stones) {
            sum += i;
        }
        int target = sum >> 1;
        //初始化dp数组
        int[] dp = new int[target + 1];
        for (int i = 0; i < stones.length; i++) {
            //采用倒序
            for (int j = target; j >= stones[i]; j--) {
                //两种情况,要么放,要么不放
                dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - 2 * dp[target];
    }
}
  • 时间复杂度:O(m × n) , m是石头总重量(准确的说是总重量的一半),n为石头块数
  • 空间复杂度:O(m)

 如果上述一维数组不好理解,可以先看看下面的二维数组代码,更为形象:

class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for (int s : stones) {
            sum += s;
        }

        int target = sum / 2;
        //初始化,dp[i][j]为可以放0-i物品,背包容量为j的情况下背包中的最大价值
        int[][] dp = new int[stones.length][target + 1];
        //dp[i][0]默认初始化为0
        //dp[0][j]取决于stones[0]
        for (int j = stones[0]; j <= target; j++) {
            dp[0][j] = stones[0];
        }

        for (int i = 1; i < stones.length; i++) {
            for (int j = 1; j <= target; j++) {//注意是等于
                if (j >= stones[i]) {
                    //不放:dp[i - 1][j] 放:dp[i - 1][j - stones[i]] + stones[i]
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - stones[i]] + stones[i]);
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        System.out.println(dp[stones.length - 1][target]);
        return (sum - dp[stones.length - 1][target]) - dp[stones.length - 1][target];
    }
}

总结

本题其实和 416.分割等和子集 几乎是一样的,只是最后对dp[target]的处理方式不同。在 分割子集 里面相当于是求背包是否正好装满,而本题是求背包最多能装多少。


494.目标和

01背包应用之“有多少种不同的填满背包最大容量的方法“

大家重点理解 递推公式:dp[j] += dp[j - nums[i]]

力扣题目链接(opens new window)

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例:

  • 输入:nums: [1, 1, 1, 1, 1], S: 3
  • 输出:5

解释:

  • -1+1+1+1+1 = 3
  • +1-1+1+1+1 = 3
  • +1+1-1+1+1 = 3
  • +1+1+1-1+1 = 3
  • +1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

提示:

  • 数组非空,且长度不会超过 20 。
  • 初始的数组的和不会超过 1000 。
  • 保证返回的最终结果能被 32 位整数存下。

看到这道题,应该有一种直觉,就是感觉好像回溯法可以暴搜出来。

事实确实如此,只不过会超时。

这道题目咋眼一看和动态规划背包啥的也没啥关系。

本题要如何使表达式结果为 target,既然为 target,那么就一定有 left组合 - right组合 = target。

left + right = sum,而 sum 是固定的。right = sum - left

公式来了, left - (sum - left) = target 推导出 left = (target + sum)/2

target 是固定的,sum 是固定的,left 就可以求出来。

此时问题就是在集合 nums 中找出和为 left 的组合。

动态规划 (二维dp数组)

假设加法(正数相加)的总和为 x,那么减法(负数相加)对应的总和就是sum - x。

所以我们要求的是 x - (sum - x) = target

x = (target + sum) / 2

此时问题就转化为,装满容量为 x 的背包,有几种方法

这里的 x,就是 bagSize,也就是我们后面要求的背包容量。

大家看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响

这么担心就对了,例如sum是 5,target是 2 的话其实就是无解的,所以:

if ((target + sum) % 2 == 1) return 0; // 此时没有方案

同时如果target 的绝对值已经大于sum,那么也是没有方案的。

if (Math.abs(target) > sum) return 0; // 此时没有方案

因为每个物品(题目中的1)只用一次!

这次和之前遇到的背包问题不一样了,之前都是求容量为 j 的背包,最多能装多少。

本题则是装满有几种方法。其实这就是一个组合问题了。

  • 确定dp数组以及下标的含义

先用 二维dp数组 求解本题,dp[i][j]:使用 下标为[0, i]的 nums[i] 能够凑满 j(包括 j )这么大容量的包,有 dp[i][j] 种方法。

  • 确定递推公式

我们先手动推导一下,这个二维数组里面的数值。

先只考虑物品0,如图:

(这里的所有物品,都是题目中的数字1)。

装满背包容量为0 的方法个数是1,即 放0件物品。

装满背包容量为1 的方法个数是1,即 放物品0。

装满背包容量为2 的方法个数是0,目前没有办法能装满容量为2的背包。

接下来 考虑 物品0 和 物品1,如图:

装满背包容量为0 的方法个数是1,即 放0件物品。

装满背包容量为1 的方法个数是2,即 放物品0 或者 放物品1。

装满背包容量为2 的方法个数是1,即 放物品0 和 放物品1。

其他容量都不能装满,所以方法是0。

接下来 考虑 物品0 、物品1 和 物品2 ,如图:

装满背包容量为0 的方法个数是1,即 放0件物品。

装满背包容量为1 的方法个数是3,即 放物品0 或者 放物品1 或者 放物品2。

装满背包容量为2 的方法个数是3,即 放物品0 和 放物品1、放物品0 和 物品 2、 放物品1 和 物品2。

装满背包容量为3的方法个数是1,即 放物品0 和 物品1 和 物品2。

通过以上举例,我们来看 dp[2][2] 可以有哪些方向推出来。

如图红色部分:

dp[2][2] = 3,即 放物品0 和 放物品1、放物品0 和 物品 2、放物品1 和 物品2, 如图所示,三种方法:

容量为2 的背包,如果不放 物品2 有几种方法呢

有 dp[1][2] 种方法,即 背包容量为2,只考虑物品0 和 物品1 ,有 dp[1][2] 种方法,如图:

容量为2 的背包, 如果放 物品2 有几种方法呢

首先 要在背包里 先把物品2的容量空出来, 装满 刨除物品2容量 的背包 有几种方法呢?

刨除物品2容量后的背包容量为 1。

此时装满背包容量为1 有 dp[1][1] 种方法,即: 不放物品2,背包容量为1,只考虑物品 0 和 物品 1,有 dp[1][1] 种方法。

如图:

有录友可能疑惑,这里计算的是放满 容量为2的背包 有几种方法,那物品2去哪了?

在上面图中,你把物品2补上就好,同样是两种方法。

dp[2][2] = 容量为2的背包不放物品2有几种方法 + 容量为2的背包放物品2有几种方法

所以 dp[2][2] = dp[1][2] + dp[1][1] ,如图:

以上过程,抽象化如下:

  • 不放物品 i:即背包容量为 j,里面不放物品 i,装满有 dp[i - 1][j] 中方法。

  • 放物品 i: 即:先空出物品 i 的容量,背包容量为(j - 物品 i 容量),放满背包有 dp[i - 1][j - 物品 i 容量] 种方法。

本题中,物品i的容量是 nums[i],价值也是 nums[i]。

递推公式:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];

考到这个递推公式,我们应该注意到,j - nums[i] 作为数组下标,如果 j - nums[i] 小于零呢?

说明背包容量装不下 物品 i,所以此时装满背包的方法值 等于 不放物品 i 的装满背包的方法,即:dp[i][j] = dp[i - 1][j];

所以递推公式:

if (nums[i] > j)
    dp[i][j] = dp[i - 1][j]; 
else
    dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
  • dp数组如何初始化

先明确递推的方向,如图,求解 dp[2][2] 是由 上方和左上方推出。

那么二维数组的最上行 和 最左列一定要初始化,这是递推公式推导的基础,如图红色部分:

关于dp[0][0]的值,在上面的递推公式讲解中已经讲过,装满背包容量为0 的方法数量是1,即 放0件物品。

那么最上行dp[0][j] 如何初始化呢?

dp[0][j]:只放物品 0, 把容量为j的背包填满有几种方法。

只有背包容量为 物品0 的容量的时候,方法为1,正好装满。

其他情况下,要不是装不满,要不是装不下

所以初始化:dp[0][nums[0]] = 1 ,其他均为0 。

表格最左列也要初始化,dp[i][0] : 背包容量为0, 放物品0 到 物品 i,装满有几种方法。

都是有一种方法,就是放0件物品。

即 dp[i][0] = 1

  • 确定遍历顺序

在明确递推方向时,我们知道 当前值 是由上方和左上方推出。

那么我们的遍历顺序一定是 从上到下,从左到右。

因为只有这样,我们才能基于之前的数值做推导。

例如下图,如果上方没数值,左上方没数值,就无法推出 dp[2][2]。

那么是先 从上到下 ,再从左到右遍历,例如这样:

for (int i = 1; i < nums.length; i++) { // 行,遍历物品
    for (int j = 0; j <= bagSize; j++) { // 列,遍历背包
    }
}

还是先 从左到右,再从上到下呢,例如这样:

for (int j = 0; j <= bagSize; j++) { // 列,遍历背包
    for (int i = 1; i < nums.length; i++) { // 行,遍历物品
    }
}

其实以上两种遍历都可以! (但仅针对二维DP数组是这样的)

  • 举例推导dp数组

输入:nums: [1, 1, 1, 1, 1], target: 3

bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4

dp数组状态变化如下:

这么大的矩阵,我们是可以自己手动模拟出来的。

在模拟的过程中,既可以帮我们寻找规律,也可以帮我们验证 递推公式加遍历顺序是不是按照我们想象的结果推进的。

最后二维dp数组的Java代码如下:

class Solution {
    public int findTargetSumWays(int[] nums, int target) {

        // 01背包应用之“有多少种不同的填满背包最大容量的方法“
        // 易于理解的二维数组解法及详细注释

        int sum = 0;
        for(int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }

        // 注意nums[i] >= 0的题目条件,意味着sum也是所有nums[i]的绝对值之和
        // 这里保证了sum + target一定是大于等于零的,也就是left大于等于零(毕竟我们定义left大于right)
        if(sum < Math.abs(target)){
            return 0;
        }

        // 利用二元一次方程组将left用target和sum表示出来(替换掉right组合),详见分析
        // 如果所求的left数组和为小数,则作为整数数组的nums里的任何元素自然是没有办法凑出这个小数的
        if((sum + target) % 2 != 0) {
            return 0;
        }

        int left = (sum + target) / 2;
        
        // dp[i][j]:遍历到数组第i个数时, left(背包容量)为j时的能装满背包的方法总数
        int[][] dp = new int[nums.length][left + 1];

        // 初始化最上行(dp[0][j]),当nums[0] == j时(注意nums[0]和j都一定是大于等于零的,因此不需要判断等于-j时的情况),有唯一一种取法可取到j,dp[0][j]此时等于1
        // 其他情况dp[0][j] = 0
        // java整数数组默认初始值为0
        if (nums[0] <= left) {
            dp[0][nums[0]] = 1;
        }

        // 初始化最左列(dp[i][0])
        // 当从nums数组的索引0到i的部分有n个0时(n > 0),每个0可以取+/-,因此有2的n次方中可以取到j = 0的方案
        // n = 0说明当前遍历到的数组部分没有0全为正数,因此只有一种方案可以取到j = 0(就是所有数都不取)
        int numZeros = 0;
        for(int i = 0; i < nums.length; i++) {
            if(nums[i] == 0) {
                numZeros++;
            }
            dp[i][0] = (int) Math.pow(2, numZeros);

        }

        // 递推公式分析:
        // 当nums[i] > j时,这时候nums[i]一定不能取,所以是dp[i - 1][j]种方案数
        // nums[i] <= j时,num[i]可取可不取,因此方案数是dp[i - 1][j] + dp[i - 1][j - nums[i]]
        // 由递推公式可知,先遍历i或j都可
        for(int i = 1; i < nums.length; i++) {
            for(int j = 1; j <= left; j++) {
                if(nums[i] > j) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
                }
            }
        }


        return dp[nums.length - 1][left];
        
    }
}

动态规划 (一维dp数组)

将二维dp数组压缩成一维dp数组,原理是一样的,即重复利用每一行的数值。

既然是重复利用每一行,就是将二维数组压缩成一行。

dp[i][j] 去掉 行 的维度,即 dp[j],表示:填满 j(包括j)这么大容积的包,有 dp[j] 种方法。

  • 确定递推公式

二维DP数组递推公式: dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];

去掉维度i 之后,递推公式:dp[j] = dp[j] + dp[j - nums[i]] ,即:dp[j] += dp[j - nums[i]]

这个公式在后面在讲解背包解决排列组合问题的时候还会用到!

  • dp数组如何初始化

在上面 二维dp数组中,我们讲解过 dp[0][0] 初始为1,这里dp[0] 同样初始为1 ,即装满背包为0的方法有一种,放0件物品。

  • 确定遍历顺序

遍历物品放在外循环,遍历背包在内循环,且内循环倒序(为了保证物品只使用一次)。

  • 举例推导dp数组

输入:nums: [1, 1, 1, 1, 1], target: 3

bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4

dp数组状态变化如下:

大家可以和 二维dp数组的打印结果做一下对比。

一维DP的Java代码如下:

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) sum += nums[i];

        //如果target的绝对值大于sum,那么是没有方案的
        if (Math.abs(target) > sum) return 0;
        //如果(target+sum)除以2的余数不为0,也是没有方案的
        if ((target + sum) % 2 == 1) return 0;

        int bagSize = (target + sum) / 2;
        int[] dp = new int[bagSize + 1];
        dp[0] = 1;

        for (int i = 0; i < nums.length; i++) {
            for (int j = bagSize; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }

        return dp[bagSize];
    }
}
  • 时间复杂度:O(n × m),n为正数个数,m为背包容量
  • 空间复杂度:O(m),m为背包容量

474.一和零

通过这道题目,大家先粗略了解, 01背包,完全背包,多重背包的区别,不过不用细扣,因为后面 对于 完全背包,多重背包 还有单独讲解。

力扣题目链接(opens new window)

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例 1:

  • 输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3

  • 输出:4

  • 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

  • 输入:strs = ["10", "0", "1"], m = 1, n = 1
  • 输出:2
  • 解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

提示:

  • 1 <= strs.length <= 600
  • 1 <= strs[i].length <= 100
  • strs[i] 仅由 '0' 和 '1' 组成
  • 1 <= m, n <= 100

这道题目,还是比较难的,也有点像程序员自己给自己出个脑筋急转弯,程序员何苦为难程序员呢。

来说题,本题不少同学会认为是多重背包,一些题解也是这么写的。

其实本题并不是多重背包,再来看一下这个图,捋清几种背包的关系

416.分割等和子集1

多重背包是每个物品,数量不同的情况。

本题中 strs 数组里的元素就是物品,每个物品都是一个!

而 m 和 n 相当于是一个背包,两个维度的背包

理解成多重背包的同学主要是把 m 和 n 混淆为物品了,感觉这是不同数量的物品,所以以为是多重背包。

但本题其实是01背包问题!

只不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。

开始动规五部曲:

  • 确定dp数组(dp table)以及下标的含义

dp[i][j]:最多有 i 个 0 和 j 个 1 的 strs 的最大子集的大小为 dp[i][j]

  • 确定递推公式

dp[i][j] 可以由前一个 strs 里的字符串推导出来,strs 里的字符串有 zeroNum个0,oneNum个1。

dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。

然后我们在遍历的过程中,取dp[i][j]的最大值。

所以递推公式:dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

此时大家可以回想一下01背包的递推公式:dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);

对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。

这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。

  • dp数组如何初始化

因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。

  • 确定遍历顺序

我们之前讲过一维01背包一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!

那么本题也是,物品就是strs里的字符串,背包容量就是题目描述中的m和n。

代码如下:

for (String str : strs) {
    oneNum = 0;
    zeroNum = 0;
    for (char ch : str.toCharArray()) {
        if (ch == '0') {
            zeroNum++;
        } else {
            oneNum++;
        }
    }
//倒序遍历
for (int i = m; i >= zeroNum; i--) {
    for (int j = n; j >= oneNum; j--) {
        dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
    }
}

有同学可能想,那个遍历背包容量的两层for循环先后循序有没有什么讲究?

没讲究,都是物品重量的一个维度,先遍历哪个都行!

  • 举例推导dp数组

以输入:["10","0001","111001","1","0"],m = 3,n = 3为例

最后dp数组的状态如下所示:

474.一和零

以上动规五部曲分析完毕,Java代码如下:

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        //dp[i][j]表示i个0和j个1时的最大子集
        int[][] dp = new int[m + 1][n + 1];
        int oneNum, zeroNum;
        for (String str : strs) {
            oneNum = 0;
            zeroNum = 0;
            for (char ch : str.toCharArray()) {
                if (ch == '0') {
                    zeroNum++;
                } else {
                    oneNum++;
                }
            }
            //倒序遍历
            for (int i = m; i >= zeroNum; i--) {
                for (int j = n; j >= oneNum; j--) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
}
  • 时间复杂度: O(kmn),k 为strs的长度
  • 空间复杂度: O(mn)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值