动态规划学习

题型

动态规划的分类
序列型:问题可以按照某种顺序一步步决策,例如最长上升子序列。
分割型:问题需要将数据分割成多个部分,例如矩阵链乘问题或分割等和子集。
背包型:问题涉及选择与否,例如0/1背包问题、完全背包问题等。
区间型:问题涉及一系列的区间,需要进行区间合并或分割,例如区间合并游戏。
树形DP:问题中的数据结构是树形的,需要在树上进行动态规划,例如树的直径或最大独立集。
状态压缩DP:当状态可以通过位操作压缩表示时,如旅行商问题(TSP)。

解题思路

动态规划解题步骤

定义状态:
确定状态数组表示什么含义。通常状态表示到达当前步骤的最优解。
状态转移方程:
这是动态规划中最核心的部分,需要找出状态之间的关系。例如,dp[i] 可能依赖于 dp[i-1] 或 dp[i-2] 等。
初始化状态:
根据问题的边界条件来初始化状态。例如,最小路径问题中 dp[0][0] 是起始点。
填充状态表:
按照逻辑顺序(通常是自底向上)计算所有状态。

基础动态规划题

509. 斐波那契数

在这里插入图片描述
思路:
题目其实已经告诉我们递推公式了。
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1。
我们就考虑怎么去初始化这个dp,然后后续怎么遍历呢?dp解决问题主要就是从下往上解决问题,这样就可以避免重复性。对于这道题是不是就属于从左往右遍历?
完整代码如下

class Solution {
public:
    int fib(int n) {
        if(n<0){
            return 0;
        }

         vector<int> dp(n + 1);
         dp[0] = 0; // 初始化第一个元素

         if(n>0){
            dp[1] = 1;   // 如果大于0 初始化第二个元素
         }
 
        for(int i = 2 ; i<=n;i++){
            dp[i] = dp[i-1]+dp[i-2]; // 后续dp数组填补
        }
    
        for(int i = 0 ;i<=n;i++){
        cout<<dp[i]<<"  ";
        }
         return dp[n];
    }
};

70. 爬楼梯

在这里插入图片描述
该题和斐波那契数列数列一样都是经典dp题库。

完整代码:

class Solution {
public:
    int climbStairs(int n) {
        if(n<1) return 0; // 确保楼梯大于0

        vector<int>dp(n+1);  // dp[i] 就代表当 n=i 所保存的方法
        dp[1] = 1;
        if(n>1) dp[2] = 2;  // 大于1 初始化第二个元素
        for(int i =3;i<=n;i++){
            dp[i] = dp[i-1]+dp[i-2];
        }
        return dp[n];
    }
};

746. 使用最小花费爬楼梯

在这里插入图片描述
思路:

想明白一点,对于比如我想到达第三个阶梯,是不是就两种方案:

  1. 我从第一个阶梯 走两步 花费:dp[i-2]+cost[i-2]
  2. 我从第二个阶梯 走一步 花费:dp[i-1]+cost[i-1] 所以,我们依旧坚持从下往上的思路,我每次都选择最小花费,那最后不就是最小花费了嘛?

重点:
此处dp代表什么?此处dp代表到达i这个点,我需要花费多少

完整代码:

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int>dp(cost.size()+1);
        dp[0] = 0;  // `此处dp代表到达i这个点,我需要花费多少`
        dp[1] = 0;
        for(int i = 2 ; i<=cost.size();i++){
            dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }  
        return dp[cost.size()];
    }
};

62.不同路径

在这里插入图片描述
思路:

1.先明确dp代表什么,此处代表从左上角到达该点的走路方法数
2.怎么得到递推公式呢? 因为只能向右和向下
就属于 dp[i][j] = dp[i-1][j]+dp[i][j-1];
3.再根据此公式填充 dp 数组表

在这里插入图片描述
完整代码如下:

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m,vector<int>(n,0));
        
        //初始化
        for(int i = 0; i<m;i++){
            dp[i][0] = 1;
        }
        for(int j = 0; j<n;j++){
            dp[0][j] = 1;
        }
        // 补充dp表
        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];
                cout<<dp[i][j]<<" ";
            }
        }
        return dp[m-1][n-1];
    }
};

63. 不同路径 II

在这里插入图片描述
思路:

和上文的不同路径其实没多大区别。只不过对于障碍物,我们在dp中置为0就好了
递推公式没变化: dp[i][j] = dp[i-1][j]+dp[i][j-1];

我这里还是和上题一样,先初始化行和列,只不过此处我们的 dp[i][0] = dp[i - 1][0],dp[0][j] = dp[0][j - 1]。因为是第一行和第一列嘛。

完整代码:

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        vector<vector<int>> dp(m,vector<int>(n,0));

        if (obstacleGrid[0][0] == 1) {
            dp[0][0] = 0;
        } else {
            dp[0][0] = 1;
        }

        // 初始化
        for(int i = 1; i<m;i++){
            if (obstacleGrid[i][0] == 1) {
                dp[i][0] = 0;
            } else {
                dp[i][0] = dp[i - 1][0];
            }
        }
        
        for(int j = 1; j<n;j++){
            if (obstacleGrid[0][j] == 1) {
                dp[0][j] = 0;
            } else {
                dp[0][j] = dp[0][j - 1];
            }
        }

        // 填充dp数组
        for(int i = 1 ;i<m;i++){
            for(int j = 1; j<n;j++){
                if(obstacleGrid[i][j] == 1){
                    dp[i][j] = 0;
                }else{
                    dp[i][j] = dp[i-1][j]+dp[i][j-1];
                }
            }
        }
        
        cout<<endl;
        for(int i = 0 ;i<obstacleGrid.size();i++){
            for(int j = 0; j<obstacleGrid[0].size();j++){
               cout<<dp[i][j]<<" ";
            }
        }
        return dp[m-1][n-1];
    }
};

343. 整数拆分

在这里插入图片描述
思路:

先确定 dp[i] 这个代表 对于 i 这个整数,最大乘积化为多少。
其次,我们怎么从小推出 dp?
dp[i] = j*(i-j), dp[i] = dp[i-j]*j,就这两种方案。

class Solution {
public:
    int integerBreak(int n) {
        vector<int>dp(n+1);

        dp[2] = 1;
        for(int i = 3; i<=n;i++){
            for(int j = 1 ; j<=i-1 ; j++){ // `拆分次数,拆分为什么情况`
                dp[i] = max(dp[i], max(j*(i-j),dp[i-j]*j));
            }
        }
        return dp[n];
    }
};

96.不同的二叉搜索树

在这里插入图片描述
什么是二叉搜索树?
二叉搜索树是一种特殊的二叉树,其中每个节点满足以下条件:

左子树中所有节点的值都小于根节点的值。
右子树中所有节点的值都大于根节点的值。
左右子树也分别为二叉搜索树。

思路:

对序列我们任一取它随意一个点 i 作为根节点,则,左子树由 [1, i-1] 构成,右子树由 [i+1, n] 构成。
状态转移方程如下:,这里 dp[i-1] 是左子树的数量,而 dp[n-i] 是右子树的数量。
状态

class Solution {
public:
    int numTrees(int n) {
        vector<int>dp(n+1);

        dp[0] = 1; // `空树也是搜索二叉树`
        dp[1] = 1;

        for(int nodes = 2; nodes<=n ;nodes++){
            for(int root = 1; root <= nodes; root++){ 
                dp[nodes] += dp[root-1]*dp[nodes-root];
            }
        }
        return dp[n];
    }
};

01背包问题

// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
        if (j < weight[i]) dp[i][j] = dp[i - 1][j]; 
        // `这个是代表后续物品重量大于背包,防止了无效的操作(尝试放入一个根本无法放入的物品)`
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    }
}

416. 分割等和子集

在这里插入图片描述
回溯法,会产生超时问题

class Solution {
public:

    vector<int> path;

    bool back(vector<int>& nums,int index,int all){
        int res = allpath(path);

        if(res == all/2){
            return true;
        }
        for(int i = index;i<nums.size();i++){
            path.push_back(nums[i]);
            if(back(nums,i+1,all)){
                return true;
            }
            path.pop_back();
        }
        return false;
    }

    int allpath( vector<int> path){
        int temp = 0;
        for(int i = 0; i<path.size();i++){
            temp+=path[i];
        }
        return temp;
    }

    bool canPartition(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        int all = 0;
        for(int i = 0;i <nums.size();i++){
            all+=nums[i];
        }
        if(all %2 != 0) return false;
        return back(nums,0,all);
    }
};

此时我们使用dp,但此处我用的是二维dp

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        int all = 0; // 总数
        for(int num : nums){
            all += num;
        }
        if(all %2 != 0) return false;
        int target = all/2;  // 总和一半

        vector<vector<int>>dp(nums.size()+1,vector<int>(target+1,0)); // dp数组

        for(int i= 0;i<=nums.size();i++){
            dp[i][0] = 0; // 初始化,背包容量为0的时候
        }

        // dp 数组填充
        for(int i = 1; i<=nums.size();i++){
            for(int j = 1; j<=target;j++){
                if(j < nums[i-1]){
                    dp[i][j] = dp[i-1][j];
                }
                else{
                    dp[i][j] = max(dp[i-1][j],dp[i-1][j-nums[i-1]]+nums[i-1]);
                }
            }
        }
        if(dp[nums.size()][target] == target){
            return true;
        }
        return false;
    }
};

一维dp数组

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        int all = 0; // 总数
        for(int num : nums){
            all += num;
        }
        if(all %2 != 0) return false;
        int target = all/2;  // 总和一半

        vector<int>dp(target+1,0); // dp数组

        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;
    }
};

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

在这里插入图片描述
思路:

这个和上个题分割子问题,没什么区别,就是分为两堆石头,然后差值就是剩下的。
只是再次注意一点,因为是不重复的东西,我们一维dp是从后往前进行遍历的。

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int allnum = 0;
        for(int num : stones){
            allnum += num;
        }

        int target = allnum/2;
        vector<int>dp(target+1,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 allnum - dp[target] - dp[target];
    }
};

494.目标和

在这里插入图片描述
回溯法:

我的第一个想法就是回溯,逐步进行搜索.

// 回溯法
class Solution {
public:
    int count;
    void back(vector<int>& nums,int target,int index,int sum){
        if(index == nums.size()){
            if(sum == target){
                count++;
            }
            return;
        }
        back(nums,target,index+1,sum-nums[index]);
        back(nums,target,index+1,sum+nums[index]);
    }

    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        back(nums,target,0,sum);
        return count;
    }
};

动态规划:

此处动态规划是属于那种计数问题,常规递归公式如下
dp[j] += dp[j - nums[i]];

474.一和零

在这里插入图片描述
思路:

这里,我们相通一个点,子集字符串就等于物品
背包就是 m 和 n 它的含量.
dp[i][j] = max(dp[i][j],dp[i-zeros][j-ones]+1); 这个就是从后往前遍历,

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        // `外层物品,内层背包`
        vector<vector<int>>dp(m+1,vector<int>(n+1,0)); 

        for(string& str : strs){
            int zeros = count(str.begin(),str.end(),'0');
            int ones = str.size() - zeros;

            for(int i = m;i>=zeros;i--){
                for(int j = n;j>=ones;j--){
                    dp[i][j] = max(dp[i][j],dp[i-zeros][j-ones]+1);
                }
            }
        }
        return dp[m][n];
    }
};

完全背包问题

核心代码:
这是属于组合

for (int i = 0; i < coins.size(); i++) { // 遍历物品
    for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
        dp[j] += dp[j - coins[i]];
    }
}

这是属于排列

for (int j = 0; j <= amount; j++) { // 遍历背包容量
    for (int i = 0; i < coins.size(); i++) { // 遍历物品
        if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
    }
}

518.零钱兑换II

在这里插入图片描述
思路:

我们先分辨出这是个完全背包问题.
dp[j] = dp[j] +dp[j-coins[i]];
// dp[j] 表示之前我没加上 coins[i] 的方法数
// dp[j-coins[i]] 表示我加上 coins[i] 的方法数
// 这样总的叠加就是新的dp[j]

完整代码:

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int>dp(amount+1,0);
        dp[0] = 1;
        for(int i = 0;i<coins.size();i++){
            for(int j = coins[i];j<=amount;j++){
                // dp[j] 表示之前我没加上 coins[i] 的方法数
                // dp[j-coins[i]] 表示我加上 coins[i] 的方法数
                // 这样总的叠加就是新的dp[j]
                dp[j] += dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
};

377. 组合总和 Ⅳ

在这里插入图片描述
思路:

这是一个排列问题,.
核心就是,先背包,后物品

完整代码:

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<uint64_t>dp(target+1,0);

        dp[0] = 1;
        for(int j = 0; j<=target ;j++){
            for(int i = 0; i<nums.size();i++){
                if(j>=nums[i]) dp[j] += dp[j-nums[i]];
            }
            for(int num : dp){
                cout<<num<<' '; 
            }
            cout<<endl;
        }
        return dp[target];
    }
};

322. 零钱兑换

在这里插入图片描述
思路:

我的第一个想法就是回溯,代码如下,但是存在问题,就是超出时间限制

class Solution {
public:

    int res;
    int count = 0;
    int minnum = INT_MAX;
    void back(vector<int>& coins,int amount,int res,int index,int count){
        if(res > amount){
            return;
        }
        if(res == amount){
            minnum = min(minnum,count); 
            return;
        }
        for(int i = index; i>=0;i--){
            res += coins[i];
            back(coins,amount,res,i,count+1);
            res -= coins[i];
        }
    }

    int coinChange(vector<int>& coins, int amount) {
        // 贪心方法
        sort(coins.begin(),coins.end());
        //if(coins.back() > amount) return -1;
        back(coins,amount,0,coins.size()-1,0);
        return minnum == INT_MAX ? -1 : minnum;

    }
};

279.完全平方数

在这里插入图片描述
思路

我们先明白,这是什么问题,完全背包问题
其次,物品是什么?物品就是数字,从1到n开平方;
背包是什么?就是n;

class Solution {
public:
    int numSquares(int n) {
        vector<int> dp(n+1,n+1);
        dp[0] = 0;
        for(int i = 1;i*i<=n;i++){
            for(int j = i*i;j<=n;j++){
                dp[j] = min(dp[j],dp[j-i*i]+1);
            }
        }
        return dp[n];
    }
};

139.单词拆分

在这里插入图片描述
思路一:回溯法(超出时间限制)

class Solution {
public:

    string path;
    bool res = false;
    void back(vector<string>& wordDict,string s,int index){
        if(path == s){
            res = true;
            return;
        }
        if(path.size()>s.size()){
            return;
        }
        for(int i = 0;i<wordDict.size();i++){
            path.append(wordDict[i]);
            back(wordDict,s,i);
            path.erase(path.size()-wordDict[i].size(),wordDict[i].size());
        }
    }

    bool wordBreak(string s, vector<string>& wordDict) {
        back(wordDict,s,0);
        return res;
    }
};

思路二:加入记忆搜索的回溯

class Solution {
public:
    string path;
    bool res = false;
    unordered_map<int, bool> memo; // 用于记忆化的哈希表

    void back(vector<string>& wordDict, string& s, int index){
        if (index == s.size()) {
            res = true;
            return;
        }
        if (memo.count(index) && !memo[index]) {
            return; // 如果这个子问题已解决且结果为 false,直接返回
        }
        if (path == s) {
            res = true;
            memo[index] = true; // 记录成功的分解
            return;
        }
        if (path.size() > s.size()) {
            return;
        }
        for (int i = 0; i < wordDict.size(); i++) {
            path.append(wordDict[i]);
            if (s.substr(0, path.size()) == path) { // 只有当路径是目标字符串的前缀时才递归
                back(wordDict, s, index + wordDict[i].size());
            }
            path.erase(path.size() - wordDict[i].size(), wordDict[i].size());
            if (res) return; // 如果已经找到解,就中断循环
        }
        memo[index] = res; // 记录当前索引的结果
    }

    bool wordBreak(string s, vector<string>& wordDict) {
        back(wordDict, s, 0);
        return res;
    }
};

思路三:完全背包.

字符串就是背包
字典就是物品
字符串是由特定物品组成,就等于这还是个排列问题

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        int n = s.size(); // 背包
        int m = wordDict.size(); // 物品

        vector<bool>dp(n+1,false);
        dp[0] = true;

        for(int j = 1;j<=n;j++){
            for(int i = 1;i<=m;i++){
                string word = wordDict[i-1];
                if(j >= word.size()  && s.substr(j-word.size(),word.size()) == word ){
                    dp[j] = dp[j] || dp[j-word.size()];
                }
            }
        }        
        return dp[n];
    }
};

多重背包问题

打家劫舍问题

核心代码:

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];
    }

198.打家劫舍

在这里插入图片描述
思路:

首先、我们先搞清楚 dp[i] 代表什么,代表从前 i 个房屋中能够偷取的最大金额。
第一、dp[i - 1] 表示不偷取当前房屋(第 i 个房屋)时,前 i 个房屋能够偷取的最大金额。
第二、dp[i - 2] + nums[i - 1] 表示偷取当前房屋时,前 i 个房屋能够偷取的最大金额。其中 dp[i - 2] 表示不偷取前一个房屋(第 i-1 个房屋),因为连续偷两个相邻的房屋会触发警报,所以要从前前一个房屋(第 i-2 个房屋)开始计算,当前房屋的金额 nums[i - 1]。

完整代码:

class Solution {
public:
    int rob(vector<int>& nums) {
        vector<int>dp(nums.size()+1,0);
        dp[0] = 0;
        dp[1] = nums[0];
        cout<<dp[1]<<"lala ";
        for(int i = 2; i<=nums.size();i++){
            dp[i] = max(dp[i-1],dp[i-2]+nums[i-1]);
        }
        return dp[nums.size()];

    }
};

213.打家劫舍II

在这里插入图片描述
思路:

我们把问题分解为两个子问题:
问题一:我们偷取第一个房子,不偷最后一个
问题二:我们不偷第一个,我们偷最后一个
最后比较,谁大返回谁

class Solution {
public:
    int rob(vector<int>& nums) {
        int n =  nums.size();
        if(n==0) return 0;
        if(n == 1) return nums[0];
        if(n == 2) return max(nums[0],nums[1]);

        // 获取第一个房屋,不偷最后一个
        vector<int> dp1(n+1,0);
        dp1[0] = 0;
        dp1[1] = nums[0];
        for(int i = 2 ; i<=n-1 ; i++){
            dp1[i] = max(dp1[i-1],dp1[i-2]+nums[i-1]);
        }

        // 获取最后一个房屋,不偷第一个
        vector<int> dp2(n+1,0);
        dp2[0] = 0;
        dp2[1] = 0;
        dp2[2] = nums[1];
        dp2[3] = max(nums[1],nums[2]);
        for(int i = 4 ; i<=n;i++){
            dp2[i] = max(dp2[i-1],dp2[i-2]+nums[i-1]);
        }

        return max(dp1[n-1],dp2[n]);

    }
};

337.打家劫舍 III

在这里插入图片描述
思路:

分为两个子问题:
问题一:获取当前根节点最大数
问题二:不获取当前根节点最大数

class Solution {
public:
    int rob(TreeNode* root) {
        vector<int>res = rogchange(root);
        return max(res[0],res[1]);
    }

    vector<int> rogchange(TreeNode *root){
        if(!root) return{0,0};

        vector<int>left = rogchange(root->left);
        vector<int>right = rogchange(root->right);

        //{0,1}  0代表不获取当前节点,1代表获取当前节点数
        // 不获取根节点
        int dp0 = max(left[0],left[1])+max(right[0],right[1]);
        //  获取根节点
        int dp1 = root->val + left[0] + right[0];

        return {dp0,dp1};
    }
};

股票问题

121. 买卖股票的最佳时机

在这里插入图片描述

思路一:暴力搜索,会超出时间限制

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        // 暴力
        int res = INT_MIN;
        for(int i = 0; i<prices.size();i++){
            for(int j = i+1;j<prices.size();j++){
                if((prices[j] - prices[i] )>=0){
                    res = max(res,(prices[j] - prices[i] ));
                }
            }
        }
        return res == INT_MIN ? 0:res;
    }
};

暴力优化了一下:

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

        int minnum = prices[0];
        int res = INT_MIN;

        for(int i =1;i<n;i++){
            if(prices[i]<minnum){
                minnum = prices[i];
            }
            else{
                res = max(res,prices[i]-minnum);
            }
        }
        return res== INT_MIN ? 0:res;
    }
};

思路三:动态规划

我们先分解为子问题:
问题一: 当天没股票 我的总金额
是不是没有股票就是, dp[i-1][0](前一天就没有股票), dp[i-1][1]+prices[i])(或者就是前一天室友股票的但是我今天,卖掉)
dp[i][0] = max(dp[i-1][0] , dp[i-1][1]+prices[i]); // 没有股票
问题二: 当天有彩票,我的总金额.
dp[i-1][1](前一天我就已经有了彩票) -prices[i],代表我今天刚买入彩票
dp[i][1] = max(dp[i-1][1],-prices[i]); // 有股票

完整代码:

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

        // 初始化状态  0 代表没有股票,1 代表有股票
        dp[0][0] = 0;       // 第一天没股票
        dp[0][1] = -prices[0];  // 第一天有股票的金额

        for(int i = 1; i<n;i++){
            // 两种情况,含有股票,和没有股票
            dp[i][0] = max(dp[i-1][0] , dp[i-1][1]+prices[i]); // 没有股票

            dp[i][1] = max(dp[i-1][1],-prices[i]); // 有股票
        }
        return dp[n-1][0]; // 返回最后一天没有股票的最大金额
    }
};

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

在这里插入图片描述
思路:

我们还是分为两个子问题
问题一:当天是否有股票
问题二:当天是否没股票

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n+1,vector<int>(2,0));

        // 0代表没股票,1代表有股票
        dp[0][0] = 0;
        dp[0][1] = -prices[0];

        for(int i = 1 ;i<n;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]); // 当天股票
        }
        return dp[n-1][0];
    }
};

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

在这里插入图片描述
思路一:

我还是分解为子问题
问题一:第一次购买
问题二:第二次购买

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

        vector<vector<vector<int>>> dp(n, vector<vector<int>>(3, vector<int>(2, 0)));

        // 初始化第一天的情况
        dp[0][0][0] = 0;
        dp[0][0][1] = -prices[0];
        dp[0][1][0] = dp[0][2][0] = 0;
        dp[0][1][1] = INT_MIN; // 不可能的状态初始化为一个非常小的数

        for (int i = 1; i < n; i++) {
            // 第 i 天没有进行任何交易,且不持有股票。
            dp[i][0][0] = dp[i-1][0][0];
            // 第 i 天没有进行任何交易,但持有股票。
            dp[i][0][1] = max(dp[i-1][0][1], dp[i-1][0][0] - prices[i]);  // 只买不卖

            // 第 i 天进行了一次交易,且不持有股票。
            dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][0][1] + prices[i]);
            // 第 i 天进行了一次交易,持有股票
            dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][1][0] - prices[i]);  // 第一次买入

            // 第 i 天进行了两次交易,且不持有股票。
            dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][1][1] + prices[i]);  // 第二次卖出
            //dp[i][2][1] = INT_MIN;  // 在限制条件下不存在进行第三次买入的可能
        }
        
        // 最大利润,可以是0次、1次或2次交易
        return max({dp[n-1][0][0], dp[n-1][1][0], dp[n-1][2][0]});
    }
};

三维数组优化一下

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

        dp[0][0] = 0; // 第一天没有任何交易。
        dp[0][1] = -prices[0]; //第一天买入股票
        dp[0][2] = 0;// 第一天不可能完成一次卖出,初始化为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 max(dp[n-1][4],dp[n-1][2]);
    }
};

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

在这里插入图片描述
思路:

借鉴上一题,我们同样化为两个子问题
问题一: 单数代表我买股票怎么样操作
问题二:双数代表我卖股票怎么样操作

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>>dp(n+1,vector<int>(2*k+1,0));

        // 单数代表我买入股票,双数代表我卖出股票
        dp[0][0] = 0;
        for(int j = 1; j<=2*k ; j+=2){
            dp[0][j] = -prices[0];
        }

        for(int i = 1 ;i<n;i++){
            for(int j = 1;j<=2*k;j++){
                if(j % 2 == 0){
                    dp[i][j] = max(dp[i-1][j],dp[i-1][j-1]+prices[i]);
                }
                else{
                    dp[i][j] = max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
                }
            }
        }
        return dp[n-1][2*k];
    }
};

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

在这里插入图片描述

思路:

直接按照上题的模板写出的

完整代码:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if(n<=1) return 0;
        
        // 自动确定最大交易次数
        int k = min((n + 1) / 3, n / 2);
        vector<vector<int>> dp(n+1,vector<int>(2*k+1,0));
		// 初始化
        dp[0][0] = 0;
        for(int j = 1 ; j<=2*k;j+=2){
            dp[0][j] = -prices[0]; 
        }
        for(int j = 1 ; j<=2*k;j++){
            if(j%2 == 0){
                dp[1][j] = max(dp[0][0], dp[0][1] + prices[1]); // 第二天结束时,不持有股票
            }
            else{
                dp[1][j] = max(dp[0][1], -prices[1]);  // 第二天结束时,持有股票(直接买入)
            }
        }
		// 数组填充
        for(int i = 2;i<n;i++){
            for(int j = 1; j<=2*k ;j++){
                if(j%2 == 0){
                    dp[i][j] = max(dp[i-1][j],dp[i-1][j-1] + prices[i]); //卖出
                }
                else{
                    dp[i][j] = max(dp[i-1][j],dp[i-2][j-1] - prices[i]); // 买入,隔一天买入
                }
            }
        }
        return dp[n-1][2*k];
    }
};

优化一下:
思路:

分解为两个子问题

我们先定义两个状态:
dp[i][0]: 第 i 天结束时,不持有股票的最大利润(可能是冷冻期的一天)。
dp[i][1]: 第 i 天结束时,持有股票的最大利润。

问题一:当天没有股票
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]):第 i 天结束时不持有股票的利润来自两种情况,前一天也不持有或者前一天持有但在第 i 天卖出。
问题二:当天有股票
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i]):第 i 天结束时持有股票的利润也来自两种情况,前一天持有或者前两天不持有(因为冷冻期)并在第 i 天买入。

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

        vector<vector<int>>dp(n+1,vector<int>(2,0));

        dp[0][0] = 0; // 第一天结束时,不持有股票
        dp[0][1] = -prices[0]; // 第一天结束时,持有股票
        dp[1][0] = max(dp[0][0],dp[0][1]+prices[1]); // 第二天结束时,不持有股票
        dp[1][1] = max(dp[0][1],-prices[1]);  // 第二天结束时,持有股票(直接买入)

        for(int i = 2 ;i<n;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-2][0] - prices[i]); // 持有股票时考虑冷冻期
        }
        return dp[n-1][0];

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

在这里插入图片描述
思路:

这个和最初的可以任意次数购买股票一样
额外的地方就是再买入的时候加上手续费

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        vector<vector<int>> dp(n+1,vector<int>(2,0));

        dp[0][0] = 0;
        dp[0][1] = -prices[0] -fee;

        for(int i = 1; i<n; 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[n-1][0];
    }
};

股票问题总结

核心代码如下:

k 代表能够买卖几次。
代码初始化如下:

        // 单数代表我买入股票,双数代表我卖出股票
        dp[0][0] = 0;
        for(int j = 1; j<=2*k ; j+=2){
            dp[0][j] = -prices[0];
        }

dp数组填充如下:

        for(int i = 1 ;i<n;i++){
            for(int j = 1;j<=2*k;j++){
                if(j % 2 == 0){
                    dp[i][j] = max(dp[i-1][j],dp[i-1][j-1]+prices[i]);
                }
                else{
                    dp[i][j] = max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
                }
            }
        }

子序列问题

300.最长递增子序列

在这里插入图片描述
思路:

我们设定的dp[i]为末尾为 nums[i] 的序列长度
遍历

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        if(n<=1) return n;
        vector<int>dp(n+1,1);

        int res = 1;

        // dp[i] 代表 当最长子序列末尾为 nums[i] 的最长子序列
        // 填充 dp 数组
        for(int i = 1; i<n;i++){
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]){
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            res = max(res,dp[i]);
        }
        return res;
    }
};

674. 最长连续递增序列

在这里插入图片描述
思路:

我们是需要连续的,核心代码如下
if(nums[i]>nums[i-1]){
dp[i] = dp[i-1]+1;
}
dp[i] 代表最长子序列末尾为 nums[i] 这个数

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        int n = nums.size();
        if(n<=1) return n;
        vector<int>dp(n+1,1);

        int res = 1;
        for(int i = 1;i<n;i++){
            if(nums[i]>nums[i-1]){
                dp[i] = dp[i-1]+1;
            }
            res = res>dp[i]?res:dp[i];
        }
        return res;
    }
};

718. 最长重复子数组

在这里插入图片描述
思路:

dp[i][j] 代表什么?代表 以 nums1[i-1] 和 nums2[j-1] 结尾的最长公共子数组的长度

完整代码:

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size();
        int m = nums2.size();
        vector<vector<int>> dp(n+1,vector<int>(m+1,0));
        int res = 0;

        for(int i = 1;i<=n;i++){
            for(int j = 1;j<=m;j++){
                if(nums1[i-1] == nums2[j-1]){
                    dp[i][j] = dp[i-1][j-1]+1;
                    res  = max(res,dp[i][j]);
                }else{
                    dp[i][j] = 0;
                }
            }
        }
        return res;
    }
};

1143.最长公共子序列

在这里插入图片描述
思路:

dp[i][j] = dp[i-1][j-1] + 1;
如果 text1[i-1] 和 text2[j-1] 相同,说明当前两个字符可以作为最长公共子序列的一部分。此时,dp[i][j] 应该是不包括这两个字符的子序列长度 dp[i-1][j-1] 加上这两个匹配的字符(即加1)

dp[i][j] = max(dp[i][j-1],dp[i-1][j]);
dp[i-1][j]:这代表包含 text1 中前 i-1 个元素和 text2 中前 j 个元素的最长公共子序列。这意味着我们正在考虑忽略 text1[i-1] 的情况。
dp[i][j-1]:这代表包含 text1 中前 i 个元素和 text2 中前 j-1 个元素的最长公共子序列。这意味着我们正在考虑忽略 text2[j-1] 的情况。

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n = text1.size();
        int m = text2.size();
        vector<vector<int>> dp(n+1,vector<int>(m+1,0));

        int res = 0;

        for(int i = 1;i<=n ;i++){
            for(int j= 1;j<=m;j++ ){
                if(text1[i-1] == text2[j-1]){
                    dp[i][j] = dp[i-1][j-1] + 1;
                    //res = max(res,dp[i][j]);
                }else{
                    dp[i][j] = max(dp[i][j-1],dp[i-1][j]);
                }
                res = max(res,dp[i][j]);
            }
        }
        return res;
    }
};

1035.不相交的线

在这里插入图片描述

class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size();
        int m = nums2.size();
        vector<vector<int>> dp(n+1,vector<int>(m+1,0));

        int index = 1;
        int res = 0;
        for(int i = 1;i<=n;i++){
            for(int j = index;j<=m;j++){
                if(nums1[i-1] == nums2[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[n][m];
    }
};

53. 最大子序和

在这里插入图片描述
思路:

dp[i] 表示以 nums[i] 结尾的最大子数组和
如果 dp[i - 1](即以 nums[i-1] 结尾的最大子数组和)小于0,那么任何包含 nums[i-1] 的子数组都不如 nums[i] 本身,所以从 nums[i] 开始一个新的子数组。
否则,将 nums[i] 添加到现有子数组中

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        if(n==0) return 0;
        if(n==1) return nums[0];
        vector<int>dp(n+1,0);
        dp[0] = nums[0];
        int res = dp[0]; // 初始化最大和为第一个元素
        for(int i = 1; i<n;i++){
            if(dp[i-1]<0){
                dp[i] = nums[i];
            }
            else{
                dp[i] = dp[i-1]+nums[i];
            }
            res = max(res,dp[i]);
        }
        return res;
    }
};

392.判断子序列

在这里插入图片描述
思路:

最简单的方法,双指针法,一步一步往前移动

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int n = s.size();
        int m = t.size();
        int sindex = 0;
        int tindex = 0;

        while(sindex<n && tindex<m){
            if(s[sindex] == t[tindex]){
                sindex++;
            }
            tindex++;
        }
        return sindex == n;
    }
};

思路二:动态规划

这里是引用

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int n = s.size();
        int m = t.size();
        if(n==0) return true;
        if(m==0 && n != 0) return false;
        vector<bool> dp(n+1,false);
        dp[0] = true;

        int index = 1;
        for(int i = 1;i<=n;i++){
            for(int j = index;j<=m;j++){
                if(s[i-1] == t[j-1]){
                    dp[i] = dp[i-1] && true;
                    index = j+1;
                    break;
                }
            }
        }
        return dp[n];
    }
};

115.不同的子序列

在这里插入图片描述

思路一:回溯法(会超过时间限制)

class Solution {
public:
    string path;
    int count;
    void back(string s,string t,int index){
        if(path == t){
            count++;
        }
        for(int i = index;i<s.size();i++){
            path.push_back(s[i]);
            back(s,t,i+1);
            path.pop_back();
        }
    }

    int numDistinct(string s, string t) {
        //int count = 0;
        back(s,t,0);
        return count;
    }
};

思路二:动态规划

dp[i][j] 表示考虑 s 的前 i 个字符和 t 的前 j 个字符时,t 作为 s 的子序列出现的次数。
分解为子问题
当 s[i-1] == t[j-1] 时,dp[i][j] = dp[i-1][j-1] + dp[i-1][j]。这意味着当前字符可以作为一个新的子序列的结尾(dp[i-1][j-1]),加上我们可以忽略这个字符(dp[i-1][j])。
当 s[i-1] != t[j-1] 时,dp[i][j] = dp[i-1][j],即忽略 s 的当前字符。
在这里插入图片描述

class Solution {
public:
    int numDistinct(string s, string t) {
        int n = s.size();
        int m = t.size();

        // 初始化
        vector<vector<unsigned long long >>dp(n+1,vector<unsigned long long >(m+1,0));
        for(int i = 0;i<=n;i++){
            dp[i][0] = 1;
        }

        // 填充 dp 数组
        for(int i = 1;i<=n;i++){
            for(int j = 1;j<=m;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];
                }
            }
        }
        // for(int i = 0;i<=n;i++){
        //     for(int j= 0;j<=m;j++){
        //         cout<<dp[i][j]<<"  ";
        //     }
        //     cout<<endl;
        // }
        return dp[n][m];
    }
};

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

在这里插入图片描述
思路:

初始化:
dp[i][0] = i:如果 word2 是空字符串,将 word1 转换成空字符串需要的最少操作次数就是删除所有字符,因此操作次数等于 word1 的长度。
dp[0][j] = j:同理,如果 word1 是空字符串,那么将空字符串转换成 word2 需要的最少操作次数就是插入所有 word2 的字符。

填充动态规划表:
如果当前字符匹配(word1[i-1] == word2[j-1]),则不需要额外操作,直接继承左上角的值(dp[i-1][j-1])。
如果当前字符不匹配,则需要考虑三种操作:
删word1[i - 1],最少操作次数为dp[i - 1][j] + 1。
删word2[j - 1],最少操作次数为dp[i][j - 1] + 1
同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2

class Solution {
public:
    int minDistance(string word1, string word2) {
        int n = word1.size();
        int m = word2.size();

        // 初始化
        vector<vector<int>> dp(n+1,vector<int>(m+1,0));
        for(int i = 0; i<=n;i++){
            dp[i][0] = i;
        }
        for(int j = 0 ;j<=m; j++){
            dp[0][j] = j;
        }

        // 填充数组
        for(int i = 1 ;i<=n;i++){
            for(int j= 1;j<=m;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]+1,dp[i][j-1]+1,dp[i-1][j-1]+2});
                }
            }
        }
        return dp[n][m];
    }
};

72. 编辑距离

在这里插入图片描述
思路:

dp[i][j]表示将 word1 的前 i 个字符转换成 word2 的前 j 个字符所需的最小操作数

如果 word1[i-1] 等于 word2[j-1](字符相同,无需操作):
dp[i][j] = dp[i-1][j-1]

否则(字符不同,需要一次操作):
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
dp[i-1][j] + 1: 从 word1 中删除一个字符
dp[i][j-1] + 1: 向 word1 中插入一个字符
dp[i-1][j-1] + 1: 替换 word1 中的一个字符

删除操作 (dp[i-1][j] + 1):
状态说明:dp[i-1][j] 表示将 word1 的前 i-1 个字符转换成 word2 的前 j 个字符所需的最小操作数。
操作意义:若要从 dp[i-1][j] 转变到 dp[i][j],意味着我们要将 word1 的第 i 个字符删除,使得 word1 的前 i-1 个字符匹配 word2 的前 j 个字符。因此,在 dp[i-1][j] 的基础上增加一次删除操作(+1)。

插入操作 (dp[i][j-1] + 1):
状态说明:dp[i][j-1] 表示将 word1 的前 i 个字符转换成 word2 的前 j-1 个字符所需的最小操作数。
操作意义:要从 dp[i][j-1] 转变到 dp[i][j],意味着我们需要在 word1 的第 i 个字符后插入 word2 的第 j 个字符,使得 word1 的前 i 个字符加上这个新插入的字符能匹配 word2 的前 j 个字符。因此,我们在 dp[i][j-1] 的基础上增加一次插入操作(+1)。

替换操作 (dp[i-1][j-1] + 1):
状态说明:dp[i-1][j-1] 表示将 word1 的前 i-1 个字符转换成 word2 的前 j-1 个字符所需的最小操作数。
操作意义:要从 dp[i-1][j-1] 转变到 dp[i][j],如果 word1[i-1] 不等于 word2[j-1],我们可以通过替换 word1 的第 i 个字符(实际上是 i-1 位置上的字符)为 word2 的第 j 个字符(实际上是 j-1 位置上的字符)来实现这一目标。这样 word1 的前 i 个字符就能匹配 word2 的前 j 个字符了。因此,我们在 dp[i-1][j-1] 的基础上增加一次替换操作(+1)。

class Solution {
public:
    int minDistance(string word1, string word2) {
        int n = word1.size();
        int m = word2.size();

        // 初始化
        vector<vector<int>> dp(n+1,vector<int>(m+1));
        for(int i = 0;i<=n;i++){
            dp[i][0] = i;
        }
        for(int j = 0 ;j<=m;j++){
            dp[0][j] = j;
        }

        // 填充数组
        for(int i = 1;i<=n;i++){
            for(int j = 1; j<=m;j++){
                if(word1[i-1] == word2[j-1]){
                    dp[i][j] = dp[i-1][j-1];
                }else{
                    dp[i][j] = min({dp[i][j-1]+1,dp[i-1][j]+1,dp[i-1][j-1]+1});
                }
            }
        }
        return dp[n][m];
    }
};

回文

647. 回文子串

在这里插入图片描述

class Solution {
public:
    int countSubstrings(string s) {
        int n = s.size();

        vector<vector<bool>> dp(n, vector<bool>(n, false));
        int count = 0;

        // 单字符回文
        for(int i = 0 ;i<n;i++){
            dp[i][i] = true;
            count++;
        }

        // 两个连续的字符回文
        for(int i =0;i<n-1;i++){
            if(s[i] == s[i+1]){
                dp[i][i+1] = true;
                count++;
            }
        }

        // 数组填充
        for(int len = 3;len<=n;len++){
            for(int i = 0;i<n-len+1;i++){
                int j = i+len-1;
                if(s[i] == s[j] && dp[i+1][j-1]==true){
                    count++;
                    dp[i][j]=true;
                }
            }
        }
        return count;
    }
};

516.最长回文子序列

在这里插入图片描述

dp[i][j] 为子字符串 s[i] 到 s[j] 的最长回文子序列的长度

状态转移方程
如果 s[i] 等于 s[j]:
如果字符相等,那么 dp[i][j] = dp[i+1][j-1] + 2。
如果 s[i] 不等于 s[j]:
如果字符不相等,那么考虑不包括 s[i] 或 s[j] 的情况:dp[i][j] = max(dp[i+1][j], dp[i][j-1])。

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();

        // 初始化
        vector<vector<int>>dp(n+1,vector<int>(n+1,0));
        for(int i = 0;i<n;i++){
            dp[i][i] = 1;
        }
        for(int len = 2;len<=n;len++){
            for(int i= 0;i<=n-len;i++){
                int j = i+len-1;
                if(s[i] == s[j]){
                    dp[i][j] = dp[i+1][j-1]+2;
                }
                else{
                    dp[i][j] = max(dp[i+1][j],dp[i][j-1]);
                }
            }
        }
        return dp[0][n-1];
    }
};
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值