代码随想录——动态规划

一、动态规划的解题步骤

  1. 确定dp数组以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

二、基础动态规划问题

1.斐波那契数

代码随想录——动态规划_第1张图片

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

 2.使用最小花费爬楼梯

代码随想录——动态规划_第2张图片

class Solution {
public:
    int minCostClimbingStairs(vector& cost) {
    vector dp(cost.size()+1);
    //初始化,可以自由选择从第0阶开始网上爬还是第1阶开始往上爬
    dp[0]=0;//到达第0阶需要花费0
    dp[1]=0;//到达第1阶需要花费0
    //dp[i]表示到达第i的费用
    for(int i=2;i<=cost.size();i++)
    {
        //到达第i阶有两种方法:从i-1阶往上爬一阶/从i-2阶往上爬两阶
        dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
    }
    //返回从倒数两个台阶中那个台阶跳到终点的花费低
    return dp[cost.size()];
    }
};

3.不同路径

代码随想录——动态规划_第3张图片

class Solution {
public:
    int uniquePaths(int m, int n) {
    vector> dp(m,vector(n));
    dp[0][0]=1;
   //到达每一个格子dp[i][j]的方法有两种:
   //dp[i-1][j]往下移、dp[i][j-1]往右移
    for(int i=0;i=0&&j-1>=0)
            dp[i][j]=dp[i-1][j]+dp[i][j-1];
            else if(i-1>=0)
            dp[i][j]=dp[i-1][j];
            else if(j-1>=0)
            dp[i][j]=dp[i][j-1];
        }
    }
    return dp[m-1][n-1];
    }
};

 4.不同路径||

代码随想录——动态规划_第4张图片

class Solution {
public:
    int uniquePathsWithObstacles(vector>& obstacleGrid) {
    //如果终点有障碍物,则肯定到不了,return 0
    if(obstacleGrid[obstacleGrid.size()-1][obstacleGrid[0].size()-1]==1)
    return 0;
    vector> dp(obstacleGrid.size(),vector(obstacleGrid[0].size()));
    dp[0][0]=1;//初始化
    //到达每一个格子dp[i][j]的方法有两种:
    //dp[i-1][j]往下移、dp[i][j-1]往右移【要求dp[i-1][j]、dp[i][j-1]没有障碍物】
    for(int i=0;i=0&&obstacleGrid[i-1][j]!=1)
            dp[i][j]+=dp[i-1][j];
            if(j-1>=0&&obstacleGrid[i][j-1]!=1)
            dp[i][j]+=dp[i][j-1];
        }
    }
    return dp[obstacleGrid.size()-1][obstacleGrid[0].size()-1];
    }
};

 5.不同的二叉搜索树

代码随想录——动态规划_第5张图片

//确定初始值dp[0]以及dp[1]
//节点个数为n时,需要一个根节点,因此左右两颗子树可以分到n-1个节点
//树的种类=左树的种类*右树的种类
//(左树节点个数为0、1...,右树节点个数为n-1、n-2.....)
class Solution {
public:
    int numTrees(int n) {
    vector dp(n+1);
    dp[0]=1;
    dp[1]=1;
    for(int i=2;i<=n;i++)
    {
        for(int j=0;j

6.单词划分

代码随想录——动态规划_第6张图片

//使用背包问题解决单词拆分,需要注意!!!
//一旦dp[i]表示字符串为(s.begin(),s.begin()+i)区间是可以被字典表示,则dp[i]将不能被赋值false
class Solution {
public:
    bool wordBreak(string s, vector& wordDict) {
    //dp[i]表示字符串为(s.begin(),s.begin()+i)区间是否可以被字典表示
    vector dp(s.length()+1,false);
    dp[0]=true;
    
    for(int j=0;j<=s.length();j++)
    {
        for(int i=0;i=wordDict[i].length())
            {
                string ss(s.begin()+j-wordDict[i].length(),s.begin()+j);
                //一旦dp[i]表示字符串为(s.begin(),s.begin()+i)区间是可以被字典表示,则dp[i]将不能被赋值false
                if(ss==wordDict[i]&&dp[j]==false)
                dp[j]=dp[j-wordDict[i].length()]&&true;
            }
        }
    }
    return dp[s.length()];

    }
};

7.整数拆分

代码随想录——动态规划_第7张图片

class Solution {
public:
    int integerBreak(int n) {
    vector dp(n+1);
    dp[0]=1;
    dp[1]=1;
    for(int i=2;i<=n;i++)
    {
        for(int j=1;j

 2.剪绳子|

代码随想录——动态规划_第8张图片

class Solution {
public:
    int cuttingRope(int n) {
    vector dp(n+2);
    dp[2]=1;
    dp[3]=2;
    for(int i=4;i<=n;i++)
    {
        for(int j=1;j

3.剪绳子||

代码随想录——动态规划_第9张图片

中心思想尽可能将绳子以长度 3等分为多段时,乘积最大。【不要问为啥】

切分规则:
    最优: 3 。把绳子尽可能切为多个长度为 3 的片段,留下的最后一段绳子的长度可能为 0,1,2 三种情况。
    次优: 2 。若最后一段绳子长度为 2 ;则保留,不再拆为 1+1 。
    最差: 1 。若最后一段绳子长度为 1 ;则应把一份 3+1替换为 2+2,因为 2×2>3×1。

算法流程:
    当 n≤3 时,按照规则应不切分,但由于题目要求必须剪成 m>1 段,因此必须剪出一段长度为 1 的绳子,即返回 n−1。
    当 n>3 时,求 n 除以 3 的 整数部分 a 和 余数部分 b (即 n=3a+b ),并分为以下三种情况:
        当 b=0 时,直接返回 3^{a}%1000000007;
        当 b=1 时,要将一个 1+3 转换为 2+2,因此返回 (3^{a}−1×4)%1000000007;
        当 b=2 时,返回 (3^{a}×2)%1000000007。

class Solution {
public:
    int cuttingRope(int n) {
        if(n <= 3) 
            return n - 1;
        int b = n % 3, p = 1000000007;
        long ret = 1;
        int lineNums=n/3;           //线段被我们分成以3为大小的小线段个数
        for(int i=1;i

三、背包问题

1.01背包

题目描述整体为,给定背包容量weight,物品数组,物品价值array[]={val1,val2,val3....},物品重量huge[]={weight1,weight2,weight3...}。问如何将物品装进背包,使得背包中物品的总价值最大。

1.初始化dp数组,dp[i]表示当背包容量为i时可以装的最大物品总价值;

2.遍历顺序,个人习惯,对于0/1背包问题,每个物品只能用一次的情况下,采用外物品,内背包容量的遍历方式。由于每个物品只能用一次,所以背包容量遍历要倒序;

3.推导公式:dp[i]={dp[i],dp[i-huge[j]]+val[j]}; 表示当前背包中物品的最大价值总和取决于是否添加物品j。

  • 如果添加物品j,则需要在dp[i-huge[j]]的基础上添加,dp[i-huge[j]]+val[j]
  • 如果不添加物品j,则直接保存上一次不存在物品j时,背包容量为i的结果dp[i]

1.分割等和子集

代码随想录——动态规划_第10张图片

//题目可以转换成背包容量为SUM/2的背包问题;
class Solution {
public:
    bool canPartition(vector& nums) {
    int SUM=0;
    for(int i=0;i dp(SUM/2+1,0);
    for(int i=0;i=nums[i];j--)
        {
            //推导公式:背包容量为j所能装的最大物品数
            dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
        }
    }
    //当容量为SUM/2时背包内物品数也为SUM/2说明可以均分
    if(dp[SUM/2]==SUM/2)
    return true;
    return false;
    }
};

 2.最后一块石头的重量||

代码随想录——动态规划_第11张图片

//将一堆石头尽可能平均分配成两堆,多出的石头即为最后会剩下的石头块
//问题转换为:将石头尽可能分配到容量大小为SUM/2的堆中,两堆石头相撞,一堆完全粉碎,另一堆剩下的即为最终保留的大小
class Solution {
public:
    int lastStoneWeightII(vector& stones) {
    int sum=0;
    for(int i=0;i dp(sum/2+1,0);
    //获得容量大小为sum/2的堆能够装下的石头总量
    for(int i=0;i=stones[i];j--)
        {
            dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
        }
    }
    return sum-2*dp[sum/2];
    }
};

3.目标和

代码随想录——动态规划_第12张图片

//问题转换,转换成容量为(SUM-target)/2,物品为nums[i]的背包问题
//dp[i]表示背包容量为i时,物品的选择组合种数
class Solution {
public:
    int findTargetSumWays(vector& nums, int target) {
    int SUM=0;
    for(int i=0;iSUM)
    return 0;
    //情况二:原数组元素之和-目标值后的数不能均分,应该排除
    if((SUM-target)%2!=0)
    return 0;
    //情况三:转换成容量为(SUM-target)/2,物品为nums[i]的背包问题
    //dp[i]表示背包容量为i时,物品的选择组合种数
    vector dp((SUM-target)/2+1);
    dp[0]=1;
    for(int i=0;i=nums[i];j--)
        {
            dp[j]+=dp[j-nums[i]];
        }
    }
    return dp[(SUM-target)/2];
    }
};

4.一和零

代码随想录——动态规划_第13张图片

//这个问题实质上也是背包问题,只不过是二维背包问题【注意!!!!】
//一维背包问题:物品数组(只有A类属性)、背包容量(只有A类容量限制)
//二维背包问题:物品数组(既有A类属性又有B类属性)、背包容量(既有A类容量限制又有B类容量限制)
class Solution {
public:
    vector get01(string s)
    {
        vector array(2);
        for(int i=0;i& strs, int m, int n) {
    //dp[i][j]表示容量0为i,容量1为j的最大子集数量数
    vector> dp(m+1,vector(n+1));
    dp[0][0]=0;
    for(int k=0;k array=get01(strs[k]);
        //推导公式只是将一维的dp[i]=max(dp[i],dp[i-strs[k]]+1)变成二维的
        for(int i=m;i>=array[0];i--)//0
        {
            for(int j=n;j>=array[1];j--)//1
            {
                dp[i][j]=max(dp[i][j],dp[i-array[0]][j-array[1]]+1);
            }
        }
    }
    return dp[m][n];
    }
};

2.完全背包

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

由于每个物品可以无限次放入,所以内循环遍历是正序遍历。

求使得背包中物品总价值最大的集合数量,根据集合的类型分为两种:

  • 如果所求的是组合(无顺序要求),则外循环为物体,内循环为背包容量;
  • 如果所求的是排列(有顺序要求),则外循环为背包容量,内循环为物体;

一、完全背包——排列

1.爬楼梯

代码随想录——动态规划_第14张图片

//完全背包问题求排列
class Solution {
public:
    int climbStairs(int n) {
    //dp[i]为爬上i层楼又多少种方法
    vector dp(n+1);
    dp[0]=1;
    vector array{1,2};
    for(int i=0;i<=n;i++)
    {
        for(int j=0;j<2;j++)
        {
            if(i>=array[j])
            //使用累加表示!!!!!!!!!
            //到达d[i]没有使用物品array[j]的方法数+从dp[i-array[j]]使用物品array[j]的方法数
            dp[i]+=dp[i-array[j]];
        }
    }
    return dp[n];
    }
};

 2.组合总和IV

代码随想录——动态规划_第15张图片

//【注意!!!注意!!!注意!!!】
//在用动态规划求解数组中元素组合种类个数时
//如果所求的是组合(无顺序要求),则外循环为物体,内循环为背包容量
//如果所求的是排列(有顺序要求),则外循环为背包容量,内循环为物体
class Solution {
public:
    int combinationSum4(vector& nums, int target) {
    vector dp(target+1);
    dp[0]=1;//类似于这种确定组合数的题目,初始值dp[0]通常为1,不确定的话可以画表推理
    for(int i=0;i<=target;i++)
    {
        for(int j=0;j=nums[j]&&dp[i]

2、完全背包——组合(求组合的数量)

1.零钱兑换

代码随想录——动态规划_第16张图片

class Solution {
public:
    int change(int amount, vector& coins) {
    //dp[j]:总金额为j时,硬币组合数
    vector dp(amount+1);
    dp[0]=1;
    for(int i=0;i大
        for(int j=coins[i];j<=amount;j++)
        {
            //dp[j]表示还没有硬币i的时候,要组合成总金额为j的组合数量
            //dpdp[j-coins[i]]表示有了硬币i时,使用该硬币组合成总金额为j的组合数量
            //总数即为硬币[0,i]可以组成总金额为j的组合数
            dp[j]=dp[j]+dp[j-coins[i]];
        }
    }
    return dp[amount];
    }
};

2、完全背包——组合(求组合中,元素数量最少的组合有多少个元素组成)

1.完全平方数

代码随想录——动态规划_第17张图片

//这道题与上一道零钱兑换一样
//求装满背包的最少物品数,尤其需要注意dp数组的初始化,以及推导公式的使用条件
class Solution {
public:
    int numSquares(int n) {
    //dp[i]表示,和为i的完全平方数最少个数
    //【初始化最要命】
    //初始化dp[i]==INT_MAX说明何为i的完全平方数组合不存在
    vector dp(n+1,INT_MAX);
    dp[0]=0;//表示和为0的完全平方数个数为0
    for(int i=1;i*i<=n;i++)
    {
        for(int j=i*i;j<=n;j++)
        {
    //不需要像"零钱兑换"那样存在下面的条件,因为平方数和为j-i*i的平方数组合总是存在的(毕竟有1)
    // if(dp[j-i*i]!=INT_MAX)
            dp[j]=min(dp[j],dp[j-i*i]+1);
        }
    }
    return dp[n];

    }
};

 2.零钱兑换

代码随想录——动态规划_第18张图片

//完全背包的问题,每个硬币可以重复使用
class Solution {
public:
    int coinChange(vector& coins, int amount) {
    //dp[i]表示,总金额为i时可以最少用dp[i]各硬币表示
    //【下面两行初始化最重要!!!】
    //首先将dp数组初始化为INT_MAX。表示金额总数为i时,无法用硬币组合表示
    //dp[0]=0表示,初始化总金额为0时,需要0个硬币表示(也可以画表格推导)
    vector dp(amount+1,INT_MAX);
    dp[0]=0;
    
    //求的时组合不是排列,所以内外循环顺序可以颠倒
    for(int i=0;i

四、打家劫舍

1.打家劫舍

代码随想录——动态规划_第19张图片

class Solution {
public:
    int rob(vector& nums) {
    //如果只有一间房屋,则直接返回
    if(nums.size()==1)
    return nums[0];
    //dp[i]表示偷到第i所房屋时,已经偷窃到的最大金额总数
    vector dp(nums.size());
    //初始化dp[0]\dp[1]
    dp[0]=nums[0];
    dp[1]=max(nums[0],nums[1]);
 
    for(int i=2;i

2.打家劫舍||

代码随想录——动态规划_第20张图片

注意由于所有的房屋都被围城了一圈,所以在考虑将第一间房屋纳入偷窃的范围,则最后一间房屋就不能纳入偷窃的范围(不管偷窃金额最大情况下最后一间房屋偷没偷)。同理,考虑将最后一间房屋纳入偷窃的范围,则第一间房屋就不能纳入偷窃的范围。

//由于房屋是成环状排列,因此分两种情况考虑:
//情况一:考虑可能包含首元素,但一定不包含尾元素;
//情况二:考虑可能包含尾元素,但一定不包含首元素;
class Solution {
public:
    int robRange(vector& nums,int start,int end)
    {
        if(end-start+1==1)
        return nums[start];       
        vector 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];
    }
    int rob(vector& nums) {
    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);
    }
};

3.打家劫舍|||

代码随想录——动态规划_第21张图片

这道题不算真正意义上的动态规划,应该归类为贪心算法。

对于每一个节点root都有两个状态:
状态一:偷取当前节点的房屋能够获得的最大金额root[1];
状态二:不偷取当前节点的房屋能够获得最大的金额root[0];

  • 偷当前节点,则在两个叶子节点中就只能选择不偷的那个数组元素 :val1=cur->val+left[0]+right[0];
  • 不偷当前节点,而是偷当前节点的子节点,则在两个叶子节点中可以选择偷或者不偷的那个数组元素: val2=max(left[1],left[0])+max(right[1],right[0]);
//【注意!!!注意!!!注意!!!】二叉树的dp动态规划,采用后序遍历
//对于每个节点都设置一个二元数组,分别存储是否偷取当前节点金额所得到的金额数
//情况一:如果偷取当前节点的金额,则在两个叶子节点上只能分别获取不包含左右两个叶子节点金额的那部分金额
//情况二:如果不偷取当前节点的金额,则在两个叶子节点上既可以对是否包含叶子节点金额的二元数组元素取选取金额较大的数组元素
class Solution {
private:
    vectorbacktravle(TreeNode* cur)
    {
        if(cur==nullptr)
        return {0,0};//叶子节点,不管是在该节点上偷与不偷,偷到的金额都是0
        vector left=backtravle(cur->left);
        vector right=backtravle(cur->right);
        int val1;
        int val2;
        //偷当前节点,则在两个叶子节点中就只能选择不偷的那个数组元素
        val1=cur->val+left[0]+right[0];
        //不偷当前节点,而是偷当前节点的子节点,则在两个叶子节点中可以选择偷或者不偷的那个数组元素
        val2=max(left[1],left[0])+max(right[1],right[0]);
        return {val2,val1};
    }
public:
    int rob(TreeNode* root) {
    vector result=backtravle(root);
    return max(result[0],result[1]);
    }
};

五、股票问题

采用动态规划解决买卖股票问题有一个通用的解法,具体如下:

1.dp数组,根据当天股票买卖人的状态设定dp数组的维数,dp[i][j]表示第i股票买卖人在j状态下的最大利润。
2.分析造成dp[i][j]即第i天状态j的情况有哪些,去其中利润最大的情况。
3.初始化dp[0][j]根据实际状态进行初始化。

1.只允许进行单次买卖

代码随想录——动态规划_第22张图片

//dp[i][0]表示第i天处于买入状态下,获取的最大利润
//dp[i][1]表示第i天处于卖出状态下,获取的最大利润
class Solution {
public:
    int maxProfit(vector& prices) {
    vector> dp(prices.size(),vector(2));
    dp[0][0]=-prices[0];
    dp[0][1]=0;
    for(int i=1;i

 2.不限股票买卖次数

代码随想录——动态规划_第23张图片

//dp[i][0]表示第i天处于买入状态下,获取的最大利润
//dp[i][1]表示第i天处于卖出状态下,获取的最大利润
class Solution {
public:
    int maxProfit(vector& prices) {
    vector> dp(prices.size(),vector(2));
    dp[0][0]=-prices[0];
    dp[0][1]=0;
    for(int i=1;i

 3.指定股票买卖次数

代码随想录——动态规划_第24张图片

//dp[i][j]表示第i天,j状态下的利润,在第i天的时候,此时的交易一共存在如下五种状态:
//无操作状态dp[i][0],说明第i-1天也是无操作状态,此时dp[i][0]=dp[i-1][0];
//第一次购入股票状态dp[i][1],造成这个状态有两种情况:1.延续了昨天的第一次购买股票状态;2.今天刚购买第一支股票,昨天是无操作状态,此时dp[i][1]=max(dp[i-1][1],dp[i][0]-prices[i]);
//第一次卖出股票状态dp[i][2],造成这个状态有两种情况:1.延续了昨天第一次卖出股票的状态;2.今天刚卖出第一支股票,昨天是第一次购入股票状态,此时dp[i][2]=max(dp[i-1][2],dp[i][1]+prices[i]);
//第二次购入股票状态dp[i][3],造成这个状态有两种情况:1.延续了昨天的第二次购买股票状态;2.今天刚购买第二支股票,昨天是第一次卖出股票状态,此时dp[i][3]=max(dp[i-1][3],dp[i][2]-prices[i]);
//第二次卖出股票状态dp[i][4],造成这个状态有两种情况:1.延续了昨天第二次卖出股票的状态;2.今天刚卖出第二支股票,昨天是第二次购入股票状态,此时dp[i][4]=max(dp[i-1][4],dp[i][3]+prices[i]);
class Solution {
public:
    int maxProfit(vector& prices) {
    //dp[i][j]表示第i天,j状态下的利润
    vector> dp(prices.size(),vector(5));
    //初始化
    dp[0][0]=0;//第1天无操作,利润为0
    dp[0][1]=-prices[0];//第一天第一次买入,利润为-prices[0]
    dp[0][2]=0;//第一天第一次卖出,当天完成第一次买入卖出,所以利润也是0
    dp[0][3]=-prices[0];//第一天第二次买入,情况为当天完成了第一次的买入卖出,所以利润为-prices[0]
    dp[0][4]=0;//第二天卖出,当天完成第二次买入卖出,所以利润也是0
    for(int i=1;i

 4.卖出股票含冷冻期

代码随想录——动态规划_第25张图片

//买入股票状态dp[i][0]:上一次可能是买入状态,也可能是冷冻状态,但是绝不可能是卖出状态
//卖出股票状态dp[i][1]:上一次可能是买入状态,也可能是卖出状态,但是绝不可能是冷冻状态
//冷冻期状态dp[i][2]:上一次必然是卖出状态
class Solution {
public:
    int maxProfit(vector& prices) {
    vector> dp(prices.size(),vector(3));
    dp[0][0]=-prices[0];//第一天买入股票,利润为-prices[0]
    dp[0][1]=0;//第一天买出股票,当天买入卖出,利润为0
    dp[0][2]=0;//第一天就冷冻,所以利润为0
    for(int i=1;i

 5.含手续费的股票买卖

代码随想录——动态规划_第26张图片

//股票买入状态dp[i][0],造成这个状态的原因有两种情况:1.第i天刚购入股票,第i-1天处于股票卖出状态 2.维持第i-1天的购入股票状态
//股票卖出状态dp[i][1],造成这个状态的原因有两种情况:1.第i天刚卖出股票,第i-1天处于购入股票状态 2.维持第i-1天的股票卖出状态
class Solution {
public:
    int maxProfit(vector& prices, int fee) {
    vector> dp(prices.size(),vector(2));
    dp[0][0]=-prices[0];//第一天买入股票,利润为-prices[0]
    dp[0][1]=0;//第一天卖出股票,初始化利润为0【因为即使当天买入卖出还要搭上手续费,所以最大利润就是0,不会是负数】
    for(int i=1;i

六、子序列问题

1.公共子序列问题

子序列是原数组中按照索引顺序但是非连续的集合。

1.最长递增子序列

代码随想录——动态规划_第27张图片

注意:dp数组初始化dp[i]表示以i为终点的最长递增子序列的长度,因此最终整个字符串的最长递增子序列不是dp[s.size()-1],因为可能最长递增系序列的终点不是原数组末尾元素。所以原数组的最长递增子序列的长度需要在求解dp[i]的过程中取最大值。

动态规划遍历方法:针对每一个nums[i]都会寻找[0:i]之间以小于nums[i]结尾的最长递增子序列长度,从而获得以nums[i]结尾的最长递增子序列长度

class Solution {
public:
    int lengthOfLIS(vector& nums) {
    //dp[i]表示以dp[i]为结尾的最大递增子序长度
    //初始化dp[i]=1,因为只有1个数也长度为1
    vector dp(nums.size(),1);
    int M=0;
    //针对每一个nums[i]都会寻找[0:i]之间以小于nums[i]结尾的最长递增子序列长度
    //从而获得以nums[i]结尾的最长递增子序列长度
    for(int i=1;i

 2.最长递增子序列的个数

代码随想录——动态规划_第28张图片

本题要点,组织两个数组:dp,count
其中dp[i]表示以nums[i]结尾的最长递增子序列长度;
其中count[i]表示以nums[i]结尾的最长递增子序列个数;
计算过程中时刻更新整个数组中最长递增子序列长度,最后用这个长度去count数组匹配,求和;

//本题要点,组织两个数组:dp,count
//其中dp[i]表示以nums[i]结尾的最长递增子序列长度
//count[i]表示以nums[i]结尾的最长递增子序列个数
//在计算过程中,时刻更新整个数组中最长递增子序列长度,最后用这个长度去count数组匹配,求和
class Solution {
public:
    int findNumberOfLIS(vector& nums) {
    //maxLenth定义为最长递增子序列的长度
    int maxLength=1;
    //dp[i]表示[0:i]区间内最长连续递增的子序列长度
    //初始化为1,所有元素本身就是一个长度为1的子序列
    vector dp(nums.size(),1);
    //其中count[i]表示以元素nums[i]结尾的最长递增子序列个数
    vector count(nums.size(),1);
    for(int i=1;i

3.最长公共子序列

代码随想录——动态规划_第29张图片

//此处题目要求的是【最大重合子序列,不要求连续!!!】
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
    //dp[i][j]表示text1[0:i]text2[0:j]的公共最长子数组的长度
    vector> dp(text1.length()+1,vector(text2.length()+1));
    for(int i=0;i

4.不相交的线

代码随想录——动态规划_第30张图片

求不相交的线的条数,与求nums1和nums2的最大公共子序列的题目方法一致,只不过是将公共子序列的长度换成线的条数。

5.判断子序列

代码随想录——动态规划_第31张图片

与求最大公共子序列题目方法一致,只不过是判断最大公共子序列是否等于较短的那个字符串的长度。

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

代码随想录——动态规划_第32张图片

与求最大公共子序列的方法一样,只不过本题是求出最大公共子序列后,要删除的步数=word1.length()+word2.length()-2*最大公共子序列长度。

2.子数组和问题

代码随想录——动态规划_第33张图片

注意:dp[i]表示字符串dp[0:i]的以元素nums[i]为结尾的最大数组和,所以整个数组中的最大子数组和需要在求dp[i]的过程中对比保存

class Solution {
public:
    int maxSubArray(vector& nums) {
    //dp[i]表示遍历到nums的第i个点时,此时手里保留的连续子数组之和。
    //【是手中保留的,不是最大连续子数组之和,只要手中保留的是正数,就可以为后续提供助力!!!!】
    vector dp(nums.size());
    dp[0]=nums[0];
    int result=dp[0];
    for(int i=1;i=0,他会对dp[i]形成助力,所以dp[i]将对dp[i-1]进行累积
            dp[i]=nums[i]+dp[i-1];
        }
        //最大和要在过程中获得
        result=max(dp[i],result);
    }
    return result;
    }
};

3.不同的子序列

代码随想录——动态规划_第34张图片

对于上述问题乍一看无从下手,无法直接获得dp数组的推导公式。关于这类推导公式无法直接看出的或者无法对dp数组初始化的问题,有以下处理方法:

1.确定dp数组的中dp[i][j]代表的意思;
2.画表格,列出几行例子去寻找规律;

//关于这类推导公式无法直接看出的或者无法对dp数组初始化的问题,有以下处理方法
//1.确定dp数组的中dp[i][j]代表的意思
//2.画表格,列出几行例子去寻找规律
class Solution {
public:
    int numDistinct(string t, string s) {
    //dp[i][j]表示字符串s[0:i]在字符串t[j]中作为子序列出现的次数
    vector> dp(s.length()+1,vector(t.length()+1));
    //dp数组初始化,空字符串在字符串t[j]中出现的次数为1
    for(int i=0;i<=t.length();i++)
    {
        dp[0][i]=1;
    }
    for(int i=0;i

 4.连续子序列

代码随想录——动态规划_第35张图片

//此处用的是动态规划
class Solution {
public:
    int strStr(string haystack, string needle) {
    vector> dp(needle.length()+1,vector(haystack.length()+1,0));
    for(int i=0;i<=needle.length();i++)
    dp[i][0]=1;
    for(int j=0;j<=haystack.length();j++)
    dp[0][j]=1;

    for(int i=0;i

七、编辑距离

代码随想录——动态规划_第36张图片

1.dp数组初始化,dp[i+1][j+1]表示,word1[0:i]转换成word2[0:j]所使用的最少操作数;
当word2的长度为0时,操作数就等于word1的长度(都是删除操作),dp[i][0]=i;
当word1的长度为0时,操作数就等于word2的长度(都是删除操作),dp[0][j]=j;

2.推导公式分为下面几种情况:
(1)如果新多出来的两个字符相等,则其操作数与dp[i-1][j-1]时一样
(2)如果新多出来的两个字符不相等,则有如下三种处理方法:
         1.在word1[i-1]的基础上在word1[i]位置上增加一个与word2[j]一样的字符;
         2.在word2[j-1]的基础上在word2[j]位置上增加一个与word1[i]一样的字符;
         3.将现在word1[i-1]word2[j-1]的基础上将字符word1[i]word2[j]换成一样的;

class Solution {
public:
    int minDistance(string word1, string word2) {
    //dp[i+1][j+1]表示,word1[0:i]转换成word2[0:j]所使用的最少操作数
    vector> dp(word1.length()+1,vector (word2.length()+1));
    //当word2的长度为0时,操作数就等于word1的长度(都是删除操作)
    for(int i=0;i<=word1.length();i++)
    {
        dp[i][0]=i;
    }
    //当word1的长度为0时,操作数就等于word2的长度(都是删除操作)
    for(int j=0;j<=word2.length();j++)
    {
        dp[0][j]=j;
    }

    for(int i=0;i

八、回文字符串

用动态规划解决回文字符串主要有两类问题:判断字符串中的回文字符串的数量(连续)、计算字符串中最大回文子序列的长度(不连续)。

题目类型一:判断字符串中的回文字符串的数量(连续)

代码随想录——动态规划_第37张图片

1.dp数组的初始化,i表示截取的字符串头,j表示截取的字符串尾,dp[i][j]表示字符串s[i : j]是否为回文子序列,如果是dp[i][j]=1,否则dp[i][j]=0;

2.推导公式有下面三种情况:
(1)s[i]=s[j]即字符串两端的字符相等,当字符串长度为1或2时,判定该字符串时回文字符串dp[i][j]=1;
(2)s[i]=s[j]即字符串两端的字符相等,当字符串长度大于2时,字符串s[i : j]是否为回文字符串取决于字符串s[i+1:j-1],所以dp[i][j]=dp[i+1][j-1];
(3)s[i]!=s[j]如果一个字符串的两端都不相等,则这个字符串肯定不是回文字符串dp[i][j]=0;

3.遍历顺序,因为dp[i][j]状态取决于dp[i+1][j-1],所以遍历顺序一定是一个正序一个倒序。并且需要保证dp[i+1][j-1]先于dp[i][j]被处理,所以是i倒序,j正序

class Solution {
public:
    int countSubstrings(string s) {
    //dp[i][j]表示s[i:j]是否为回文子串,是则dp[i][j]=1,否则dp[i][j]=0
    vector> dp(s.length(),vector(s.length(),0));
    int count=0;
    //【遍历顺序是最关键的!!!!】
    //外循环为截取的字符串尾,内循环为截取的字符串头
    for(int i=s.length()-1;i>=0;i--)
    {
        //字符串的头一定要大于等于字符串的尾,所以初始条件为j=i
        for(int j=i;j

题目类型二:计算字符串中最大回文子序列的长度(不连续)

代码随想录——动态规划_第38张图片

1.初始化dp数组,i表示截取的字符串头,j表示截取的字符串尾,dp[i][j]表示字符串s[i : j]的最大回文子序列的长度。

2.推导公式,判断字符串s[i : j]中最大回文子序的长度有如下几种情况:
(1)当s[i]=s[j],s[i:j]长度为1或2时,则直接给定回文子串的长度为1或2,dp[i][j]=j-i+1;
(2)当s[i]=s[j],字符串长度大于2时,字符串s[i:j]中最大回文子序的长度取决于字符串s[i+1:j-1],dp[i][j]=dp[i+1][j-1]+2;
(3)当字符串两端的元素不相等时,s[i : j]字符串回文子序列的长度可以继承的状态有两种去除字符串头s[i]、去除字符串尾s[j],所以dp[i][j]=max(dp[i][j-1],dp[i+1][j]);

3.遍历顺序,因为dp[i][j]状态取决于dp[i+1][j-1],所以遍历顺序一定是一个正序一个倒序。并且需要保证dp[i+1][j-1]先于dp[i][j]被处理,所以是i倒序,j正序

class Solution {
public:
    int longestPalindromeSubseq(string s) {
    //dp[i][j]表示s[i:j]中子序列回文字符串最大长度
    vector> dp(s.length(),vector(s.length()));
    //【遍历顺序是最关键的!!!!】
    //外循环为截取的字符串尾,内循环为截取的字符串头
    for(int i=s.length()-1;i>=0;i--)
    {
        //字符串的头一定要大于等于字符串的尾,所以初始条件为j=i
        for(int j=i;j

3.最长回文子串

代码随想录——动态规划_第39张图片

动态规划,确定字符串s[i : j]是否为回文子串,并时刻更新最大回文子串的长度和首尾索引,最后在原字符串中截取该段即为原字符串的最长回文子序。

class Solution {
public:
    string longestPalindrome(string s) {
    int length=1;
    int start=0;
    int end=0;
    //dp[i][j]表示字符串s[i:j]是否是回文字符串
    vector> dp(s.length(),vector (s.length(),0));
    for(int j=0;j1&&dp[i+1][j-1]==1||j-i==1)
                {                  
                    dp[i][j]=1;
                    //时刻记录回文子串的最大长度,以及该子串的首尾位置
                    if(j-i+1>=length)
                    {
                        length=j-i+1;
                        start=i;
                        end=j;
                    }
                }       
            }
        }
    }
    //根据最大子串的首尾位置获取最大子串。
    string result;
    for(int i=start;i<=end;i++)
    result+=s[i];
    return result;
    }
};

分割回文子串!!!!!!!!!

代码随想录——动态规划_第40张图片

//步骤一:通过动态规划,获取原字符串s中所有的回文字符串组合,dp[i][j]==0表示s[i:j]是回文子串
//步骤二:通过动态规划,设置新的DP数组,DP[i]表示字符串s[0:i]的最少分割次数。
class Solution {
    vector> dp;
public:
    //步骤一:通过动态规划,获取原字符串s中所有的回文字符串组合,dp[i][j]==0表示s[i:j]是回文子串
    void step1(string s,vector> &dp)
    {
        for(int j=0;j1&&dp[i+1][j-1]==1)
                    dp[i][j]=1;
                }
            }
        }
    }
    int minCut(string s) {
    dp=vector>(s.length(),vector(s.length(),0));
    step1(s,dp);

    //DP[i]表示字符串s[0:i]的最少分割次数。
    vector DP(s.length());
    //先假设将每个单独的字符都分割出来,所以DP[i]=i
    for(int i=0;i

 参考代码随想录

你可能感兴趣的:(代码随想录,动态规划,算法,c++)