数据结构与算法之动态规划 做题思路总结 附详解

个人学习 代码随想录 的做题笔记,如果对你有帮助,请一键三连(点赞+收藏+关注)哦~ 感谢支持!欢迎各位在评论区与博主友好讨论!缓慢更新中……



一般从以下几点分别考虑:

  1. 子状态:
  2. 递推状态:
  3. 初始值:
  4. 遍历顺序:
  5. 返回结果:

1.斐波那契数列:

0,1,1,2,3……

求前两个数之和可得此数列。

  1. 子状态:F(n)
  2. 递推状态:F(n)=F(n-1)+F(n-2)
  3. 初始值:F(0)=0,F(1)=F(2)=1
  4. 遍历顺序:一维数组从左到右
  5. 返回结果:F(N)
public class Solution {
  public int Fibonacci(int n) {
    // 初始值
    if(n <= 0)
      return 0;
    int[] a = new int[n + 1];
    a[0] = 0;
    a[1] = 1;
    for(int i = 2; i <= n; ++i)
   {
      // F(n)=F(n-1)+F(n-2)
      a[i] = a[i - 1] + a[i - 2];
   }
    return a[n];
 }
}

实际上实现这个数列用a,b,result三个值就可以了。

比如:

int a = 1;
int b = 1;
int result = 0;
for (int i = 3; i <= n; i++)//0,1,1
{
    // F(n)=F(n-1)+F(n-2)
    result = a + b;
    // 更新值
    a = b;
    b = result;
}

2.最小路径和:

依题意,到F(i,j)能从上面或左面,需要前面的值才能得出接下来的值,建立二维数组存储对应格子的最短路径和 

  1. 子状态:从(0,0)到达(1,0),(1,1),(2,1),...(m-1,n-1)的最短路径。F(i, j): 从(0,0)到达F(i, j)的最短路径
  2. 递推状态:F(i,j)=F(i-1, j)+F(i, j-1)
  3. 初始值:F(0, j)=1,F(i, 0)=1   即最左边一列到其他列只能往右,最上边一行到其他行只能往下
  4. 遍历顺序:从左到右按顺序即可
  5. 返回结果:F(m-1,n-1)
class Solution {
public:
    
    int minPathSum(vector<vector<int> >& grid) {
        // write code here
        if(grid.size()==0)
            return 0;
        int m=grid.size();
        int n=grid[0].size();
        
        for(int i=1;i<m;i++)
            grid[i][0]=grid[i-1][0]+grid[i][0];
        for(int j=1;j<n;j++)
            grid[0][j]=grid[0][j-1]+grid[0][j];
        
        for(int i=1;i<m;i++)
        {
            for(int j=1;j<n;j++)
            {
                grid[i][j]=min(grid[i-1][j],grid[i][j-1])+grid[i][j];
            }
        }
        return grid[m-1][n-1];
    }
};

有障碍版本:

自己写的繁琐: 

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        if(obstacleGrid[obstacleGrid.size()-1][obstacleGrid[0].size()-1]==1)
            return 0;
        vector<vector<int>>a(obstacleGrid.size(),vector<int>(obstacleGrid[0].size(),0));
        for(int i=0;i<obstacleGrid.size()&&obstacleGrid[i][0]==0;i++)
        {
            a[i][0]=1;
        }
        for(int i=0;i<obstacleGrid[0].size()&&obstacleGrid[0][i]==0;i++)
        {
            a[0][i]=1;
        }

        for(int i=1;i<obstacleGrid.size();i++)
        {
            for(int j=1;j<obstacleGrid[0].size();j++)
            {
                if(obstacleGrid[i-1][j]==0&&obstacleGrid[i][j-1]==0)
                {
                    a[i][j]=a[i-1][j]+a[i][j-1];
                }
                else if(obstacleGrid[i-1][j]==0&&obstacleGrid[i][j-1]==1)
                    a[i][j]=a[i-1][j];
                else if(obstacleGrid[i][j-1]==0&&obstacleGrid[i-1][j]==1)
                    a[i][j]=a[i][j-1];
            }
        }

        return a[obstacleGrid.size()-1][obstacleGrid[0].size()-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));
        for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) 
            dp[i][0] = 1;
        for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) 
            dp[0][j] = 1;
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {

                if (obstacleGrid[i][j] == 1) 
                    continue;

                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m - 1][n - 1];
    }
};

 for循环跳出,进行下一次循环的条件:

if (obstacleGrid[i][j] == 1) continue;

即如果当前格子有障碍,就不算谁能到达它了,此障碍需要被略过。 


3.爬楼梯问题

使用最小花费爬楼梯,带权值楼梯。

依旧是从前一个台阶或前两个台阶可以到达当前台阶。

  1. 子状态:F(i),到达第 i阶需要的最小体力,c(i)原题给的台阶对应体力
  2. 递推状态:F(i)=min(F(i-1)+F(i-2))+c(i)
  3. 初始值:F(1)=c(1),F(0)=c(0),
  4. 遍历顺序:一维数组从左到右
  5. 返回结果:由于题意是遍历完体力楼梯数组后还要再上一个台阶,此时对于楼顶来说是不花体力的,但要从前一个台阶和前两个台阶中选出体力最小的。
    min(F[c.size() - 1], F[c.size() - 2]);

4. 整数拆分

  1. 子状态:F(i),数字i拆分后对应的最大的乘积
  2. 递推状态:F(i)=max(F[i],max(j*(i-j),F[i-j]*j)); max(上一次计算的乘积,拆分成两个数,拆分成多个数)
  3. 初始值:F(2)=1; 题意n从2开始
  4. 遍历顺序:一维数组从左到右,i=3,j=1; F(2)已经初始化了,第一次循环计算i-j刚好可得2
  5. 返回结果:F(n);
class Solution {
public:
    int integerBreak(int n) {
        vector<int>a(n+1);
        a[2]=1;
        for(int i=3;i<=n;i++)
        {
            for(int j=1;j<i-1;j++)
            {
                a[i]=max(a[i],max(j*(i-j),a[i-j]*j));
            }
        }
        return a[n];
    }
};

5.不同的二叉搜索树

1个节点:1个二叉树

2个节点:2个二叉树

3个节点:5 个二叉树

 

可以发现:

1为头结点:2个右子树的布局与2个节点时相同;

2为头结点:左子树和右子树与1个节点时相同;

3为头结点:2个左子树的布局与2个节点时相同

此时便可想到一种递推关系:

F(1)=1,

F(2)=F(1)*F(0)+F(1)*F(0)=2, 即左子树为1节点*右子树0个节点的树的个数+左子树为0节点*右子树1个节点的树的个数

F(3)=F[2] * F[0] + F[1] * F[1] + F[0] * F[2]=2*1+1*1+1*2=5

模拟一下该过程可得递推公式,i,j的范围

  1. 子状态:F(i),i个节点对应的二叉树个数
  2. 递推状态:F(i)+=F(i-j)*F(j-1);

    F[i] += F[以j为头结点左子树节点数] * F[以j为头结点右子树节点数]

    j相当于是头结点的元素,从1遍历到i为止。j<=i

    递推公式:F(i)+=F(i-j)*F(j-1)j-1 :j为头结点左子树节点数量,i-j :以j为头结点右子树节点数量

  3. 初始值:F(0)=1,乘法计算
  4. 遍历顺序:一维数组从左到右
  5. 返回结果:F(n);
class Solution {
public:
    int numTrees(int n) {
        vector<int>a(n+1);
        a[0]=1;
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=i;j++)
            {
                a[i]+=a[j-1]*a[i-j];
            }
        }
        return a[n];
    }
};


 背包问题:

背包:最大容量;物品:价值、体积、每个物品的数量

01背包:每个物品的数量只有一个,放一个或者不放入背包

完全背包:每个物品的数量有无数个,放几个或者不放入背包

二维数组的01背包:

n 个物品和一个大小为 m 的背包. 给定数组 W表示每个物品的大小和数组 V 表示每个物品的

价值

  

1.子状态:f[i][j]:前i个物品放入大小为j的背包中所获得的最大价值

2.递推状态:装不下和装得下时放还是不放:

①背包容量不够不能放入第i号物品,f[i-1][j]; 

②背包容量够可以放入第i号物品,包括两种状态:放进去或者不放进去,选择一种最终价值最大的。

f[i-1][j]:不放;

f[i - 1][j - W[i]] + V[i]:先为第i号物品腾出空间,即减去该物品的容量后包内还剩多少容量,再加上i号物品的价值。

f[i][j]=max(f[i-1][j], f[i - 1][j - W[i]]  + V[i])。

3.初始值:f[i][0]=0:0~i个物品,背包容量为0时,一个物品都放不进去,价值仍为0。

f[0][j]=0:没放物品价值也为0

4.遍历顺序:双重循环,先遍历谁都行。从递推公式可得f[i][j]由(i-1,j),和(i-1,j - W[i])得到的。在左上角方向(包括正上方向)。

5.返回结果:f(N-1,m);

//动态规划部分代码实现
for (int i = 1; i < N; ++i) 
{
    for (int j = 1; j != M; ++j) 
    {

        if (A[i - 1] > j) 
        {
            result[i][j] = result[i - 1][j];
        }

        else 
        {
            int newValue = result[i - 1][j - A[i - 1]] + V[i - 1];
            result[i][j] = max(newValue, result[i - 1][j]);
        }
    }
}

一维数组01背包:

上面的算法在计算第i行元素时,只用到第i-1行的元素,所以二维的空间可以优化为一维空间。
但是如果是一维向量,需要从后向前计算,因为后面的元素更新需要依靠前面的元素未更新的值。

1.子状态:f[j]:大小为j的背包中所获得的最大价值

2.递推状态:装不下和装得下时放还是不放:

①背包容量不够不能放入第i号物品,f[[j]; 

②背包容量够可以放入第i号物品,包括两种状态:放进去或者不放进去,选择一种最终价值最大的。

f[j]:不放,容量不变;

f[ j - W[i] ] + V[i]:先为第i号物品腾出空间,即减去该物品的容量后包内还剩多少容量,再加上i号物品的价值。

f[ j ]=max( f[ j ], f[ j - W[ i ] ] + V[ i ])。

3.初始值:都初始为0

f[0][j]=0:没放物品价值也为0

4.遍历顺序:双重循环,只能先遍历物品再遍历背包容量,因为如果先遍历背包,就是在当前容量中,现有物品中选出价值最大一个的放入。倒叙遍历是保证物品i只被放入一次。如果正序遍历物品 i 就会被重复加入多次。

二维数组正序遍历是因为f[ i ][ j ]通过上一层f[i - 1][j]计算而来,本层的f[ i ][ j ]不会被覆盖。

5.返回结果:f(m);

class Solution {
public:

    int backPackII(int m, vector<int> A, vector<int> V) {
    if (A.empty() || V.empty() || m < 1) 
    {
    return 0;
    }
   
    const int N = A.size();

    const int M = m + 1;
    vector<int> result;

    result.resize(M, 0);

    for (int i = 0; i != N; ++i) 
    {
        for (int j = M - 1; j > 0; --j) 
        {

            if (A[i] > j) 
            {
                result[j] = result[j];
            }
            else
            {
                int newValue = result[j - A[i]] + V[i];
                result[j] = max(newValue, result[j]);
            }
        }
    }

    return result[m];
    }
};

6.分割等和子集

每一个数字只能选一次,有最大和的限制-->01背包存在。分割成两个子集,和相等。那直接找和为sum/2的子集就行了。

  1. 子状态:a[j],容量为 j 时的最大和
  2. 递推状态:a[j]=max(a[j],a[j-nums[i]]+nums[i]);
  3. 初始值:都为0
  4. 遍历顺序:逆序遍历 j,再写一遍原因:此递推公式与二维数组的递推公式意义一样,a[j]对应a[i-1][j],a[j-nums[i]] +nums[i]对应a[i-1][j-nums[i]] +nums[i]。逆序遍历,a[j-nums[i]]使用的是上一行(逻辑上)的值。正序遍历,a[j-nums[i]]就会被更新,接下来的a[j]使用的就不是原来的值了,应该让a[j]从未更新的值里选出最大的。

    a[j] (新)= a[j] (旧) || a[j - nums[i]] (旧)

  5. 返回结果:f(sum/2)==sum/2时;

7.最后一块石头的重量

背包最值:每次选2个石头,最终让差值最小。那么把石头分为两堆,每堆石头都选接近(所有石头总重量/2)。把所有石头放进容量为sum/2的背包,求放进去的石头的最大重量x。差值就是sum-2x。

8.目标和

背包组合:要求装满背包容量有几种方法。数组和sum,要被加的数字之和a,要被减的数字(正数)之和b,题目求的目标和 t。

a+b=sum,a-b=t。两式联立求得a=(sum+t)/2,由此可知sum+t为偶数,a也为偶数。

  1. 子状态:dp[j],装满容量j的方法
  2. 递推状态:dp[j]=dp[j]+dp[j-nums[i]]: 不选nums[i]+选nums[i]
  3. 初始值:dp[0]=1,即0容量时有一种方法,就是选0件物品
  4. 遍历顺序:内部逆序遍历
  5. 返回结果:dp[a]

9.一和零

dp[i][j],i个0和j个1时对应的最多子集。

递推:可以从放入当前子集,或者不放入,选出最大的。dp[i][j]不放入,dp[i-zero][j-one]+1在 放入后还应该有的0,1的个数对应的子集数的基础上+1。

三维数组表示:

dp[k][i][j] 表示在前 i个字符串中,使用 i 个 0 和 j 个 1 的情况下最多可以得到的字符串数量。

那去掉一个维度,最外层遍历的就是strs中k个字符串。每个字符串统计0,1个数,遍历物品。内层遍历背包容量i,j,倒序遍历。


10.零钱兑换II

看了题解中有说本题像 爬楼梯 这个题。爬楼梯扩充成每次可走1,2……m阶台阶,到达n级台阶有几种方法,是个排列问题。

每种钱可以用无数次,最终和为已给容量大小。题目给出的样例是组合而不是排列,意为 2+2+1与1+2+2是相同的。外部物品,内部背包,且物品的顺序是不变的。

物品无限次选取,而01背包只能选一次物品,保证下标j之前的dp[j]都是经过计算的就行。

  1. 子状态:dp[j],必须选取conis[i]时,装满容量 j 的方法。
  2. 递推状态:dp[j]=dp[j]+dp[j-coins[i]]。不用当前物品+用当前物品
  3. 初始值:dp[0]=1,即0容量时有一种方法,就是选0件物品。
  4. 遍历顺序:组合问题,外部物品,内部背包,从小到大遍历。
  5. 返回结果:dp[amount]

11.组合总和

物品无限,有确定的总和即容量,求排列数,完全背包。按排列计算,不同的排列方式就是不同的方法。

  1. 子状态:dp[j],装满容量 j 的方法。
  2. 递推状态:dp[j]=dp[j]+dp[j-nums[i]]。不用当前物品+用当前物品
  3. 初始值:dp[0]=1,无意义,只是为了累加时能计算。
  4. 遍历顺序:排列问题,外背包,内物品(记得判断条件[j-nums[i]]),从小到大遍历。
  5. 返回结果:dp[target]

12.零钱兑换 

每种硬币的数量是无限的,凑成总金额所需的 最少的硬币个数 :典型完全背包问题。样例显示不强调组合还是排列,硬币有顺序和没有顺序都不影响硬币的最小个数,for循环内外可交换。

  1. 子状态:dp[j],装满容量 j 的最小数量。
  2. 递推状态:dp[j]=min(dp[j],dp[j-coins[i]]+1)。不用当前物品+用当前物品且数量+1
  3. 初始值:dp[0]=0,凑够0元时,需要0个硬币。剩下的初始化为INT_MAX
  4. 遍历顺序:都行,内循环是正序。可以判断 if (a[j - coins[i]] != INT_MAX)时,再计算数量。
  5. 返回结果:dp[amount]

13.完全平方数

乍一看,看不出来什么。模拟一遍过程,就看出来是零钱兑换的相似问题了。

注意以下几点:

1.dp[0]=0。为了能进循环并赋值后面的数,实际无意义。除此之外都初始化为INT_MAX,因为求的是最小值

 2.物品遍历,本来想的是再建一个物品数组,看了答案才知道可以直接dp[j-i*i].

3.先遍历背包:i从1开始,因为完全平方数从1开始

for(int j=0;j<=n;j++)
{
     for(int i=1;i*i<=j;i++)
    {
        a[j]=min(a[j],a[j-i*i]+1);
    }
}

先遍历物品:

for (int i = 1; i * i <= n; i++) 
{ 
            for (int j = 1; j <= n; j++) 
            {
                if (j - i * i >= 0) //判断是否越界
                {
                    dp[j] = min(dp[j - i * i] + 1, dp[j]);
                }
            }
}

14.单词拆分

 字符串是背包,字典是物品。字典里的单词可以无限取出放入背包内。

  1. 子状态:dp[j],长度为j的字符串是否可以被拆分
  2. 递推状态:i<j,dp[i]&&[i,j] 这个区间的子串出现在字典里 。那么dp[j]就是true。
  3. 初始值:dp[0]=true,如果设为false,那么循环结果始终为false
  4. 遍历顺序:有的题解说都行但建议用外层背包,有的说只能用外层背包。
  5. 返回结果:dp[s.size()]

 背包做了这些题,稍微一变是不是感觉自己就不会了呢?


15.打家劫舍

通过前面的计算后面的值,该类题也是典型动态规划问题。

  1. 子状态:dp[i],前i间房可得的最大钱数
  2. 递推状态:

dp[i]:此时两种可能,第i间房被偷了,第i-1号房子就不能偷,dp[i]=dp[i-2]+nums[i];

没偷第i间,第i-1间就能偷,但可以不偷,反正等于前 i-1个房子的最高金额,dp[i]=dp[i-1];

如果在前i-1间时,第i-1间一定被偷了吗?

假设第i-1间没被偷,dp[i-1]=dp[i-2],则第i间可以被偷:

dp[i]=dp[i-1]+num[i]=dp[i-2]+nums[i]。

第i-1间被偷了,那第i间就不能被偷。

所以,dp[i]=max(dp[i]=dp[i-2]+nums[i],dp[i-1])

3.初始值:dp[0]=nums[0],dp[1]=max(nums[0],nums[1])

4.遍历顺序: 从前到后

5.返回结果:dp[nums.size()-1]


16.打家劫舍II

本题可以转为两个子问题。

从下标0号开始考虑,到倒数第二个,即不选最后一个。

从下标1号到最后一个,即不选第一个。

也可以第一个和最后一个都不选,但这种情况包含在上述两种中了,即他们中不一定非选第一个或是最后一个,具体情况具体分析。

分别求他俩的最大值(和上面的题思路一样,不过是取值范围不同),再算他俩谁大。


17.打家劫舍III

只想到了后序遍历…… 

当前节点被偷,它的子节点不能偷,子节点的子节点可以考虑。不偷当前节点,可以偷它的子节点。子节点的子节点的子节点……递归!

不由得想到三种遍历方法,要分别在左子树,右子树找大的。因为通过递归函数的返回值来做下一步计算。

定义一个数组a,下标0表示不偷,下标1为偷。

如果当前为空就返回{0,0}。

递归遍历左子树,右子树。

单层遍历:当前节点被偷,它的子节点不能偷。不偷当前节点,可以选最大子节点(子节点的子节点)偷。

最终返回{单层遍历的两个值}


18.买卖股票的最佳时机

暴力解非常明显的超时了……

  1. 子状态:dp[i][0],第i天拥有股票时现金的最大金额,dp[i][1],第i天没有股票时现金的最大金额
  2. 递推状态:

dp[i][0]:

前一天就有股票dp[i-1][0],第i天新买的股票 -prices[i]

dp[i][1]:

前一天没有股票dp[i-1][1],第i天把股票卖出去了dp[i-1][0]+prices[i]

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

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

3.初始值:dp[0][0]-=prices[0]:第一天先买股票,dp[0][1]=0:没有股票

4.遍历顺序: 从前到后

5.返回结果:dp[prices.size()-1]


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

可以多次买卖,那么第i天新买的股票就可能是从前几天没有股票时获得的利润里扣钱,即使最终结果是负值也是正常的。

dp[i][0]:

前一天就有股票dp[i-1][0],第i天新买的股票 dp[i-1][1]-prices[i],唯一与上题不同的地方。

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


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

仅限两笔买卖,一共五种状态。

0:无操作

1:第一次买入

2:第一次卖出

3:第二次买入

4:第二次卖出

 

1.子状态:dp[i][0],第i天拥有股票时现金的最大金额,dp[i][1],第i天没有股票时现金的最大金额

2. 递推状态:

dp[i][0]:

前一天对股票就没操作dp[i-1][0]

dp[i][1]:

前一天没有股票dp[i-1][1],第i天把股票买了dp[i-1][0]-prices[i]

dp[i][2]:

前一天有股票dp[i-1][2],第i天把股票卖出去了dp[i-1][1]+prices[i]

……

dp[i][0] = dp[i - 1][0];

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

……                                           

3.初始值:dp[0][0]=0:dp[0][1]-=prices[0],dp[0][3]-=prices[0]

买入,现金就减少。卖出,现金增加,并且最终取的是最大值,收获利润小于0也没必要计算。

4.遍历顺序: 从前到后

5.返回结果:dp[prices.size()-1][4],因为最后一次卖出的价格一定比第一次卖的贵


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

 通过上题找规律。

0:无操作

1:第一次买入

2:第一次卖出

3:第二次买入

4:第二次卖出

5:第三次买入

6:第三次卖出

……

偶数卖出,奇数买入。

1.子状态:dp[i][j],第i天状态j时现金的最大金额

2. 递推状态:

dp[i][0]:

前一天对股票就没操作dp[i-1][0]

dp[i][1]:

前一天没有股票dp[i-1][1],第i天把股票买了dp[i-1][0]-prices[i]

dp[i][2]:

前一天有股票dp[i-1][2],第i天把股票卖出去了dp[i-1][1]+prices[i]

……

dp[i][0] = dp[i - 1][0];

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

……                                           

for(int i=1;i<prices.size();i++)
{
    for(int j=1;j<2*k+1;j++)
    {
         if(j%2==0)
             dp[i][j]=max(dp[i-1][j-1]+prices[i],dp[i-1][j]);//卖出
         else
             dp[i][j]=max(dp[i-1][j-1]-prices[i],dp[i-1][j]);//买入
    }
}

3.初始值:dp[0][0]=0:dp[0][1]-=prices[0],dp[0][3]-=prices[0]……奇数买入,

买入,现金就减少。卖出,现金增加,并且最终取的是最大值,收获利润小于0也没必要计算。

4.遍历顺序: 从前到后

5.返回结果:dp[prices.size()-1][2*k],因为最后一次卖出的价格一定比第一次卖的贵


22.买股票冷冻期

看了好多题解,分了4个状态:今天买,不持有股票有两种:(前天卖今天保持,今天卖 ),今天冷冻期。

感觉还是两种状态简洁一点,也好理解。但上面这种分多种状态讨论的思想要学会。

0:有股票

前一天就有了,前两条天没有今天买了(买入时要考虑冷冻期:昨天是冷冻期,即前天卖了也就是没有股票,今天才能买)。

那么会有i-2,此时要考虑到i<=1时的赋值。

当只有2天,如果可以买卖。只能第一天买,第二天卖,所以一定是前一天已经买好了。

第一天买为负值,第二天不能买了,就得让0状态保持第一天的状态, 比负值还小。

1:没股票

没股票就是卖出了或者是在冷冻期,不需要管前两天的。

前一天没有,即冷冻期或者处于没有状态。

前一天有,今天卖了。

初始值第一天买入即为负值 

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

        int n = prices.size();
        if (n == 1) return 0;
        vector<vector<int>>dp(n,vector<int>(2,0));
        dp[0][0]-=prices[0];  
         
        for(int i=1;i<n;i++)
        {
            //当只有2天,如果可以买卖。只能第一天买,第二天卖,所以一定是前一天已经买好了
            //第一天买为负值,第二天不买就得让0状态保持第一天的状态
            dp[i][0]=max(dp[i-1][0],(i>1?dp[i-2][1]:0)-prices[i]);

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

    }
};

23.买股票手续费

比买股票II多了在卖出时减去手续费,因为买卖结束后才算一笔交易,所以不能在买入时就减手续费。



24.最长递增子序列

经典题目!一定要会!

1.子状态:dp[i],前i个字符里最长递增子序列的长度

2.递推状态:如果nums[i]>nums[i-1],dp[i]可由dp[i-1]+1得出,即前i-1个字符再加上第i个字符。

i循环遍历nums数组,令0<= j <=i-1。每次还要比较当前dp[i]和dp[i-1]+1哪一个更大,也就是更新最长子串。

3.初始值:每个字符都设为1个长度。

4.遍历顺序:由dp[i-1]得到所以从前往后。

5.结果:返回的是最长。滚动数组过程中无法确定i遍历完后就是最大,所以要判断遍历完j后当前数组的dp[i]是否最大。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {

        if(nums.size()==1)
            return 1;
        vector<int>a(nums.size(),1);
        int c=0;
        //0~i-1,i
        for(int i=1;i<nums.size();i++)
        {
            for(int j=0;j<i;j++)
            {
                if(nums[i]>nums[j])
                a[i]=max(a[i],a[j]+1);
            }
            c=max(c,a[i]);
            //由于dp[i]表示前i个字符的最大升序长度。i在外层循环,每一行固定i与每一列的j比大小
            //要的是最后结果i,但其他行即i取其他值时有可能升序序列长度更大
        }
        return c;
    }
};

25.最长连续递增子序列

关键在于递增,比较dp[i],dp[i-1]的值。递推公式不用再取max了,因为dp[i]意为:以i结尾的递增子序列长度=以i-1结尾的递增序列长度+1。

最终结果要取max,因为自己遍历一遍数组发现最大值不在数组末尾。

递推公式实现后一定要自己算一遍答案。


26.最长公共子序列

1.子状态:两个数组长度不一,采用二维数组分别代表两数组长度。dp[i][j]:n1数组前i个字符与n2数组前j个字符拥有的公共子序列长度。

2.递推状态:题意可知,答案是不连续。从下标1开始。

n1[i-1]与n2[j-1]相等时:dp[i][j]=dp[i-1][j-1]+1。即前从开始到前一个字符得到的最大长度+1

不相等:dp[i][j]=max(dp[i-1][j],dp[i][j-1]) 从n1[0,i-2]与n2[0,j-1],n1[0,i-1]与n2[0,j-2]的最长子序列选最大的

3.初始值:0

4.遍历顺序:正序

5.结果dp[n1.size()][n2.size()]


27.判断子序列

编辑距离系列开始啦!

1.子状态:两数组长度不一,采用二维数组分别代表两数组长度。不连续,dp[i][j]:n1数组前i个字符与n2数组前j个字符拥有的公共子序列长度。

2.递推状态:

相等:a[i][j]=a[i-1][j-1]+1; 

不相等:要删除当前字符(j-1),即a[i][j] 要看s[i - 1]与 t[j - 2]的比较结果了:a[i][j] = a[i][j - 1];

3.初始值:a[i][0]表示以i-1为下标结尾的字符串,它与空串的相同序列长度为0

4.遍历顺序:可知a[i][j]来自a[i-1][j-1],a[i][j - 1],所以为正序

5.结果:a[s.size()][t.size()]表示s与t的最长公共序列,如果和s长度相同就得到结果了

其实本题用双指针更简单一点吧~

参考自大佬的题解:

bool isSubsequence(char * s, char * t){
    while(*s && *t){
        if(*s == *t){//
            s++;
        }
        t++;
    }
    if(*s == '\0')
        return true;
    else
        return false;
}


28.不同的子序列 

如果单独拿出来这题,这类题做得少的话,估计不容易想到动态规划的做法(好吧,博主在说博主自己呜呜)。

1.子状态:两个字符串长度不一,二维数组。dp[i][j]为s以下标为i-1 出现和 t以下标j-1结尾的序列相同的个数。

2.递推状态:要得到dp[i][j],注意dp数组从1开始

相等:s[i-1]==t[j-1]:可能使用s[i-1]--->s[0,i-1]匹配t[0,j-1]--->dp[i-1][j-1],也可能不用--->s[0,i]匹配t[j-1],就是去用s数组里 i 前面的字符--->dp[i-1][j]

寻找bat:
babtbat

不相等:dp[i][j]=dp[i-1][j],既然不相等,就不用s[i-1]来匹配了。

3.初始值:dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];   dp[i][j] = dp[i - 1][j];  

那么第0行第0列都得初始化:

dp[0][j]:s为空,无法匹配t,=0;

dp[i][0]:t为空,空集是任何字符串的子集,或者理解为,s数组删n个字符,就能得到空串与空串t匹配,=1;

dp[0][0]也和dp[i][0]的理解一致,=1。

4.遍历顺序:来自左上,上面,所以正序。

5.结果:dp[s.size()][t.size()]

放个代码:

发现如果用int类型会溢出,那就用long long吧。

class Solution {
public:
    int numDistinct(string s, string t) {

         // dp[0][j]=0;
    vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1,0));
        for(int i=0;i<s.size();i++)
        {
          
            dp[i][0]=1;         
        }
        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()];
    }
};


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

本题可以理解为:两个字符串的公共子序列,最后返回两个数组长度相加-公共子序列*2。就能得到最短操作步数。

两个数组都能删了。

class Solution {
public:
    int minDistance(string word1, string word2) {
        //最长公共子序列
        vector<vector<int>>dp(word1.size()+1,vector<int>(word2.size()+1,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]+1;
                else
                    dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
            }
        }
        return word1.size()+word2.size()-dp[word1.size()][word2.size()]*2;
        

        
    }
};

30.编辑距离 

1.子状态:dp[i][j]  w1数组下标i-1结尾,与 以j-1下标结尾的w2,的最近编辑距离。

2.递推状态:

w1[i-1]==w2[j-1]:不操作,就等于dp[i-1][j-1]

w1[i-1]!=w2[j-1]:删除、增加、替换

删除:w1删除1个:dp[i-1][j]+1

删除和增加是一样的:w1: ja 和  w2: a,ja删除j或者a增加j,编辑距离一样 dp[i][j-1]+1

替换:w1替换掉w1[i-1],让他与w2[j-1]相等,即w1数组下标i-2结尾,与 以j-2下标结尾的w2,的最近编辑距离。dp[i-1][j-1]+1,在进行替换的数组里:dp[i-1][j-1]表示前面的已经相等了,再加当前i-1下标的字符被替换。

3.初始值:

dp[i][0]:以下标i-1的w1,与空字符串的 最短编辑距离:删除i个

dp[0][j]:以下标j-1的w2,与空字符串的 最短编辑距离:删除j个

4.遍历顺序:

来自左边,上边,左上,所以正序

5.结果:遍历一遍即可得到最后的答案在尾部。


31.回文子串

动态规划和双指针都行,先来看看双指针:

分别以s的每个字符为中心点,向两边扩散,找两边相等的,这是奇数个字符时的做法。

偶数个字符,比如caac,则要以两个中心字符向两边扩散。

 

动态规划:

1.子状态:dp[i][j]:[i,j]闭区间内是否为回文子串

2.递推状态:

s[i]==s[j]:

①i==j:a是回文子串

②i+1=j:aa也是

③j-1>i:已经s[i]==s[j],下标i与j差了很多,隔了很远,就得缩短空间-->dp[i+1][j-1]判断是否文回文子串。

s[i]!=s[j]:

dp[i][j]=false

3.初始值:

都为false

4.遍历顺序:

dp[i][j]由dp[i+1][j-1],也就是左下方推出,就得从下往上遍历。

5.结果:

数一数总共几个true

class Solution {
public:
    int countSubstrings(string s) {

        vector<vector<bool>>a(s.size(),vector<bool>(s.size()+1,false));
        int c=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<=1)
                    {
                        c++;
                        a[i][j]=true;
                    }
                    else if(a[i+1][j-1])
                    {
                        a[i][j]=true;
                        c++;
                    }
                }
                

            }
        }
        return c;
    }
};


32.最长回文子序列

1.子状态:dp[i][j],区间内回文子序列的长度

2.递推状态:

s[i]==s[j]:

dp[i][j]=dp[i+1][j-1]+2:加上相等的两字符

s[i]!=s[j]:

更新最大值,dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);  既然两边不相等,那就去掉一边看看是否能为最大值。

s[i]   s[i+1]    s[j-1]   s[j]

3.初始值:

式子无法推出dp[i][i],那就初始为1,其他都为0.

4.遍历顺序:

来自左下,左,下。从下往上遍历。

5.结果:

下->上遍历,左->右,结果应该在右上角。dp[0][s.size()-1]


如果对你有帮助,请一键三连(点赞+收藏+关注)哦~ 感谢支持!让更多的人看到~ 欢迎各位在评论区与博主友好讨论!(◕ᴗ◕✿)

 

 (表情包来源网络)

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值