第十天:动态规划

1.理论基础知识

        1)动态规划常见题型

                i)动态规划基础题目

                ii)背包问题

                 iii)打家劫舍

                iv)股票问题

                v)子序列问题(编辑距离)

        2)动态的常见误区

                i)递推公式只是其中一部分

                ii)ac完还有很多细致的点不会

        3)dp数组的含义(定义以及下标的含义)

        4)递推公式 (不是掌握了递推公式就可以秒杀dp)

        5)dp数组如何初始化

2.斐波那契数

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。其中 n > 1 给你n ,请计算 F(n) 

1)直接使用递归

class Solution {
public:
    int fib(int n) {
        if(n<=1)    return n;
        else return fib(n-1)+fib(n-2);
    }
};

2)使用动态规划

class Solution {
public:
    int fib(int n) {
        if(n<=1) return n;
        vector<int> dp(n+1);
        dp[0]=0,dp[1]=1;
        for(int i=0;i<=n-2;i++)
        {
            dp[i+2]=dp[i+1]+dp[i];
        }
        return dp[n];
    }
};

3.爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

当你需要爬n阶楼梯时 你只需要考虑是之前是爬n-1 还是n-2 所以和斐波那契数列是一样的

就是斐波那契数列

4.使用最小花费爬楼梯

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

class Solution {
public:
//dp数组的定义 爬到每楼的最小消耗
//dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
    int minCostClimbingStairs(vector<int>& cost) {
        int n=cost.size();
        vector<int> dp(n+1);
        dp[0]=0,dp[1]=0;
        for(int i=2;i<=n;i++)
        {
            dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[n];
    }
};

5.不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?

class Solution {
public:
    int uniquePaths(int m, int n) {
        int dp[m][n];
        for(int i=0;i<m;i++)
            dp[i][0]=1;
        for(int j=0;j<n;j++)
            dp[0][j]=1;
        for(int i=1;i<m;i++)
        {
            for(int j=1;j<n;j++)
            {
                dp[i][j]=dp[i-1][j]+dp[i][j-1];//两种走法
            }
        }
        return dp[m-1][n-1];
    }
};

6.不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

本题需要着重强调初始化 以及遇到障碍时的重要性

class Solution {
public:
//和前面那道题唯一的不同是保证障碍为0
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0
            return 0;
        int dp[m][n];
        //初始化当遇到障碍后都应该变为0
        bool mm=true, nn=true;
        for(int i=0;i<m;i++)
        {
            if(obstacleGrid[i][0]==1)
                mm=false;
            if(mm)
                dp[i][0]=1;
            else
                dp[i][0]=0;
        }
        for(int j=0;j<n;j++)
        {
            if(obstacleGrid[0][j]==1)
                nn=false;
            if(nn)
                dp[0][j]=1;
            else
                dp[0][j]=0;
        }
        for(int i=1;i<m;i++)
        {
            for(int j=1;j<n;j++)
            {
                if(obstacleGrid[i][j]==1)
                {
                    dp[i][j]=0;
                    continue;
                }
                else
                {
                    dp[i][j]=dp[i][j-1]+dp[i-1][j];
                }
            }
        }
        return dp[m-1][n-1];
    }
};

7.整数拆分

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积

这道题主要是确定关系式

class Solution {
public:
//dp数组用于存放每个n的最大数
    int integerBreak(int n) {
        int dp[n+1];
        dp[2]=1;
        for(int i=3;i<=n;i++)
        {
            int temp=0;
            for(int j=1;j<=i/2;j++)//一定记得是<=i/2“=”
            {
                temp=max(temp,max((i-j)*j,dp[i-j]*j));//是两个或者可以被分为多个
            }
            dp[i]=temp;
        }
        return dp[n];
    }
};

8.背包理论基础

1)何为01背包

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

暴力解法:o(2^n) 通过枚举每一个物品是否装
2)以下面这个例子来讲解01背包的两种遍历方式:1)二维数组 2)滚动数组

i)二维数组

        dp数组:dp[i][j]    j是背包容量    i是在0-i物品中选择

        递推公式!:dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]+value[i])

                                dp[i][j]=max(直接不装i个物品的最大值,装第i个物品的最大值)

注:在01背包中反而递推公式是比较简单的 难的是各个处的细节

        初始化(区别于滚动数组):

        

        确定遍历顺序:遍历顺序都可以(无论先遍历背包还是先遍历物品)

                                    因为整体的顺序是往右下角走

        在自己做题目的时候建议自己手画dp一部分数组然后打印检验

#include <iostream>
#include <vector>
#include <random>

using namespace std;
int bag_problem();
int main()
{
    return bag_problem();
}
int bag_problem()
{
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagweight = 4;
    int dp[weight.size()][bagweight + 1]; //
    for (int i = 0; i < weight.size(); i++)
        dp[i][0] = 0;
    for (int i = 0; i < bagweight + 1; i++)
        if (i >= weight[0])
            dp[0][i] = value[0];
    for (int i = 0; i < weight.size(); i++) // 物品
    {
        for (int j = 0; j <= bagweight; j++) // 背包
        {
            dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
        }
    }
    return dp[weight.size() - 1][bagweight];
}

ii)滚动数组

  遍历顺序

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}

初始化 全部初始化为0

void test_1_wei_bag_problem() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;

    // 初始化
    vector<int> dp(bagWeight + 1, 0);
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight] << endl;
}

int main() {
    test_1_wei_bag_problem();
}

9.分割等和子集

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

class Solution {
public:
    bool canPartition(vector<int>& nums) {//背包和物品看为一个
        sort(nums.begin(),nums.end());
        int sum=0;
        for(int i=0;i<nums.size();i++)
            sum+=nums[i];
        if(sum%2!=0)    return false;
        int target = sum/2;
        int dp[20001]={0};    //初始化为0 100*200=20000
        for(int i=0;i<nums.size();i++)//物品
        {
            for(int j=target;j>=nums[i];j--)//背包
            {
                dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
            }
        }
        if(dp[target]==target)   return true;
        return false;
    }
};

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

有一堆石头,每块石头的重量都是正整数。每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:如果 x == y,那么两块石头都会被完全粉碎;如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。

思路:其实就是分割成两堆重量相减

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        //我们令背包容量为总和的一半 可以变为背包问题
        int sum=0;
        for(int i=0;i<stones.size();i++)
        {
            sum+=stones[i];
        }
        int target=sum/2;
        int dp[3100]={0};
        //先物品后背包
        for(int i=0;i<stones.size();i++)
        {
            for(int j=target;j>=stones[i];j--)
            {
                dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
            }
        }
        return sum - dp[target]*2;
    }
};

11.目标和

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

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum=0;
        for(int i=0;i<nums.size();i++)
            sum+=nums[i];
        if((target+sum)%2==1)   return 0;//注意特判
        int bag = (target+sum)/2;
        if(bag>sum||bag<0) return 0;//注意特判
        int dp[40000]={0}; //普通初始化不能丢
        dp[0]=1;//‼️如果=0 以后则全部为0
        //此时的dp数组定义不同
        //dp数组的意思是dp[i] 背包为j的容量可以有dp[j]种组合方式
        for(int i=0;i<nums.size();i++)
        {
            for(int j=bag;j>=nums[i];j--)
                dp[j]+=dp[j-nums[i]];
        }
        return dp[bag];
    }
};

12.一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

多加了一个维度的 普通01背包

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0)); // 默认初始化0
        for (string str : strs) { // 遍历物品
            int oneNum = 0, zeroNum = 0;
            for (char c : str) {
                if (c == '0') zeroNum++;
                else oneNum++;
            }
            for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
                for (int j = n; j >= oneNum; j--) {
                    dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);//max
                }
            }
        }
        return dp[m][n];
    }
};

13.完全背包理论基础

​​​​​​​

void test_CompletePack() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;
    vector<int> dp(bagWeight + 1, 0);
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight] << endl;
}

14.零钱兑换II

        给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        if (coins.empty()) return amount == 0 ? 1 : 0; // 如果没有硬币并且金额为0,则返回1,否则返回0
        
        // 初始化二维dp数组
        vector<vector<int>> dp(coins.size(), vector<int>(amount + 1, 0));
        
        // 只使用第一种硬币的情况,填充第一行
        for (int i = 0; i <= amount; i += coins[0]) {//只有整除的才是1
            dp[0][i] = 1;
        }
        
        // 遍历所有的硬币
        for (int i = 1; i < coins.size(); i++) {
            for (int j = 0; j <= amount; j++) {
                // 第i种硬币使用0~k次,求和
                for (int k = 0; k * coins[i] <= j; k++) {
                    dp[i][j] += dp[i - 1][j - k * coins[i]];
                }
            }
        }
        
        // 返回使用所有硬币组成amount的方式
        return dp[coins.size() - 1][amount];
    }
};
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        if (coins.empty() && amount == 0) return 1; // 特殊情况:没有硬币但金额为0
        vector<vector<int>> dp(coins.size(), vector<int>(amount + 1, 0));
        
        // 初始化第一列为1,因为使用任意数量的硬币组成金额0的方式有1种:不使用任何硬币
        for (int i = 0; i < coins.size(); i++) {
            dp[i][0] = 1;
        }
        
        // 填充DP表
        for (int i = 0; i < coins.size(); i++) {
            for (int j = 1; j <= amount; j++) {
                // 如果当前硬币的面额不大于金额
                if (j - coins[i] >= 0) {
                    dp[i][j] = dp[i][j - coins[i]]; // 使用当前硬币
                }
                if (i > 0) {
                    dp[i][j] += dp[i-1][j]; // 不使用当前硬币
                }
            }
        }
        
        return dp[coins.size() - 1][amount];
    }
};

第二种代码效率更高

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        if (coins.empty()) return amount == 0 ? 1 : 0; // 如果没有硬币且金额为0,返回1;否则返回0
        
        // 使用一维dp数组进行优化
        vector<int> dp(amount + 1, 0);
        dp[0] = 1; // 初始化,任何硬币组合都能组成金额0的方式只有1种:即不使用任何硬币
        
        // 遍历硬币
        for (int i = 0; i < coins.size(); i++) {
            // 更新每个可能的金额
            for (int j = coins[i]; j <= amount; j++) {
                // 使用当前硬币
                dp[j] += dp[j - coins[i]];
            }
        }
        
        return dp[amount];
    }
};

15.组合总和 Ⅳ

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

请注意,顺序不同的序列被视作不同的组合

可以使用无线次:完全背包

顺序不同的序列被视作不同的组合:可以当作是排列

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        //强调顺序时只建议使用一位滚动数组
        int dp[1001]={0};
        //初始化
        dp[0]=1;
        for(int j=1;j<=target;j++)
        {
            for(int i=0;i<nums.size();i++)
            {   
            //C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]
                if(j>=nums[i]&&dp[j] < INT_MAX - dp[j - nums[i]])
                    dp[j]+=dp[j-nums[i]];
            }
        }
        return dp[target];
    }
};

16.爬楼梯(进阶版)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?注意:给定 n 是一个正整数

#include<cstdio>
int main()
{
    int m,n;
    scanf("%d%d",&n,&m);
    int dp[33]={0};
    dp[0]=1;
    for(int j=1;j<=n;j++)
    {
        for(int i=0;i<=m;i++)
        {
            if(j>=i)
                dp[j]+=dp[j-i];
        }
    }
    printf("%d",dp[n]);
    return 0;
}

注:后面的题目都不是简单的背包应用 需要我们注意

17.零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1 你可以认为每种硬币的数量是无限的。

注意 这里都是求min 因此初始化应该是初始化为最大值

class Solution {
public:
    static bool compare(int a,int b)
    {
        return a>b;
    }     
    int coinChange(vector<int>& coins, int amount) {
        sort(coins.begin(),coins.end(),compare);
        if(amount==0)    return 0;
        //使用动态规划
        //dp[i]存最少钱币数
        vector<int> dp(amount + 1, INT_MAX);
        dp[0]=0;
        for(int i=0;i<coins.size();i++)
        {
            for(int j=coins[i];j<=amount;j++)
            {   if (dp[j - coins[i]] != INT_MAX)
                    dp[j]=min(dp[j-coins[i]]+1,dp[j]);//滚动数组
            }
        }
        if(dp[amount]==INT_MAX) return -1;
        return dp[amount];
    }
};

18.完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

class Solution {
public:
    int numSquares(int n) 
    {
        if (isSquare(n))   return 1;
        vector <int> dp(n+5,INT_MAX);
        dp[0];
        dp[1]=1;
        for(int i=2;i<=n;i++)
        {
            if(isSquare(i)) 
            {
                dp[i]=1;
                continue;
            }
            for(int j=1;j<i;j++)
            {
                dp[i]=min(dp[i],dp[j]+dp[i-j]);
            }
        }
        return dp[n];
    }
    bool isSquare(int n) {
        int root = sqrt(n);
        return root * root == n; // 使用整数比较避免浮点数精度问题
    }
};

这么写很慢 不如下面的方法

class Solution {
public:
    int numSquares(int n) 
    {
        if (isSquare(n)) return 1;
        vector<int> dp(n+1, INT_MAX);
        dp[0] = 0; // 基础情况
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j * j <= i; j++)
            {
                dp[i] = min(dp[i], dp[i - j * j] + 1);
            }
        }
        return dp[n];
    }
    bool isSquare(int n)
    {
        int root = sqrt(n);
        return root * root == n;
    }
};

19.单词拆分

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词

‼️字符串理解成背包 单词理解成物品

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(),wordDict.end());    //使用容器减少时间复杂度
        vector<bool> dp(s.size()+5,false);
        dp[0]=true;
        for(int i=1;i<=s.size();i++)
        {
            for(int j=0;j<i;j++)
            {
                string temp = s.substr(j,i-j);//(起始位置,距离)
                if (wordSet.find(temp)!=wordSet.end()&&dp[j]==true)
                    dp[i]=true;
            }
        }
        return dp[s.size()];
    }
};

20.多重背包理论基础

在学蓝桥杯的时候再细学 学完了这里粘贴个链接

21.打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

ez

class Solution {
public:
    int rob(vector<int>& nums) {
        int dp[120]={0};//存放每个房间最多偷的量
        dp[0]=nums[0];
        if(nums.size()>=2)
            dp[1]=max(nums[0],nums[1]);
        for(int i=2;i<nums.size();i++)
        {
            dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
        }
        return dp[nums.size()-1];
    }
};

22.打家劫舍II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。

本体可以化为三种情况

        1.首选尾不选        2.尾选首不选        3. 尾首都不选(包含在12中了)

只需要看三种情况最后的答案谁最大就可以了

比较纯的写法(自己写的)

class Solution
{
public:
    int rob(vector<int> &nums)
    {
        if(nums.size()==1) return nums[0];
        int dp[120] = {0}; // 存放每个房间最多偷的量
        dp[0] = nums[0];
        if (nums.size() >= 2)
            dp[1] = max(nums[0], nums[1]);
        for (int i = 2; i < nums.size() - 1; i++)
        {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        int m1 = 0;
        if (nums.size() >= 2)
            m1 = dp[nums.size() - 2];
        //t
        printf("\n");
        for(int i=0;i<nums.size();i++)
            printf("%d",dp[i]);
        //t
        int dp2[120] = {0}; // 存放每个房间最多偷的量
        if (nums.size() >= 2)
            dp2[1] = nums[1];
        if (nums.size() >= 3)
            dp2[2] = max(nums[1], nums[2]);
        for (int i = 2; i < nums.size(); i++)
        {
            dp2[i] = max(dp2[i - 2] + nums[i], dp2[i - 1]);
        }
        int m2 = dp2[nums.size() - 1];

        //t
        printf("\n");
        for(int i=0;i<nums.size();i++)
            printf("%d",dp2[i]);
        //t
        int dp3[120] = {0}; // 存放每个房间最多偷的量
        dp3[0] = nums[0];
        if (nums.size() >= 2)
            dp3[1] = nums[1];
        if (nums.size() >= 3)
            dp3[2] = max(nums[1], nums[2]);
        for (int i = 2; i < nums.size() - 1; i++)
        {
            dp3[i] = max(dp3[i - 2] + nums[i], dp3[i - 1]);
        }
        int m3 = 0;
        if (nums.size() >= 2)
            m3 = dp3[nums.size() - 2];
        //t
        printf("\n");
        for(int i=0;i<nums.size();i++)
            printf("%d",dp3[i]);
        //t

        return max(max(m1, m2), max(m2, m3));
    }
};

利用函数去除重复部分

class Solution {
public:
    int rob(vector<int>& nums) {
        if (nums.size() == 0) return 0;
        if (nums.size() == 1) return nums[0];
        int result1 = robRange(nums, 0, nums.size() - 2); // 情况二
        int result2 = robRange(nums, 1, nums.size() - 1); // 情况三
        return max(result1, result2);
    }
    // 198.打家劫舍的逻辑
    int robRange(vector<int>& nums, int start, int end) {
        if (end == start) return nums[start];
        vector<int> dp(nums.size());
        dp[start] = nums[start];
        dp[start + 1] = max(nums[start], nums[start + 1]);
        for (int i = start + 2; i <= end; i++) {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[end];
    }
};

23.打家劫舍 III

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

本体很重要 着重复习! 讲的是树状dp

class Solution {
public:
    int rob(TreeNode* root) 
    {
        vector<int> result = tranversal(root);
        return max(result[0],result[1]);
    }
    //dp数组    dp[0]steal  dp[1]not steal
    vector<int> tranversal(TreeNode* root)
    {
        int dp[2];
        if(root==NULL)  return vector<int>{0,0};
        vector<int> left = tranversal(root->left);
        vector<int> right = tranversal(root->right);
        dp[0]=left[1]+right[1]+root->val;
        dp[1]=max(left[0],left[1])+max(right[0],right[1]);
        return vector<int>{dp[0],dp[1]};
    }
};

24. 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

使用贪心做法:(想法还挺巧妙的 我没想道)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int low = INT_MAX;
        int result = 0;
        for (int i = 0; i < prices.size(); i++) {
            low = min(low, prices[i]);  // 取最左最小价格
            result = max(result, prices[i] - low); // 直接取最大区间利润
        }
        return result;
    }
};

使用动态规划做:

使用二维数组来完成这道题

dp[i][0]代表着持有这个股票的最大现金        dp[i][1]代表着不持有这个股票的最大现金

推导公式:        dp[i][0]=max(dp[i-1][0],-prices[i]);

                            dp[i][1]=max(dp[i-1][0]+prices[i],dp[i-1][1]);

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int dp[100005][2];
        dp[0][0]=-prices[0],dp[0][1]=0;
        for(int i=1;i<prices.size();i++)
        {
            dp[i][0]=max(dp[i-1][0],-prices[i]);
            dp[i][1]=max(dp[i-1][0]+prices[i],dp[i-1][1]);
        }
        return dp[prices.size()-1][1];
    }
};

25.买卖股票的最佳时机II

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        vector<vector<int>> dp(len, vector<int>(2, 0));
        dp[0][0] -= prices[0];
        dp[0][1] = 0;
        for (int i = 1; i < len; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); // 注意这里是和121. 买卖股票的最佳时机唯一不同的地方。
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
        }
        return dp[len - 1][1];
    }
};

26.买卖股票的最佳时机III

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格 设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (n <= 1) return 0;

        int dp[n][5];
        //dp[i][0]一直没有买
        //dp[i][1]第一次持有
        //dp[i][2]第一次卖出
        //dp[i][3]第二次持有
        //dp[i][4]第二次卖出

        //初始化
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        dp[0][3] = -prices[0]; // 第二次买入,此时持有的是第一次卖出后的股票,因此是负的价格
        dp[0][4] = 0;

        for (int i = 1; i < n; i++) {
            dp[i][0] = dp[i - 1][0];
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
            dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
        }
        return dp[n - 1][4];
    }
};

27.买卖股票的最佳时机IV

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格 设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)

        本题所做的工作就是对上一道题目的模仿 建议照着上面一道题目写

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int dp[1200][2*k+2];
        //初始化
        for(int i=0;i<prices.size();i++)
        {
            dp[i][0]=0;
        }
        for(int i=0;i<k;i++)
        {
            dp[0][2*i+1]=-prices[0];
            dp[0][2*i]=0;
        }
        //递推公式
        for (int i = 1;i < prices.size(); i++) {
            for (int j = 0; j < 2 * k - 1; j += 2) {    //注意如果有*2需要处理的部分尽量在外面写完
                dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
                dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
            }
        }
        return dp[prices.size()-1][2*k];
    }
};

28.最佳买卖股票时机含冷冻期

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)

dp数组的含义:

                        dp [i] [0]持有该股票

                        dp [i] [1]刚刚卖出该股票

                        dp [i] [2]卖完的冷冻期

                        dp [i] [3]卖完了 已经过了冷冻期

dp数组的推导公式

                        dp [i] [0]=max(dp[i-1][0] , dp[i-1][3]-prices[i],dp[i-1][2]-prices[i])

                        dp[i][1]=dp[i-1][0]+price[i]

                        dp[i][2]=dp[i-1][1]

                        dp[i][3]=max(dp[i-1][3],dp[i-1][3])

dp数组的初始化

                        dp[0][0]=-prices[0]

                         其他全部初始化为0(因为下面本身没有意义 要用推导公式去反推初始化内容)

class Solution {
public:
/*
    dp [i] [0]持有该股票
    dp [i] [1]刚刚卖出该股票
    dp [i] [2]卖完的冷冻期
    dp [i] [3]卖完了 已经过了冷冻期
*/
    int maxProfit(vector<int>& prices) {
        int dp[5002][4];
        dp[0][0]=-prices[0];
        dp[0][1]=0,dp[0][2]=0,dp[0][3]=0;
        for(int i=1;i<prices.size();i++)
        {
            dp[i][0]=max(max(dp[i-1][3]-prices[i],dp[i-1][0]),dp[i-1][2]-prices[i]);
            dp[i][1]=dp[i-1][0]+prices[i];
            dp[i][2]=dp[i-1][1];
            dp[i][3]=max(dp[i-1][3],dp[i-1][2]);
        }
        int m=prices.size()-1;
        return max(max(dp[m][0],dp[m][1]),max(dp[m][2],dp[m][3]));
    }
};

29.买卖股票的最佳时机含手续费

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

和上面的题目几乎一样 只需要减掉手续费即可

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {

        int len = prices.size();
        vector<vector<int>> dp(len, vector<int>(2, 0));
        dp[0][0] -= prices[0];
        dp[0][1] = 0;
        for (int i = 1; i < len; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]-fee);
        }
        return dp[len - 1][1];
    }
};

30.最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列

dp[i] 表示以nums[i]为结尾的最长子序列的长度

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if (nums.empty()) return 0; // 如果数组为空,则最长递增子序列的长度为0
        int dp[2600];
        for (int i = 0; i < nums.size(); i++) dp[i] = 1; // 初始化dp数组,每个元素至少可以构成长度为1的子序列
        
        int result = 1; // 至少有一个数,所以结果从1开始
        for (int i = 1; i < nums.size(); i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = max(dp[i], dp[j] + 1); // 更新dp[i]
                }
            }
            result = max(result, dp[i]); // 更新全局最大长度
        }
        return result; // 返回整个数组的最长递增子序列的长度
    }
};

数组‘dp 初始化不完全:你的代码中‘int dp[2600]={1};‘这样的初始化语句只会将dp[0] 初始化为1,其他的元素都会被默认初始化为 0。根据最长递增子序列问题的定义,每个元素自身至少可以构成一个长度为1的递增子序列,因此你需要将、dp数组的所有元素都初始化为1。

31.最长连续递增序列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        if(nums.size()==1)  return 1;
        vector<int> dp(nums.size()+1,0);
        dp[0]=1;
        for(int i=0;i<nums.size()-1;i++)
        {
            if(nums[i]<nums[i+1])
                dp[i+1]=dp[i]+1;
            else
                dp[i+1]=1;
        }
        int max_len=1;
        for(int i=0;i<nums.size();i++)
        {
            printf("%d",dp[i]);
            max_len = max(max_len,dp[i]);
        }
            
        return max_len;
    }
};

32.最长重复子数组

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度

dp数组的定义:dp[i][j]存放数组A结尾为i-1    数组B结尾为j-1的最大重复子序列的长度

递推公式:       if(nums1[i]==nums2[j])

                                dp[i][j]=dp[i-1][j-1]+1;

初始化 :全部初始化为0

注意:这么定义dp数组可以有效防止index=-1 不用特殊判断

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int dp[nums1.size()+5][nums2.size()+5];
        //初始化
        for(int i=0;i<=nums1.size();i++)
        {
            for(int j=0;j<=nums2.size();j++)
            {
                dp[i][j]=0;
            }
        }
        //递推 
        int max_len=0;
        for(int i=1;i<=nums1.size();i++)    //=要遍历到最后一个元素
        {
            for(int j=1;j<=nums2.size();j++)
            {
                if(nums1[i-1]==nums2[j-1])
                {
                    if(i>=1&&j>=1)
                        dp[i][j]=dp[i-1][j-1]+1;
                }
                //printf("%d ",dp[i][j]);
                max_len=max(max_len,dp[i][j]);
            }
            //printf("\n");
        }
        return max_len;
    }
};

33.最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。若这两个字符串没有公共子序列,则返回 0。

和上面一道题类似

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int dp[text1.size()+5][text2.size()+5];
        //初始化
        for(int i=0;i<=text1.size();i++)
        {
            for(int j=0;j<=text2.size();j++)
            {
                dp[i][j]=0;
            }
        }
        //递推
        for(int i=1;i<=text1.size();i++)
        {
            for(int j=1;j<=text2.size();j++)
            {
                if(text1[i-1]==text2[j-1])
                {
                    dp[i][j]=dp[i-1][j-1]+1;
                }
                else
                {
                    dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }
        return dp[text1.size()][text2.size()];
    }
};

34.不相交的线

我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。以这种方法绘制线条,并返回我们可以绘制的最大连线数。

其实就是最长公共子序列 连代码都没有变 重点是懂思路

(其实不🍌就是保证数组的相对位置不变 就是最长公共子序列)

35.最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和

1.前面用贪心算法完成过本体的求解 实际上就是直接加上部分和>0的部分

2.使用动态规划解题

dp[i]数组的含义:包含dp[i]的子序列的和的最大值

递推公式:dp[i]=max(nums[i],dp[i-1]+nums[i])

初始化:dp[0] = nums[0];

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        if(nums.size()==1)   return nums[0];
        int dp[nums.size()+5];
        dp[0]=nums[0];
        int max_value=INT_MIN;
        for(int i=1;i<nums.size();i++)
        {
            dp[i]=max(nums[i],dp[i-1]+nums[i]);
            max_value=max(max_value,dp[i]);
        }
        return max(max_value,dp[0]);
    }
};

36.判断子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)

其实本题就是上面一道题保证子序列是s序列的长度

dp[i][j]数组的定义: 数组i-1 j-1 的为结尾的子序列的长度

推导公式: if(s[i-1]==t[i-1])        dp[i][j]=dp[i-1][j-1]+1;

                     else                     dp[i][j]=max(dp[i-1][j],dp[i][j-1]);

初始化:全部初始化为0

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int dp[105][10005];
        for(int i=0;i<s.size();i++)
        {
            for(int j=0;j<t.size();j++)
            {
                dp[i][j]=0;
            }
        }
        for(int i=1;i<=s.size();i++)
        {
            for(int j=1;j<=t.size();j++)
            {
                if(s[i-1]==t[j-1])
                    dp[i][j]=dp[i-1][j-1]+1;
                else
                    dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
                    //其实本体这里可以改写一下
                    //else dp[i][j] = dp[i][j - 1];
                    //因为有严格的大小关系
            }
        }
        return dp[s.size()][t.size()]==s.size();
    }
};

37.不同的子序列

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数 字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)题目数据保证答案符合 32 位带符号整数范围

dp[i][j]数组的定义:dp数组 i-1 j-1 为结尾的数组的最大子序列数目

递推公式:        if(s[i]==t[i])        dp[i][j]=dp[i-1][j-1]+dp[i-1][j];//不使用i-1和使用i-1

                            else                         dp[i][j]=dp[i-1][j];

初始化:dp[0][0]=1;空字符串        dp[i][0]=1        dp[0][i]=0

!因为每一个数组都是由左上角的数组推出来的 所以我们要对最左边以及最上边的一行初始化

class Solution {
public:
    int numDistinct(string s, string t) {
        int dp[s.size()+1][t.size()+1];
        // 初始化
        for(int i=0; i<=s.size(); i++) {
            dp[i][0] = 1;
        }
        for(int j=1; j<=t.size(); j++) {
            dp[0][j] = 0;
        }
        // 递推公式
        for(int i=1; i<=s.size(); i++) {
            for(int j=1; j<=t.size(); j++) {
                if(s[i-1] == t[j-1]) {
                    dp[i][j] = dp[i-1][j-1] + dp[i-1][j]; // 用+不用
                } else {
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[s.size()][t.size()];
    }
};

代码会超int 范围 改改

class Solution {
public:
    int numDistinct(string s, string t) {
        const int MOD = 1e9 + 7;
        vector<vector<long long>> dp(s.size() + 1, vector<long long>(t.size() + 1, 0));
        for (int i = 0; i <= s.size(); i++) {
            dp[i][0] = 1;
        }
        for (int j = 1; j <= t.size(); j++) {
            dp[0][j] = 0;
        }
        for (int i = 1; i <= s.size(); i++) {
            for (int j = 1; j <= t.size(); j++) {
                if (s[i - 1] == t[j - 1]) {
                    dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j]) % MOD;
                } else {
                    dp[i][j] = dp[i - 1][j] % MOD;
                }
            }
        }
        return dp[s.size()][t.size()];
    }
};

38.两个字符串的删除操作

给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符

dp[i][j]数组的定义:结尾是i-1 和j-1的数组删除的最小值

递推公式:if(word[i]==word[j])        dp[i][j]=dp[i-1][j-1]

                    else        dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1)

初始化:     dp[0]=0        dp[i][0]=i        dp[0][j]=j

class Solution {
public:
    int minDistance(string word1, string word2) {
        int dp[word1.size()+5][word2.size()+5];
        //初始化
        for(int i=0;i<=word1.size();i++)
            dp[i][0]=i;
        for(int j=0;j<=word2.size();j++)
            dp[0][j]=j;
        dp[0][0]=0; //可以不写 但是建议每次把交点的初始化重新写一遍
        //递推公式
        for(int i=1;i<=word1.size();i++)
        {
            for(int j=1;j<=word2.size();j++)
            {
                if(word1[i-1]==word2[j-1])
                    dp[i][j]=dp[i-1][j-1];
                else
                    dp[i][j]=min(dp[i-1][j],dp[i][j-1])+1;
            }
        }
        return dp[word1.size()][word2.size()];
    }
};

39.编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 

dp[i][j]数组的定义:dp数组是存放结尾i-1 j-1的数组

递推公式:

        if(word[1]==word[2])        dp[i][j]=dp[i-1][j-1]

        else        dp[i][j]=min(min(dp[i][j-1],dp[i-1][j]),dp[i-1][j-1]);

class Solution {
public:
    int minDistance(string word1, string word2) {
        int dp[word1.size()+5][word2.size()+5];
        //初始化
        for(int i=0;i<=word1.size();i++)
            dp[i][0]=i;
        for(int j=0;j<=word2.size();j++)
            dp[0][j]=j;
        dp[0][0]=0;
        for(int i=1;i<=word1.size();i++)
        {
            for(int j=1;j<=word2.size();j++)
            {
                if(word1[i-1]==word2[j-1])
                    dp[i][j]=dp[i-1][j-1];
                else
                    dp[i][j]=min(min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;//减 增 换
            }
        }
        return dp[word1.size()][word2.size()];
    }
};

40.回文子串

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

bool dp[i][j]的含义:dp数组从下标i 到下标j之间的字串都否是回文字串

递推公式

if(s[i]==s[j])
        {
            if(j-i<=3)  dp[i][j]=true;
            else
            {
                if(dp[i+1][j-1]==true)  dp[i][j]=true;
            }
        }

初始化:全部初始化为false

遍历顺序:因为本体是 左下推出右上 所以i倒序 j正序

class Solution {
public:
    int countSubstrings(string s) {
        bool dp[s.size()+5][s.size()+5];
        //初始化
        for(int i=0;i<s.size();i++)
        {
            for(int j=0;j<s.size();j++)
            {
                dp[i][j]=false;
            }
        }
        //递推公式
        int result=0;
        for(int i=s.size()-1;i>=0;i--)
        {
            for(int j=i;j<s.size();j++)
            {
                if(s[i]==s[j])
                {
                    if(j-i<=2)  dp[i][j]=true;
                    else
                    {
                        if(dp[i+1][j-1])    dp[i][j]=true;
                    }
                }
                if(dp[i][j])    result++;
            }
        }
        return result;
    }
};

41.最长回文子序列

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int dp[s.size()+5][s.size()+5];
        //初始化
        for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
        //递推公式
        for(int i=s.size()-1;i>=0;i--)
        {
            for(int j=i+1;j<s.size();j++)
            {
                if(s[i]==s[j])
                {
                    if(j-i<=2)  dp[i][j]=j-i+1;
                        else dp[i][j]=dp[i+1][j-1]+2;
                }
                else    dp[i][j]=max(dp[i][j-1],dp[i+1][j]);
            }
        }
        return dp[0][s.size()-1];
    }
};

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值