目录
[1.第 N 个泰波那契数](#1.第 N 个泰波那契数)
[17.买卖股票的最佳时机 III](#17.买卖股票的最佳时机 III)
[18.买卖股票的最佳时机 IV](#18.买卖股票的最佳时机 IV)
[34.等差数列划分 II](#34.等差数列划分 II)
[44. 通配符匹配](#44. 通配符匹配)
[52.最后一块石头的重量 II](#52.最后一块石头的重量 II)
[57. 一和零](#57. 一和零)
[59. 组合总和 Ⅳ](#59. 组合总和 Ⅳ)
前言
本篇博客是对动态规划算法一部分经典题目的整理与每道题很详细的思考与题解过程,相信大家看完能对动态规划算法类型的相关问题有一定的理解和掌握(。・∀・)ノ(动态规划这一篇一共有60道题,每一道题都是很经典的题目)
算法原理(总)
一般来说我们使用动态规划算法时,需要一个表名为dp表,我们使用动态规划就是为了将这个表给填满,进而解题

我们用动态规划算法解题一共要弄清5步
-
状态表示(最重要的一步:只有弄清这一步,才有接下来的事情)
是什么:感性的简单理解上来说,状态表示就是dp表里面的值所表示的含义
状态表示怎么来的?
a. 题目要求
b. 经验 + 题目要求 (一般都是这样来的,所以需要多练习相关题目才能有经验)
c. 分析问题过程中,发现重复子问题
- 状态转移方程(最难的一步:推导状态转移方程)
是什么:dp[i]等于什么,状态转移方程就是什么(就题分析)
搞定前两步就相当于弄懂如何使用动态规划解题的99%了,剩下的三步都是处理细节问题
- 初始化
意义:保证填表的时候不越界
根据状态转移方程进行填表,我们需要把可能会导致数组越界访问的位置都初始化
- 填表顺序
意义:为了填写当前状态的时候,所需要的状态已经计算过了
在填dp[i]时,需要保证状态转移方程所需要的等号右边数据都是存在的
- 返回值
根据题目要求以及状态表示来返回
斐波那契数列模型
1.第 N 个泰波那契数

题目解析:上面的T(n+3)=Tn+T(n+1)+T(n+2)可以转换成Tn=T(n-3)+T(n-2)+T(n-1),在前三项知道的情况下,第n个泰波那契数等于其前面三项的和,比如T3=T2+T1+T0=0+1+1=2
算法原理:动态规划
解法:动态规划
步骤:结合题目
-
状态表示是根据题目要求得出的:dp[i]就是第i个泰波纳契数列的值
-
状态转移方程:dp[i]=dp[i-1]+dp[i-2]+dp[i-3]
-
初始化:题目已经给了前三个可能会导致越界位置的值:dp[0]=0,dp[1]=1,dp[2]=1
-
填表顺序:从左向右可以确保dp[i-1]+dp[i-2]+dp[i-3]位置都存在值
-
返回值:题目要求返回的就是第n个泰波纳契数列的值,也就是dp[n]
解题代码:c++
cpp
class Solution {
public:
int tribonacci(int n)
{
//前置工作,需要处理一下n小于3的时候的边界情况
//不然在动态转移方程中如果n<3,是会发生越界访问的情况的
if(n==0)
{
return 0;
}
if(n==1||n==2)
{
return 1;
}
//动态规划步骤:
//1.创建dp表
//题中下标从0开始到n,一共有n+1个数,所以得创建n+1大小的dp表
vector<int> dp(n+1);
//2.初始化
//就是让dp[0]=0,dp[1]=1,dp[2]=1
dp[0]=0,dp[1]=1,dp[2]=1;
//3.填表
//本题中的状态转移方程是:dp[i]=dp[i-1]+dp[i-2]+dp[i-3]
//i从3开始
for(int i=3;i<=n;i++)
{
dp[i]=dp[i-1]+dp[i-2]+dp[i-3];
}
//4.返回值
//根据题意,就是第n个泰波那契数:dp[n]
return dp[n];
}
};
空间优化:只在我们第一题(比较简单)和后面的背包问题时提及,因为我们解决动态规划问题首先得把那5步搞清楚,进而解题,空间优化只是拓展的内容
优化思路:
当我们在填dp表的时候,仅需要某一状态前面的若干个状态,而再前面的状态都可以不需要的时候,我们就可以使用滚动数组进行优化(空间复杂度从O(n^2)->O(n)或者O(n)->O(1)...)
滚动数组操作:本题中我们可以定义a、b、c、d四个变量,一开始是a、b、c分别赋值为T0,T1,T2,d的值需要a、b、c相加,而后四个变量一起向右移动一步,移动之后,把b值赋值给a,c值赋值给b,d值赋值给c,d还是通过a+b+c求得,(上述赋值过程是不能反过来的,因为要是反过来,要赋给的值会由于上次的赋值给覆盖掉了),重复过程直到d到n为止

优化后的解题代码:空间复杂度为O(1)
cpp
class Solution {
public:
int tribonacci(int n)
{
//前置工作,需要处理一下n小于3的时候的边界情况
if(n==0)
{
return 0;
}
if(n==1||n==2)
{
return 1;
}
//空间优化:滚动数组
int a=0,b=1,c=1,d=0;
for(int i=3;i<=n;i++)
{
d=a+b+c;
//滚动操作
a=b;
b=c;
c=d;
}
return d;
}
};
2.三步问题

解析:

根据上图我们可以得知:我们要算第三阶台阶有几种上法,无非就是看看前面2、1、0阶台阶有几种上法,因为无论从0、1还是2上3都是在原来上它们的方法上加上一步就好了,那么对应的方法是不变的,所以直接是等于前面三阶台阶的上法之和就求得该阶台阶的上法啦
算法原理:动态规划
需要说明的是,这里使用递归是会超时的(亲身试验)
解法:动态规划
-
状态表示
根据经验+题目要求得出:dp[i]表示到达i位置时,一共有多少种上台阶的方式

- 状态转移方程
其实从上面的解析就很容易看出来一个求n阶台阶有几种上法的公式了:dp[n]=dp[n-1]+dp[n-2]=dp[n-3],不错,这也就是我们的状态表示方程了!!

3.初始化
在只考虑从1阶台阶的情况开始时,由于方程为dp[i]=dp[i-1]+dp[i-2]+dp[i-3],所以1、2、3阶台阶都得进行一下初始化,不然dp[3]=dp[2]+dp[1]+dp[0]还是dp[2]=dp[1]+dp[0]+dp[-1]都会有越界的情况,上面题目解析时就很容易看出:dp[3]=4,dp[2]=2,dp[1]=1
- 填表顺序
根据状态表示,dp[i]表示的是前面到i位置一共有多少种上台阶方式,所以填表顺序自然就是从前往后了
- 返回值:
根据上述题意和解析的理解,题目的要求就是这个dp[n],那么返回dp[n],但要注意的是题目中说这个数可能很大,所以得%1000000007,返回的也就是dp[n]%1000000007 ,包括在推dp表的时候也得%1000000007
解题代码:c++
cpp
class Solution {
public:
int waysToStep(int n)
{
//处理边界问题
if(n==1||n==2)
{
return n;
}
if(n==3)
{
return 4;
}
//初始化dp表
vector<long long> dp(n+1);
dp[1]=1;
dp[2]=2;
dp[3]=4;
//填dp表
for(int i=4;i<=n;i++)
{
dp[i]=(dp[i-1]+dp[i-2]+dp[i-3])%1000000007;
}
return dp[n]%1000000007;
}
};
3.使用最小花费爬楼梯

根据题目可知,我们的顶部是在cost数组结尾的下一个位置的,而不是最后一个元素的位置

算法原理:动态规划
解法1:
-
状态表示
根据经验+题目要求,那么dp[i]表示:到达i位置时的最小花费
-
状态转移方程
用之前或者之后的状态,推导出dp[i]的值
根据最近的一步来划分问题:

- 初始化【保证填表时不越界】
那么就是dp[0]=dp[1]=0
- 填表顺序
根据状态转移方程,轻松得知是从左往右填的
- 返回值
根据上面的顶部是结尾的下一个位置,那么就是dp[n]
解题代码:c++
cpp
class Solution {
public:
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];
}
};
这里还有个解法2,也是用动态规划来写,只不过状态表示不同:


解题代码如下:
cpp
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost)
{
int n=cost.size();
vector<int> dp(n);
//初始化
dp[n-1]=cost[n-1];
dp[n-2]=cost[n-2];
//从右往左填表
for(int i=n-3;i>=0;i--)
{
dp[i]=min(dp[i+1]+cost[i],dp[i+2]+cost[i]);
}
return min(dp[0],dp[1]);
}
};
4.解码方法


示例的话就大家去力扣上看啦
算法原理:动态规划
-
状态表示
根据经验+题目要求
以i位置为结尾,然后... ,那么就有:dp[i]表示的是以i位置为结尾时,解码方法的总数
-
状态转移方程
根据最近的一步,划分问题,那么我们根据题目的分析可得出:

这里的五角星是说明只有上面成功了才是这样的!
- 初始化

- 填表顺序
根据上面的状态表示和转移方程可知,应该是从左向右填表
- 返回值
还是根据题目以及上面的分析可知,dp[n-1]就是解码方法的总数,那么返回值就是dp[n-1]
解题代码:c++
cpp
class Solution {
public:
int numDecodings(string s)
{
int n=s.size();
vector<int> dp(n);
//初始化
//dp[0]
dp[0]=s[0]!='0';
//处理一下边界情况
if(n==1) return dp[0];
//dp[1]
if(s[0]!='0'&&s[1]!='0') dp[1]+=1;
int t=(s[0]-'0')*10+(s[1]-'0'); //前两个位置所表示的数
if(t>=10&&t<=26) dp[1]+=1;
//填表
for(int i=2;i<n;i++)
{
//处理单独编码的情况
if(s[i]!='0') dp[i]+=dp[i-1];
//处理与前面字符一起编码的情况
t=(s[i-1]-'0')*10+(s[i]-'0');
if(t>=10&&t<=26) dp[i]+=dp[i-2];
}
return dp[n-1];
}
};
其实可以看到我们这个代码算是比较长的了,原因在于初始化时写的比较多,那么我们可以进一步优化
观察可知,在上面的代码中我们初始化和下面填表相关代码是有所重合的,那么有没有一种技巧是可以将比较复杂初始化放入这个填表代码中的呢,这样就能简化不少行了,答案是当然有!!
处理边界问题以及初始化问题的技巧:

可以看到此时我们就将比较复杂的dp[1]的初始化放在了dp[2]的位置,那么此时就可以在填表时对其进行初始化;以此优化后的解题代码:
cpp
class Solution {
public:
int numDecodings(string s)
{
int n=s.size();
vector<int> dp(n+1);
//初始化放入填表中的优化技巧
dp[0]=1;//保证后面的填表是正确的
dp[1]=s[1-1]!='0';
//填表
for(int i=2;i<=n;i++)
{
//处理单独编码的情况
if(s[i-1]!='0') dp[i]+=dp[i-1];
//处理与前面字符一起编码的情况
int t=(s[i-2]-'0')*10+(s[i-1]-'0');
if(t>=10&&t<=26) dp[i]+=dp[i-2];
}
return dp[n];
}
};
可以看到代码行数少了不少啦
路径问题
5.不同路径


示例2的图:

算法原理:动态规划
-
状态表示
经验+题目要求,那么就以[i,j]为结尾时,...
dp[i] [j] 表示:走到[i,j]位置的时候,一共有多少种方式
-
状态转移方程
根据最近的一步来划分问题,我们来华图分析其中画三角形的位置
此时只有从它的左边到它以及从上边到它这个路线能行(题意),如果这个位置是[i] [j]的话,上面位置是[i-1] [j],左边位置是[i] [j-1]

相同的连线是一种方法,也就是说从[i-1] [j]到[i] [j]是一种方法,从[i] [j-1] 到[i] [j]是一种方法,那么我们的dp[i] [j]的表示就显而易见了:dp[i] [j]=dp[i-1] [j]+dp[i] [j-1]

- 初始化
根据状态转移方程以及画图分析:

当我们要填上左最外边一圈时是会越界的,那么我们先要将这些位置提前初始化好,其值也很简单得出------就是1
当然,这里的初始化可以沿用我们上一题的优化方法,一维数组是最左边加一个格子,那么这里的二维数组就是在左边加一列格子的同时上边再加一行格子,再把对角填一个格子,那么根据之前上面的边沿格子位置都需要填1就能推出新加的格子位置要填什么,如下:

这里我们可能会想为啥要这么麻烦,直接按上面那种边缘循环一下初始化为1不就好了,这是因为只是这一题对于这种优化方法看起来相对多了点弯弯绕绕,但是对于有些题来说这样进行优化后,其初始化才简单一些,而且我们写完代码就知道,这种方法在本题中可以节省好多行代码和循环的,也是写动态规划题时比较推荐的一种填表初始化优化做法
- 填表顺序
根据上面的分析,那么就是总体从上往下写每一行,每一行是从左往右来填
- 返回值
轻松根据题意和上面的分析可知,返回值就是dp[m] [n]
解题代码:c++
cpp
class Solution {
public:
int uniquePaths(int m, int n)
{
//多开一行一列进行初始化优化操作
vector<vector<int>> dp(m+1,vector<int>(n+1));
//初始化
dp[0][1]=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][n];
}
};
可以看到如果我们要使用优化前的初始化方式,是要两次循环进行填表初始化的,现在优化后只需要让vector多开一行、一列,再将dp[0] [1]初始化为1接着下面填表就好了
6.不同路径II


算法原理:动态规划
这一题的算法原理和上一题是一样的,唯一的区别就是在状态转移方程哪里加了个有无障碍物(barrier)的判断

当然这里的初始化我们只需要保证原先第一个格子的值能够是1就好了,那么我们可以选择其上面新增的格子为1,也可以选择其左侧新增的格子为1,因为上一题我们选了上面,那么这次我们就选其左侧新增的格子置1,其他新增的格子不用动默认为0就好
解题代码:c++
cpp
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid)
{
int m=obstacleGrid.size(),n=obstacleGrid[0].size();
vector<vector<int>> dp(m+1,vector<int>(n+1));
//初始化
dp[1][0]=1;
//填表
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
//唯一的区别就是该位置对应在数组中有障碍物时
//此时在该位置的路径数量肯定是0,也就是让dp[i][j]=0即可
if(obstacleGrid[i-1][j-1]==1)
{
dp[i][j]=0;
}
else
{
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
return dp[m][n];
}
};
7.珠宝的最高价值

题意:

算法原理:动态规划
-
状态表示
经验+题目要求 ------> dp[i] [j]表示到达[i,j]位置时,此时能拿到珠宝的最高价值
-
状态转移方程
根据最近的一步来划分问题,到dp[i] [j]位置时要么从左边走过来,要么从上面走过来,而又要求最高价值,那么在dp[i] [j]位置的最高价值不就是dp[i] [j-1]和dp[i-1] [j]其中的较大值再加上该位置的价值嘛,所以我们的状态转移方程就是:dp[i] [j]=max(dp[i-1] [j]+dp[i] [j-1])+frame[i-1] [j-1](这里的话对应下标是i-1和j-1是因为下面初始化时优化给dp多加一行一列之后所对应的下标映射)

- 初始化
由于填dp表第一行和第一列的位置都可能会越界,所以我们进行优化,给dp表多添加一行一列就行(和上几题一样的优化方式),根据题目来看这多的一行一列全为0就好,那么我们就开多一行一列的vector之后容器会自动填充0,那么就不需要为其手动初始化其他值了

- 填表顺序
根据状态转移方程可知,从上往下填写每一行,每一行从左往右
- 返回值
根据题目和上面的分析,返回值就是右下角的位置的值------dp[m] [n](m为行数,n为列数)
解题代码:c++
cpp
class Solution {
public:
int jewelleryValue(vector<vector<int>>& frame)
{
int m=frame.size(),n=frame[0].size();
vector<vector<int>> dp(m+1,vector<int>(n+1));
//填表
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1])+frame[i-1][j-1];
}
}
return dp[m][n];
}
};
8.下降路径最小和


题目解析:

算法原理:动态规划
-
状态表示
根据经验 + 题目要求
dp[i] [j]表示:到达[i] [j]位置时最小的下降路径
-
状态转移方程
也是根据最近的一步来划分问题

- 初始化

- 填表顺序
根据状态转移方程可知,从上往下填写每一行,填每一行时的顺序不固定,我这里就按从左往右啦
- 返回值
根据题目和上面的分析,到dp表的最后一行就是下降路径,我们要最小值,那么返回值就是dp表的最后一行中的最小值
解题代码:c++
cpp
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& matrix)
{
//二维动态规划
int n=matrix.size();
//先开对应空间全初始化为INT_MAX
vector<vector<int>> dp(n+1,vector<int>(n+2,INT_MAX));
//再完成初始化,将第一行改为0
for(int i=0;i<n+2;i++)
{
dp[0][i]=0;
}
//填表
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
dp[i][j]=min(min(dp[i-1][j-1],dp[i-1][j]),dp[i-1][j+1])+matrix[i-1][j-1];
}
}
//求dp表最后一行的最小值
int ans=INT_MAX;
for(auto n:dp[n])
{
ans=min(ans,n);
}
return ans;
}
};
9.最小路径和

算法原理:动态规划
-
状态表示
经验+题目要求 dp[i] [j]表示:到达[i,j]位置时,最小路径和

-
状态转移方程
根据最近的一步来划分问题

- 初始化

- 填表顺序
根据状态转移方程进行分析,应该是从上往下填写每一行,每一行从左往右填
- 返回值
根据题目和上面的内容分析可知,返回值就是dp[m] [n] ,m为题目矩阵行,n为题目矩阵列
解题代码:c++
cpp
class Solution {
public:
int minPathSum(vector<vector<int>>& grid)
{
int m=grid.size(),n=grid[0].size();
vector<vector<int>> dp(m+1,vector<int>(n+1,INT_MAX));
//初始化
dp[0][1]=0;
//填表
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i-1][j-1];
}
}
return dp[m][n];
}
};
10.地下城游戏

题目解析:

算法原理:动态规划
-
状态表示
根据经验+题目要求有:
1.以某个位置为结尾,....
dp[i] [j]表示:从起点出发,到达[i,j]位置的时候,所需的最低初始健康点数
如果我们这样去定义,根据题目以及题目解析,那么这个位置就不仅收上面左边位置的影响,还受到下面和右边位置的影响,因为继续走下去得考虑到下一位置时点数是否为0及以下的了------有后效性
2.以某个位置为起点,....
dp[i] [j]表示:从[i] [j]位置出发到达终点,所需的最低初始健康点数
-
状态转移方程

- 初始化

- 填表顺序
根据状态转移方程可知,应该是从下往上填每一行,每一行从右向左填
- 返回值
根据填表顺序、状态表示和题目分析可知,返回值就算dp[0] [0]
解题代码:c++
cpp
class Solution {
public:
int calculateMinimumHP(vector<vector<int>>& dungeon)
{
int m=dungeon.size(),n=dungeon[0].size();
vector<vector<int>> dp(m+1,vector<int>(n+1,INT_MAX));
//初始化
dp[m][n-1]=dp[m-1][n]=1;
//填表
for(int i=m-1;i>=0;i--)
{
for(int j=n-1;j>=0;j--)
{
dp[i][j]=min(dp[i][j+1],dp[i+1][j])-dungeon[i][j];
dp[i][j]=max(dp[i][j],1);
}
}
//返回值
return dp[0][0];
}
};
简单多状态dp
11.按摩师

题目分析:

算法原理:动态规划
-
状态表示
根据经验+题目要求
dp[i]表示:选择到i位置时,此时的最长预约时长
继续细化:
f[i]表示:选择i位置时,num[i]必选,此时的最长预约时长
g[i]表示:选择i位置时,num[i]不选,此时的最长预约时长
-
状态转移方程

- 初始化
目的还是保证填表的时候不越界,根据状态转移方程和题目分析可知:
要初始化的:f[0]=num[0] , g[0]=0(这个创建vector时默认就初始化了,不需要手动去初始化)
然后还需要考虑到如果题中数组为空则直接返回0的边界情况
- 填表顺序
根据状态转移方程可知应该是从左往右f和g表一起填
- 返回值
根据题目和状态表示可知返回值应该是 max(f[n-1],g[n-1])
解题代码:c++
cpp
class Solution {
public:
int massage(vector<int>& nums)
{
//多状态dp
//f[i]表示选择i位置时最长预约时长,g[i]就是不选i位置的最长时长
int n=nums.size();
if(n==0)
{
return 0;
}
vector<int> f(n),g(n);
//初始化
f[0]=nums[0];
//从左往右同时填表
for(int i=1;i<n;i++)
{
//f[i]就是不选i-1位置的最长预约时长加上该位置的时长
f[i]=g[i-1]+nums[i];
//g[i]就是前一个位置不选或者选的情况下的最长时长
g[i]=max(f[i-1],g[i-1]);
}
//返回值
return max(f[n-1],g[n-1]);
}
};
12.打家劫舍II

做这题之前大家先要把打家劫舍I做了,其思路和上面的按摩师完全一致
算法原理:动态规划
我们这里可以借助打家劫舍I来解题,我们用rob1表示打家劫舍I中的解题函数,那么分析这一题其实就可以进行分类讨论,选第一间房和不选第一间房的情况,然后将两种情况都可以转换为打家劫舍I的情形,取这两种情况所得的钱数的较大值就行:

解题代码:c++
cpp
class Solution {
public:
int rob1(vector<int>& nums,int left,int right)
{
//处理一下边界情况
if(left>right) return 0;
//f表表示偷该位置时所得最大金额,g表示不偷该位置的所得最大金额
int n=nums.size();
vector<int> f(n),g(n);
//初始化
f[left]=nums[left];
//填表
for(int i=left+1;i<=right;i++)
{
f[i]=g[i-1]+nums[i];
g[i]=max(f[i-1],g[i-1]);
}
return max(f[right],g[right]);
}
int rob(vector<int>& nums)
{
int n=nums.size();
return max(rob1(nums,2,n-2)+nums[0],rob1(nums,1,n-1));
}
};
13.删除并获得点数

算法原理:动态规划
我们拿到一个新题就尽量往我们熟悉的旧题上面靠:
如果这个数组是有序的,并且连续,那么就很像我们的打家劫舍问题了,这个时候单纯不能选择相邻的数

但是如果不连续,有出现断层的那就不是打家劫舍问题的思路了,所以我们得先对原题数组进行一下预处理

那么这个时候就直接套用我们打家劫舍问题得解题思路就好了
关于打家劫舍问题的动态规划相关步骤如下:

解题代码:c++
cpp
class Solution {
public:
int deleteAndEarn(vector<int>& nums)
{
//现预处理,然后对arr数组中做一次打家劫舍
sort(nums.begin(),nums.end());
int n=nums.size();
int maxnum=nums[n-1];
vector<int> arr(maxnum+1);
cout<<arr.size();
for(int i=0;i<n;i++)
{
arr[nums[i]]+=nums[i];
}
//对arr进行打家劫舍问题的动态规划
//f代表选择该位置,g表示不选择该位置
vector<int> f(maxnum+1),g(maxnum+1);
//初始化
f[0]=arr[0];
//填表
for(int i=1;i<=maxnum;i++)
{
f[i]=g[i-1]+arr[i];
g[i]=max(g[i-1],f[i-1]);
}
//返回值
return max(f[maxnum],g[maxnum]);
}
};
14.粉刷房子

题目解析:二维数组的横坐标表示房间号,列坐标0、1、2分别表示的是红蓝绿三种颜色,其中的元素代表的是对应房间刷对应颜色所需要的花费;同时相邻房间不可以选择同样的颜色
对应示例一就是:

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int minCost(vector<vector<int>>& costs)
{
int n=costs.size();
vector<vector<int>> dp(n+1,vector<int>(3));
//填表
for(int i=1;i<=n;i++)
{
dp[i][0]=min(dp[i-1][1],dp[i-1][2])+costs[i-1][0];
dp[i][1]=min(dp[i-1][0],dp[i-1][2])+costs[i-1][1];
dp[i][2]=min(dp[i-1][0],dp[i-1][1])+costs[i-1][2];
}
//返回值
return min(dp[n][0],min(dp[n][1],dp[n][2]));
}
};
15.买卖股票的最佳时机含冷冻期

题目解析:

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int maxProfit(vector<int>& prices)
{
//动态规划
//分为三种状态:买入、可交易、冷冻
int n=prices.size();
vector<vector<int>> dp(n,vector<int>(3));
//初始化
dp[0][0]=-prices[0];
//一开始就处于可交易状态就是啥也没干,处于冷冻状态就是买了又卖,利润都为0
dp[0][1]=dp[0][2]=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][2]);
dp[i][2]=dp[i-1][0]+prices[i];
}
//返回值
//这里买入状态时其实是不可能是最大利润的情况的,因为要扣钱,不考虑进去也可以
return max(dp[n-1][0],max(dp[n-1][1],dp[n-1][2]));
}
};
16.买卖股票的最佳时机含手续费

题目示例解析:

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int maxProfit(vector<int>& prices, int fee)
{
//分持有股票和不持有股票两种状态
//拿f表示持有股票时最大利润,g表示不持有股票时最大利润
int n=prices.size();
vector<int> f(n),g(n);
//初始化
f[0]=-prices[0];
//填表
for(int i=1;i<n;i++)
{
f[i]=max(f[i-1],g[i-1]-prices[i]);
g[i]=max(g[i-1],f[i-1]+prices[i]-fee);
}
return g[n-1];
}
};
17.买卖股票的最佳时机 III
前面的I和II都挺简单的,大家可以先把前置的题目做完!

样例分析:

算法原理:动态规划
究竟是几维的dp是根据你要用dp存储的信息数量决定的!!

-
这里用到了我们第二个初始化的技巧:就是当状态转移方程中有干扰我们初始化的时候,就比如说j=0时,j-1为-1,怎么可能完成了-1次交易呢,所以我们可以在g的状态转移方程中先将前一天的g状态赋给g,再用个判断来判定j-1是否大于等于0,合法的时候再进行max(g[i] [j]【提前已经把g[i-1] [j]赋给g[i] [j]了】,f[i-1] [j-1]+p[i])取最大值
-
同时初始化时在一开始我们就不能买了卖、卖了买了,因为最多的交易次数是有限制的,不能交易任意次,那么我们不能让其影响我们,所以就将初始行的1、2列都初始化为 -无穷 即可,但是要注意的是在这里我们不能用INT_MIN来表示-无穷,因为我们是有相减的式子存在的,INT_MIN减一个数就超出int的最小范围了,是会报错的,此时选择 -0x3f3f3f3f(int最小值的一半,也就是1/2*INT_MIN),这个数好在足够小于题中可能出现的最小数,不会影响结果,其次就是在要被做减法的式子中不会超出数据范围,不会报错!!------这个在做算法题中非常常用
解题代码:c++
cpp
class Solution {
public:
int maxProfit(vector<int>& prices)
{
//多状态dp
//f[i][j]表示在i天进行j次交易情况下持有股票的状态
//g[i][j]表示在i天进行j次交易情况下不持有股票的状态
int n=prices.size();
int tmp=-0x3f3f3f3f;
vector<vector<int>> f(n,vector<int>(3,tmp));
auto g=f;
//初始化
f[0][0]=-prices[0],g[0][0]=0;
//填表
for(int i=1;i<n;i++)
{
for(int j=0;j<3;j++)
{
f[i][j]=max(f[i-1][j],g[i-1][j]-prices[i]);
g[i][j]=g[i-1][j];
if(j-1>=0)
{
g[i][j]=max(g[i][j],f[i-1][j-1]+prices[i]);
}
}
}
return max(g[n-1][0],max(g[n-1][1],g[n-1][2]));
}
};
18.买卖股票的最佳时机 IV

题目解析:

算法原理:动态规划
本题的动态规划思路和上一题是一样的

解题代码:c++
cpp
class Solution {
public:
int maxProfit(int k, vector<int>& prices)
{
//就是和III是一个思路的
int n=prices.size();
int tmp=-0x3f3f3f3f;
//优化一下k,其实最多也就只能交易n/2次
k=min(k,n/2);
vector<vector<int>> f(n,vector<int>(k+1));
auto g=f;
//进行初始化
f[0][0]=-prices[0];
for(int j=1;j<=k;j++)
{
f[0][j]=g[0][j]=tmp;
}
//填表
for(int i=1;i<n;i++)
{
for(int j=0;j<=k;j++)
{
f[i][j]=max(f[i-1][j],g[i-1][j]-prices[i]);
g[i][j]=g[i-1][j];
if(j-1>=0)
{
g[i][j]=max(g[i][j],f[i-1][j-1]+prices[i]);
}
}
}
//返回值为g[n-1]行中利润最大的那一列的值
int ans=INT_MIN;
for(int j=0;j<=k;j++)
{
ans=max(g[n-1][j],ans);
}
return ans;
}
};
子数组系列
19.最大子数组和

算法原理:动态规划
法一:暴力枚举

法二:动态规划

解题代码:c++
cpp
class Solution {
public:
int maxSubArray(vector<int>& nums)
{
int n=nums.size();
vector<int> dp(n+1);
//填表
int ans=INT_MIN;
for(int i=1;i<=n;i++)
{
dp[i]=max(nums[i-1],dp[i-1]+nums[i-1]);
ans=max(ans,dp[i]);
}
return ans==INT_MIN?nums[0]:ans;
}
};
20.环形子数组的最大和

算法原理:动态规划
本题和上一题的不同在于本题的数组是成环的,那么我们可以将其尽量转换为上一题的样子然后套用上一题的方式来解题(和我们前面的打家劫舍II用到的思想是一样的------转换成我们熟悉的题)
那么我们可以把这一问题拆成下面两种普通子数组问题的情况

那么我们的动态规划思路:

这里重点说一下返回值,如果数组是【-2,-3,-1】,那么fmax就是-1,gmin则就是sum,sum-gmin为0了,此时如果使用max(gmin,fmax)来求返回值就会返回0,但实际上其最大和是-1,结果就不对了;所以我们要判断一下sum是否和gmin相等,相等的情况下表示数组中全为负数,那么直接返回fmax,不相等才对fmax和gmin取较大值
解题代码:c++
cpp
class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums)
{
int n=nums.size();
int sum=0;
for(auto n:nums)
{
sum+=n;
}
vector<int> f(n+1),g(n+1);
//填表
int fmax=INT_MIN,gmin=INT_MAX;
for(int i=1;i<=n;i++)
{
f[i]=max(nums[i-1],f[i-1]+nums[i-1]);
fmax=max(fmax,f[i]);
g[i]=min(nums[i-1],g[i-1]+nums[i-1]);
gmin=min(gmin,g[i]);
}
//返回值
return sum==gmin?fmax:max(fmax,sum-gmin);
}
};
21.乘积最大子数组

算法原理:动态规划

状态表示和状态转移方程的意思是求最大值时如果我们的nums[i]是小于0的,那么它应该乘上前面子数组的最小值,也就是g[i-1]之后才可能是较大的乘积,大于0则是正常乘上最大值也就是f[i-1];那么这个最小值乘积也需要一个状态表示,同时求的是最小值,如果nums[i]大于0则乘上一个位置的最小值也就是g[i-1]后才可能是最小值,而当nums[i]小于0时乘上上一个位置的最大值也就是f[i-1]才有可能是最小值;nums[i-1]等于0时不影响结果 ------所以有两个表示和两个状态转移方程
解题代码:c++
cpp
class Solution {
public:
int maxProduct(vector<int>& nums)
{
//子数组动态规划
//f[i]表示以i位置为结尾的所有子数组中的最大乘积
//g[i]表示以i位置为结尾的所有子数组中的最小乘积
int n=nums.size();
vector<int> f(n+1),g(n+1);
//初始化,0位置处不能影响我们的填表,那么给1无论如何一开始的最大乘积肯定是nums[0]
f[0]=g[0]=1;
//填表
int ans=INT_MIN;
for(int i=1;i<=n;i++)
{
f[i]=max(nums[i-1],max(nums[i-1]*f[i-1],nums[i-1]*g[i-1]));
ans=max(ans,f[i]);
g[i]=min(nums[i-1],min(nums[i-1]*g[i-1],nums[i-1]*f[i-1]));
}
//返回值
return ans;
}
};
22.乘积为正数的最长子数组长度

题目解析:

算法原理:动态规划

解释一下状态转移方程:f[i]那边当nums[i]为负时,如果g[i-1]凑不出来负数最长子数组,那么这个位置为负数时,此时肯定也是没有子数组的,那么就是0,不然就是g[i-1]+1;而nums[i]为正时,不管前面能不能凑出来,我这个i位置是正数,那它加1就完事了;nums[i]为0时,此时长度肯定为0的,不需要考虑
解题代码:c++
cpp
class Solution {
public:
int getMaxLen(vector<int>& nums)
{
int n=nums.size();
//f[i]表示i位置乘积为正数的最长子数组长度
//g[i]表示i位置乘积为负数的最长子数组长度
vector<int> f(n+1),g(n+1);
//填表
int ans=0;
for(int i=1;i<=n;i++)
{
if(nums[i-1]>0)
{
f[i]=f[i-1]+1;
g[i]=g[i-1]==0?0:g[i-1]+1;
}
else if(nums[i-1]<0)
{
f[i]=g[i-1]==0?0:g[i-1]+1;
g[i]=f[i-1]+1;
}
ans=max(ans,f[i]);
}
return ans;
}
};
23.等差数列划分

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums)
{
//动态规划
//dp[i]表示以i位置为结尾所有子数组中有多少等差数列
int n=nums.size();
vector<int> dp(n);
//填表
int ans=0;
for(int i=2;i<n;i++)
{
int num1=nums[i-1]-nums[i-2];
int num2=nums[i]-nums[i-1];
dp[i]=num1==num2?dp[i-1]+1:0;
ans+=dp[i];
}
return ans;
}
};
24.最长湍流子数组

算法原理:动态规划

关于初始化:由于最差情况下,不管是f[i]还是g[i]的长度都是1,那么直接全初始化为1就好
解题代码:c++
cpp
class Solution {
public:
int maxTurbulenceSize(vector<int>& arr)
{
//f[i]表示以i位置为结尾的所有子数组中最后呈现"上升"状态下的最长湍流子数组的长度
//g[i]表示以i位置为结尾的所有子数组中最后呈现"下降"状态下的最长湍流子数组的长度
int n=arr.size();
//最差情况的长度也就是1,那么直接全初始化为1就好
vector<int> f(n,1),g(n,1);
//填表
int ans=1; //最差情况的结果也就是1
for(int i=1;i<n;i++)
{
if(arr[i]>arr[i-1])
{
//此时是下降趋势
g[i]=f[i-1]+1;
}
else if(arr[i]<arr[i-1])
{
//此时是上升趋势
f[i]=g[i-1]+1;
}
ans=max(max(f[i],g[i]),ans);
}
return ans;
}
};
25.单词拆分

案例解释:

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict)
{
//小优化,将词典中的单词放入哈希表中以便查找
unordered_set<string> hash;
for(auto& s:wordDict)
{
hash.insert(s);
}
//动态规划
int n=s.size();
vector<bool> dp(n+1);
//初始化
dp[0]=true; //保证后续填表是正确的
s=' '+s; //使原始字符串的下标统一 + 1
//填表
for(int i=1;i<=n;i++)
{
for(int j=i;j>=1;j--) //找到最后一个单词的起始位置
{
string ss=s.substr(j,i-j+1);
if(dp[j-1]==true&&hash.count(ss))
{
dp[i]=true;
break;
}
}
}
//返回值
return dp[n];
}
};
26.环绕字符串中唯一的子字符串

题目解析:

算法原理:动态规划

返回值这里的相同字串只能统计一次,所以得进行去重
解题代码:c++
cpp
class Solution {
public:
int findSubstringInWraproundString(string s)
{
//dp[i]表示以i位置为结尾的所有字串中有多少个在base中出现过
int n=s.size();
vector<int> dp(n,1);
//填表
vector<int> ans(26);
ans[s[0]-'a']=1;
for(int i=1;i<n;i++)
{
if(s[i-1]+1==s[i]||s[i-1]=='z'&&s[i]=='a')
{
dp[i]+=dp[i-1];
}
ans[s[i]-'a']=max(dp[i],ans[s[i]-'a']);
}
//返回值
int sum=0;
for(auto n:ans)
{
sum+=n;
}
return sum;
}
};
子序列问题
27.最长递增子序列

关于子序列和子数组:


算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int lengthOfLIS(vector<int>& nums)
{
//动态规划
int n=nums.size();
vector<int> dp(n,1);
//填表
int ans=1;
for(int i=1;i<n;i++)
{
//从0-(i-1)位置的子序列中的最大长度
for(int j=0;j<=i-1;j++)
{
//需要严格递增
if(nums[j]<nums[i])
{
//取最大
dp[i]=max(dp[j]+1,dp[i]);
}
}
//记录最长子序列
ans=max(ans,dp[i]);
}
//返回值
return ans;
}
};
28.摆动序列

题目解析:

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int wiggleMaxLength(vector<int>& nums)
{
//动态规划
//f[i]表示以i位置为结尾处于上升时的最长子序列的长度
//g[i]表示以i位置为结尾处于下降时的最长子序列的长度
int n=nums.size();
vector<int> f(n,1),g(n,1);
//填表
int ans=1;
for(int i=1;i<n;i++)
{
for(int j=0;j<=i-1;j++)
{
if(nums[j]<nums[i])
{
//此时为上升趋势
//前置得是下降的
f[i]=max(g[j]+1,f[i]);
}
else if(nums[j]>nums[i])
{
//此时为下降趋势
//前置得是上升的
g[i]=max(f[j]+1,g[i]);
}
}
ans=max(f[i],g[i]);
}
//返回值
return ans;
}
};
29.最长递增子序列的个数

在解决这道问题之前,我们先学习一下如果是在数组中找最大值出现的次数这一问题的解决方式:

源码如下:

弄懂了这一问题,我们就可以以此为基础然后结合子序列的动态规划思想来解决接下来的主问题了!
算法原理:动态规划
在统计次数之前得先知道其长度,所以需要两个状态表示,然后在统计最长长度时顺便统计其出现的次数,此时就可结合用上我们上面的解题方法啦

解题代码:c++
cpp
class Solution {
public:
int findNumberOfLIS(vector<int>& nums)
{
//动态规划
//len[i]表示以i位置为结尾的所有子序列中,最长递增子序列的"长度"
//count[i]表示以i位置为结尾的所有子序列中,最长递增子序列的"个数"
int n=nums.size();
vector<int> len(n,1),count(n,1);
//填表
int lenmax=1,countmax=1;
for(int i=1;i<n;i++)
{
for(int j=0;j<i;j++)
{
if(nums[j]<nums[i])
{
if(len[j]+1==len[i]) count[i]+=count[j];
else if(len[j]+1>len[i]) {
//更新最大长度
len[i]=len[j]+1;
//重置出现次数
count[i]=count[j];
}
}
}
//更新返回值
if(lenmax==len[i]) countmax+=count[i];
else if(lenmax<len[i]) lenmax=len[i],countmax=count[i];
}
//返回值
return countmax;
}
};
30.最长数对链

题目解析:

算法原理:动态规划

排完序时以i位置为结尾的对一定连不到后面的对的,要连也一定是和前面的对,此时才可以使用连续的子序列的动态规划解题方法来处理这道题
解题代码:c++
cpp
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs)
{
//动态规划
//dp[i]表示以i位置为结尾前面所有子序列中最长数对链的长度
int n=pairs.size();
vector<int> dp(n,1);
//先排个序
sort(pairs.begin(),pairs.end());
//填表
int ans=1;
for(int i=1;i<n;i++)
{
for(int j=0;j<i;j++)
{
if(pairs[j][1]<pairs[i][0])
{
dp[i]=max(dp[j]+1,dp[i]);
}
}
ans=max(dp[i],ans);
}
//返回值
return ans;
}
};
31.最长定差子序列

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int longestSubsequence(vector<int>& arr, int difference)
{
//动态规划
//dp[i]表示以i位置为结尾最长等差子序列的长度
//将i位置对应的值和dp[i]直接绑定到hash中,然后在哈希表中进行动态规划
int n=arr.size();
unordered_map<int,int> hash;
//初始化
hash[arr[0]]=1;
//填表
int ans=1;
for(int i=1;i<n;i++)
{
//表中有就是对应的dp值,没有就是0,0+1正好就是单独时的长度
hash[arr[i]]=hash[arr[i]-difference]+1;
ans=max(ans,hash[arr[i]]);
}
//返回值
return ans;
}
};
32.最长的斐波那契子序列的长度

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int lenLongestFibSubseq(vector<int>& arr)
{
//动态规划
//dp[i][j]表示以i位置以及j位置元素为结尾的所有子序列中,最长的
//斐波那契数列的长度
int n=arr.size();
vector<vector<int>> dp(n,vector<int>(n,2));
//优化:将arr[i]与i绑定放入哈希表中
unordered_map<int,int> hash;
for(int i=0;i<n;i++)
{
hash[arr[i]]=i;
}
//填表
int ans=2;
for(int j=2;j<n;j++) //固定最后一个位置
{
for(int i=1;i<j;i++) //固定倒数第二个位置
{
int a=arr[j]-arr[i];
if(hash.count(a)&&a<arr[i])
{
dp[i][j]=dp[hash[a]][i]+1;
}
ans=max(ans,dp[i][j]);
}
}
//返回值
return ans<3?0:ans;
}
};
33.最长等差数列

算法原理:动态规划

注意:此题不像上一题斐波那契数列题中说明了严格递增,此时就有可能出现重复的元素, 没有保证元素唯一,因此不能套用上一题的哈希表优化方式,不然就很可能覆盖掉前面的、更优的前驱位置!!
解题代码:c++
采用优化一和填表顺序一方法
cpp
class Solution {
public:
int longestArithSeqLength(vector<int>& nums)
{
//动态规划
int n=nums.size();
//哈希表优化
unordered_map<int,vector<int>> hash;
for(int i=0;i<n;i++)
{
hash[nums[i]].push_back(i);
}
//dp[i][j]表示以i位置以及j位置元素为结尾的所有子序列中,
//最长等差数列的长度
vector<vector<int>> dp(n,vector<int>(n,2));
//填表
int ans=2;
for(int j=2;j<n;j++) //固定最后一个位置
{
for(int i=1;i<j;i++) //固定倒数第二个位置
{
int a=2*nums[i]-nums[j];
if(hash.count(a))
{
auto& vec = hash[a];
//查找最后一个小于 i 的索引
auto it = lower_bound(vec.begin(), vec.end(), i);
if (it != vec.begin()) {
//lower_bound 返回的是第一个 >= i 的索引
//我们是要它前一个最后一个小于i的索引下标
int k = *prev(it);
dp[i][j] = dp[k][i] + 1;
}
}
ans=max(dp[i][j],ans);
}
}
//返回值
return ans;
}
};
采用优化二和填表顺序二方法
cpp
class Solution {
public:
int longestArithSeqLength(vector<int>& nums)
{
//动态规划
int n=nums.size();
//哈希表优化
unordered_map<int,int> hash;
//先把0位置进行初始化
hash[nums[0]]=0;
//dp[i][j]表示以i位置以及j位置元素为结尾的所有子序列中,
//最长等差数列的长度
vector<vector<int>> dp(n,vector<int>(n,2));//创建+初始化
//填表
int ans=2;
for(int i=1;i<n;i++) //先固定倒数第二个数
{
for(int j=i+1;j<n;j++) //枚举倒数第一个数
{
int a=2*nums[i]-nums[j];
if(hash.count(a))
{
dp[i][j]=dp[hash[a]][i]+1;
}
//返回值的话就是找最大值
ans=max(ans,dp[i][j]);
}
//i位置填完表之后放入哈希表中
//保证哈希表中永远只存当前i之前出现的元素的索引
hash[nums[i]]=i;
}
//返回值
return ans;
}
};
34.等差数列划分 II

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums)
{
//动态规划
//dp[i][j]表示以i位置以及j位置元素为结尾的所有子序列中
//等差数列的个数
int n=nums.size();
vector<vector<int>> dp(n,vector<int>(n));
//哈希表优化
unordered_map<long long,vector<int>> hash;
//哈希表中存元素和以及其下标数组
for(int i=0;i<n;i++)
{
hash[nums[i]].push_back(i);
}
//填表
int ans=0;
for(int j=2;j<n;j++) //先固定倒数第一个数
{
for(int i=1;i<j;i++) //枚举倒数第二个数
{
long long a=2LL*nums[i]-nums[j];
if(hash.count(a))
{
//枚举出a的所有下标
for(int k=0;k<hash[a].size();k++)
{
//如果这个下标小于i表示合法
if(hash[a][k]<i)
{
//就进行填表,此时加等上dp[hash[a][k]][i]
//的同时还需要加上这个位置和i、j一起构成的等差
dp[i][j]+=(dp[hash[a][k]][i]+1);
}
}
}
ans+=dp[i][j];
}
}
//返回值
return ans;
}
};
回文串问题
35.回文子串

题目解析:

算法原理:动态规划
其实动态规划算法并不是解决这题最优的算法,中心拓展算法、马拉车算法会更好解决这种回文串的问题,但在本处不够多介绍(马拉车算法的话用处太过于局限并且难,沉没成本还是比较高的)

解题代码:c++
cpp
class Solution {
public:
int countSubstrings(string s)
{
//动态规划
//dp[i][j]表示在i到j范围是否是回文串(i<=j)
int n=s.size();
vector<vector<bool>> dp(n,vector<bool>(n));
//填表,从下往上填写每一行
int ans=0;
for(int i=n-1;i>=0;i--)
{
for(int j=i;j<n;j++)
{
if(s[i]!=s[j])
{
dp[i][j]=false;
}
else if(s[i]==s[j])
{
if(i==j||i+1==j)
{
dp[i][j]=true;
}
else{
dp[i][j]=dp[i+1][j-1];
}
}
if(dp[i][j]==true)
ans++;
}
}
//返回值
return ans;
}
};
36.最长回文子串

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
string longestPalindrome(string s)
{
// 动态规划
// dp[i][j]表示i到j范围的字串是否是回文子串
// 再通过记录是回文字串中的i与j的最大距离即为最长回文字串长度
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n, false));
// 填表
int len = 1,begin=0;
for(int i=n-1;i>=0;i--)
{
for(int j=i;j<n;j++)
{
if(s[i]==s[j])
{
dp[i][j]=i+1<j?dp[i+1][j-1]:true;
}
if(dp[i][j]&&(j-i+1>len))
{
len=j-i+1;
begin=i;
}
}
}
// 返回值
return s.substr(begin,len);
}
};
37.分割回文串IV

算法原理:动态规划
ok,本题就是上面所说的用动态规划dp表来记录所有的字串是否为回文子串的思想可以让这道题变为简单题
我们的算法实现思路和上面的题是完全一样的:
cpp
for(int i=n-1;i>=0;i--)
{
for(int j=i;j<n;j++)
{
if(s[i]==s[j])
dp[i][j]=i+1<j?dp[i+1][j-1]:true;
}
}
就是拿dp[i] [j]表示在i到j范围的字串是否为回文子串
之后直接在通过两层循环来枚举出是否有两个分割位置能够将该字符串正好分为三个回文子串就好了,就只需要判断dp[0] [i] 、dp[i+1] [j]、dp[j+1] [n-1] 这三位置的dp表中是否都为true就好了
解题代码:c++
cpp
class Solution {
public:
bool checkPartitioning(string s)
{
//动态规划
//dp[i][j]表示在i到j范围的字串是否为回文子串
int n=s.size();
vector<vector<bool>> dp(n,vector<bool>(n));
//填表,从下往上填
for(int i=n-1;i>=0;i--)
{
for(int j=i;j<n;j++)
{
if(s[i]==s[j])
dp[i][j]=i+1<j?dp[i+1][j-1]:true;
}
}
//枚举是否有能将字符串分割成三个回文子串
for(int i=0;i<n-2;i++)
{
//如果第一个区间不是那下面就没必要做了,直接下次循环
if(!dp[0][i]) continue;
for(int j=i+1;j<n-1;j++)
{
if(dp[i+1][j]&&dp[j+1][n-1])
{
//表明可以被分割成三个回文子串
return true;
}
}
}
return false;
}
};
38.分割回文串II

题目解析:

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int minCut(string s)
{
//动态规划
//dp[i]表示:s[0,i]区间上的最长的字串最小的分割为回文子串的次数
int n=s.size();
//初始化为无穷是为了填表时dp[i]不干扰求最小值
vector<int> dp(n,INT_MAX);
//先用回文子串问题的方式保存一下字串的信息------优化
vector<vector<bool>> isPal(n,vector<bool>(n));
for(int i=n-1;i>=0;i--)
{
for(int j=i;j<n;j++)
{
if(s[i]==s[j])
isPal[i][j]=i+1<j?isPal[i+1][j-1]:true;
}
}
//再填dp表
for(int i=0;i<n;i++)
{
if(isPal[0][i])
//如果这个范围就是回文串,那不需要分割了
dp[i]=0;
else
{
for(int j=1;j<=i;j++)
{
//如果j到i的字串为回文串,那就在前一个位置基础上加1
if(isPal[j][i])
dp[i]=min(dp[j-1]+1,dp[i]);
}
}
}
//返回值
return dp[n-1];
}
};
39.最长回文子序列

题目解析:

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int longestPalindromeSubseq(string s)
{
//动态规划
//dp[i][j]表示在i到j范围的最长回文序列的长度
int n=s.size();
vector<vector<int>> dp(n,vector<int>(n));
//填表
for(int i=n-1;i>=0;i--)
{
for(int j=i;j<n;j++)
{
if(s[i]==s[j])
{
if(i==j)
dp[i][j]=1;
else if(i+1==j)
dp[i][j]=2;
else
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];
}
};
40.让字符串成为回文串的最少插入次数

题目解析:

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int minInsertions(string s)
{
//动态规划
//dp[i][j]表示i到j区间内的字串,使它成为回文串的最小插入次数
int n=s.size();
vector<vector<int>> dp(n,vector<int>(n));
//填表
for(int i=n-1;i>=0;i--)
{
for(int j=i+1;j<n;j++) //j=i+1,i==j时不需要处理,dp就是0
{
if(s[i]==s[j])
{
//就是等于内部的dp值
dp[i][j]=dp[i+1][j-1];
}
else
{
//那就是在两个内部范围[i,j-1]和[i+1,j]内存的最小次数再填个对应字符
//也就是存的值------次数+1
dp[i][j]=min(dp[i+1][j]+1,dp[i][j-1]+1);
}
}
}
//返回值
return dp[0][n-1];
}
};
两个数组的dp问题
41.最长公共子序列

题目解析:

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int longestCommonSubsequence(string text1, string text2)
{
//动态规划
//dp[i][j]表示t1的0-i区间以及t2的0-j区间内所有的子序列中
//最长公共子序列的长度
int m=text1.size(),n=text2.size();
//考虑空串情况就多开一行一列
vector<vector<int>> dp(m+1,vector<int>(n+1));
//字符串映射
text1=' '+text1,text2=' '+text2;
//填表
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
if(text1[i]==text2[j])
{
//末尾值相同,就让该位置dp值等于上一个位置的值加1即可
dp[i][j]=dp[i-1][j-1]+1;
}
else
{
//分三种情况,其中第三种情况包含在一二种情况中了
//仅考虑前两种情况即可,dp[i-1][j-1]不需要考虑
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
}
//返回值
return dp[m][n];
}
};
42.不相交的线

算法原理:动态规划
这道题通过题意其实很容易看出就是求最长公共子序列的问题,那么我们套用一下上一题的思路就好了

解题代码:c++
cpp
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2)
{
//动态规划
//dp[i][j]表示n1的0-i位置和n2的0-j位置处所有子序列中
//最长公共子序列长度
int m=nums1.size(),n=nums2.size();
//初始化就多开一行一列,默认为0就不会越界,也不会影响填表
vector<vector<int>> dp(m+1,vector<int>(n+1));
//填表
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;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[m][n];
}
};
43.不同的子序列

题目解析:

算法原理:动态规划

-
这里初始化额外开出的第一行表示当t字符串为空时,第一列为当s字符串为空时,那么s字符串为空时,除了t字符串也为空串时有一个字串,其他情况都是0,但是t字符串为空时s字符串始终都是有空串这一子串的,所以此时dp值就是1;这也就是初始化时新开的第一行填1和第一列除了第一个位置之外填0的原因了
-
然后这道题还有比较恶心的地方是虽然题目给了最终的结果不会超过整型范围,但是不代表中间的过程不会超,特别是我们这里在填表的过程中是有相加的步骤的,那么此时就可能会溢出,所以这里我们的dp表应该得从原先常用的int类型换成double类型的(这里亲身试验过long long也是会溢出的,毕竟还是整型,double 的表示范围是比 long long 大的)

解题代码:c++
cpp
class Solution {
public:
int numDistinct(string s, string t)
{
//动态规划
//dp[i][j]表示s字符串0-j区间内所有子序列中
//有多少个t字符串0-i区间内的字串
int m=t.size(),n=s.size();
vector<vector<double>> dp(m+1,vector<double>(n+1));
//初始化
for(int j=0;j<=n;j++) dp[0][j]=1;
//下标映射
s=" "+s,t=" "+t;
//填表
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
dp[i][j]=s[j]==t[i]?dp[i-1][j-1]+dp[i][j-1]:dp[i][j-1];
}
}
//返回值
return dp[m][n];
}
};
44. 通配符匹配

题目解析:

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
bool isMatch(string s, string p)
{
//动态规划
//dp[i][j]表示p[0,j]区间内的字串能否匹配s[0,i]区间内的字串
int m=s.size(),n=p.size();
vector<vector<bool>> dp(m+1,vector<bool>(n+1));
//下标映射
s=" "+s,p=" "+p;
//初始化
dp[0][0]=true; //p为空串肯定能完全匹配s为空串
//s为空,第一行剩下元素初始化的话得从1到n得判断p位置处的字符是否为*
//如果p中为*那就可以完全匹配,但只有有一个其他字符就不行了
for(int j=1;j<=n;j++)
{
if(p[j]=='*') dp[0][j]=true;
else break; //遇上第一个不是*号,那么后面的直接都是false
}
//填表
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
if(p[j]=='?') dp[i][j]=dp[i-1][j-1];
else if(p[j]=='*') dp[i][j]=dp[i][j-1]||dp[i-1][j];
else{
if(s[i]==p[j]) dp[i][j]=dp[i-1][j-1];
}
}
}
//返回值
return dp[m][n];
}
};
45.正则表达式匹配

题目解析:

算法原理:动态规划

- 初始化填新开的第一行表时,当s为空时,p只有有任意字符后面接的是 *号就可以进行匹配空串填true,但是只有有一个任意字符后面接的不是 *号,那么就匹配不了了,得填false,所以初始化时得进行判断
解题代码:c++
cpp
class Solution {
public:
bool isMatch(string s, string p)
{
//动态规划
//dp[i][j]表示p[0,j]范围的子串能否匹配s[0,i]范围的字串
int m=s.size(),n=p.size();
vector<vector<bool>> dp(m+1,vector<bool>(n+1));
//下标映射
s=" "+s,p=" "+p;
//初始化
dp[0][0]=true;
for(int j=2;j<=n;j+=2)
{
if(p[j]=='*')
dp[0][j]=true;
else break;
}
//填表
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
if(p[j]=='*')
{
dp[i][j]=dp[i][j-2]||(p[j-1]=='.'||s[i]==p[j-1])&&dp[i-1][j];
}
else
{
dp[i][j]=(s[i]==p[j]||p[j]=='.')&&dp[i-1][j-1];
}
}
}
//返回值
return dp[m][n];
}
};
46.交错字符串

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
bool isInterleave(string s1, string s2, string s3)
{
//动态规划
//dp[i][j]表示s1[1,i]区间字符串以及s2[1,j]区间字符串能否
//拼接凑成s3[1,i+j]区间的字符串
int m=s1.size(),n=s2.size();
//先判断一下特殊情况
if(m+n!=s3.size()) return false;
vector<vector<bool>> dp(m+1,vector<bool>(n+1));
//预处理
s1=" "+s1,s2=" "+s2,s3=" "+s3;
//初始化
dp[0][0]=true; //三个字符串都为空串时肯定可以拼接
//初始化第一行
for(int j=1;j<=n;j++)
{
if(s2[j]==s3[j]) dp[0][j]=true;
else break;
}
//初始化第一列
for(int i=1;i<=m;i++)
{
if(s1[i]==s3[i]) dp[i][0]=true;
else break;
}
//填表
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
//只要有一个情况存在就是true
dp[i][j]=(s1[i]==s3[i+j]&&dp[i-1][j])
||(s2[j]==s3[i+j]&&dp[i][j-1]);
}
}
//返回值
return dp[m][n];
}
};
47.两个字符串的最小ASCII删除和

题目解析:

算法原理:动态规划

-
这里是减去两倍的最大和,原因是sum记录的是两个字符串中所有字符的ascii值,公共子序列的字符是在s1和s2中都存在的,因此得减两倍
-
状态转移方程的第二、第三种情况均不是等号,只是因为求的是最大值,那么二、三包含了第四种情况,那么不用考虑第四种情况下,二、三才可以使用dp[i] [j-1]和dp[i-1] [j] 来表示
解题代码:c++
cpp
class Solution {
public:
int minimumDeleteSum(string s1, string s2)
{
//动态规划
//dp[i][j]表示s1[0,i]范围和s2[0,j]范围内所有子序列中
//公共子序列的ascill的最大值
int m=s1.size(),n=s2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1));
//填表
int sum=0,flag=1;//记录两字符串ascii总和
for(int i=1;i<=m;i++)
{
sum+=s1[i-1];
for(int j=1;j<=n;j++)
{
if(flag)
{
sum+=s2[j-1];
}
if(s1[i-1]==s2[j-1]) dp[i][j]+=dp[i-1][j-1]+s1[i-1];
dp[i][j]=max(max(dp[i-1][j],dp[i][j-1]),dp[i][j]);
}
flag=0;
}
//返回值
return sum-dp[m][n]*2;
}
};
48.最长重复子数组

算法原理:动态规划

-
这里注意的是子数组问题不能选择以0-i的区间了,因为后面的i+1是不确定是否能够跟在这个区间中最长子数组后面的(因为子数组要连续),子序列可以是因为子序列可以不连续,所以这里的状态表示就是以...为结尾这种了,n2的j也是一样的
-
返回值这里也是一样不能像前面的题返回最后位置的值,因为要连续,那么就不一定是最后一个位置处有最长子数组
解题代码:c++
cpp
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2)
{
//动态规划
//dp[i][j]表示n1以i为结尾的所有子数组和n2以j为结尾所有子数组中
//最长的公共子数组的长度
int m=nums1.size(),n=nums2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1));
//填表
int ans=0;
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
if(nums1[i-1]==nums2[j-1]) dp[i][j]=dp[i-1][j-1]+1;
ans=max(ans,dp[i][j]);
}
}
//返回值
return ans;
}
};
背包问题
背包问题概述

49.01背包

题目解析:

算法原理:动态规划
- 第二问的时候,是正好凑到为背包容量才有效填表,那么约定dp[i] [j]为-1来表示没有从前i个物品中选处总体积正好等于j这样的情况(这里要和等于0的时候区分开,等于0时也是有意义的),那么dp数组在初识化时新开的第一行从第一个位置开始表示没有物品,但是背包容量不为0,那么这种也是没有从前i个物品中选处总体积正好等于j这样的情况,填-1即可
题解代码:c++
cpp
#include <iostream>
#include <vector>
#include<algorithm>
using namespace std;
int main()
{
//动态规划
//第一问:
//dp[i][j]表示从前i个物品中选,总体积不超过j的所有选法中
//价值最大的选法
//第二问:
//dp[i][j]表示从前i个物品中选,总体积恰好为j的所有选法中
//价值最大的选法
//前置工作
int n=0,V=0;
cin>>n>>V;
//第一列记录物品体积,第二列记录物品价值
vector<vector<int>> num(n+1,vector<int>(2));
int v=0,w=0;
for(int i=1;i<=n;i++)
{
cin>>v>>w;
num[i][0]=v,num[i][1]=w;
}
//开始动态规划
vector<vector<int>> dp1(n+1,vector<int>(V+1));
vector<vector<int>> dp2(n+1,vector<int>(V+1));
//dp2的初始化
for(int j=1;j<=V;j++) dp2[0][j]=-1;
//填表
for(int i=1;i<=n;i++)
{
for(int j=1;j<=V;j++)
{
dp1[i][j]=dp1[i-1][j];
if(j-num[i][0]>=0)
dp1[i][j]=max(dp1[i][j],dp1[i-1][j-num[i][0]]+num[i][1]);
dp2[i][j]=dp2[i-1][j];
if(j-num[i][0]>=0&&dp2[i-1][j-num[i][0]]!=-1)
dp2[i][j]=max(dp2[i][j],dp2[i-1][j-num[i][0]]+num[i][1]);
}
}
//方案1答案
cout<<dp1[n][V]<<endl;
//方案2答案
int ret2=dp2[n][V]==-1?0:dp2[n][V];
cout<<ret2;
}
那么背包问题少不了优化方案(一般都是在空间上做优化):

-
原始的滚动数组:如果当前行的状态只依赖于上一行的状态,那么我们只需要两个一维数组,来回滚动,依次交替来完成填表
-
但是我们这里可以直接就使用一个一维数组来完成
优化后的代码如下:
cpp
#include <iostream>
#include <vector>
#include<algorithm>
using namespace std;
int main()
{
//动态规划
//第一问:
//dp[i][j]表示从前i个物品中选,总体积不超过j的所有选法中
//价值最大的选法
//第二问:
//dp[i][j]表示从前i个物品中选,总体积恰好为j的所有选法中
//价值最大的选法
//前置工作
int n=0,V=0;
cin>>n>>V;
//第一列记录物品体积,第二列记录物品价值
vector<vector<int>> num(n+1,vector<int>(2));
int v=0,w=0;
for(int i=1;i<=n;i++)
{
cin>>v>>w;
num[i][0]=v,num[i][1]=w;
}
//开始动态规划
//空间优化,只用一维数组来进行滚动
vector<int> dp1(V+1),dp2(V+1);
//dp2初始化
for(int j=1;j<=V;j++) dp2[j]=-1;
//填表
for(int i=1;i<=n;i++)
{
for(int j=V;j>=num[i][0];j--)
{
dp1[j]=max(dp1[j],dp1[j-num[i][0]]+num[i][1]);
if(dp2[j-num[i][0]]!=-1)
dp2[j]=max(dp2[j],dp2[j-num[i][0]]+num[i][1]);
}
}
//方案1答案
cout<<dp1[V]<<endl;
//方案2答案
int ret2=dp2[V]==-1?0:dp2[V];
cout<<ret2;
}
50.分割等和子集

算法原理:动态规划
题意已转换为从数组中选一些数出来,看能否使之和等于总数组和sum的一半,那么面临选与不选,要正好为sum的一半,此时不就像我们的01背包问题了吗?

-
新开的第一列初始化时,表示要求和为0的,那么我们不选数组中的任何一个元素就可以了,所以得填true,而第一行除开0位置,其他位置是要求和不为0,但数组中没有元素,那么肯定是选不出来的,默认false
-
当我们发现每一个数都是面临选或者不选时候,这个选择就是01背包问题的敏感点,研究这样的题目时,看到一个元素可以选或者不选,就可以试试往01背包问题去走
解题代码:c++
cpp
class Solution {
public:
bool canPartition(vector<int>& nums)
{
//动态规划
//dp[i][j]表示从前i个数中选,所有选法中是否能凑成j这个数
int n=nums.size();
//求一下数组和
int sum=0;
for(auto N:nums) sum+=N;
//如果sum是奇数,那么肯定是分割不了的
if(sum%2) return false;
//dp
int target=sum/2;
vector<vector<bool>> dp(n+1,vector<bool>(target+1));
//初始化
for(int i=0;i<=n;i++) dp[i][0]=true;
//填表
for(int i=1;i<=n;i++)
{
for(int j=1;j<=target;j++)
{
if(j>=nums[i-1])
dp[i][j]=dp[i-1][j-nums[i-1]]||dp[i-1][j];
else
dp[i][j]=dp[i-1][j];
}
}
//返回值
return dp[n][target];
}
};
空间优化后的代码:
cpp
class Solution {
public:
bool canPartition(vector<int>& nums)
{
//动态规划
//dp[i][j]表示从前i个数中选,所有选法中是否能凑成j这个数
int n=nums.size();
//求一下数组和
int sum=0;
for(auto N:nums) sum+=N;
//如果sum是奇数,那么肯定是分割不了的
if(sum%2) return false;
//dp
int target=sum/2;
//优化为一维滚动数组
vector<bool> dp(target+1);
//初始化
dp[0]=true;
//填表
for(int i=1;i<=n;i++)
{
for(int j=target;j>=nums[i-1];j--)
{
dp[j]=dp[j]||dp[j-nums[i-1]];
}
}
//返回值
return dp[target];
}
};
51.目标和

算法原理:动态规划

- 这里的初始化要注意了,由于题目当中的数组元素是可能为0的,所以新开的第一列是要凑和为0的情况,那么其实是有可能能凑到的,并且选法也可能有多种,那么就得进表中去进行dp计算的操作了,所以这里进行初始化比较麻烦,但是我们不用初始化这第一列,因为要初始化的前提是会避免影响填表操作,可是我们看填表操作,只有dp[i-1] [j-nums[i-1]]会有可能影响,但是我们是在j>=nums[i-1]条件成立时才进去执行这条代码的,因此并不会越界影响填表,也就没有必要初始化
题解代码:c++
cpp
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target)
{
//动态规划
//所有+后数和为a,-后数和的绝对值为b,总和sum,a-b=target,a+b=sum
//那么推出a=(target+sum)/2
//也就是说我们只需要看+号即可,那么就是原数组中元素怎么样,那就怎么样
//dp[i][j]表示从数组前i个元素中选出元素所有之和能等于j数的方法
int n=nums.size();
int sum=0;
for(auto N:nums) sum+=N;
if((sum + target) % 2 != 0 || abs(target) > sum) return 0;
int a = (sum + target)/2;
if(a < 0) return 0;
vector<vector<int>> dp(n+1,vector<int>(a+1));
//初始化
dp[0][0]=1;
//填表
for(int i=1;i<=n;i++)
{
for(int j=0;j<=a;j++)
{
dp[i][j]=dp[i-1][j];
if(j>=nums[i-1])
dp[i][j]+=dp[i-1][j-nums[i-1]];
}
}
//返回值
return dp[n][a];
}
};
优化之后的代码:
cpp
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target)
{
//动态规划
int n=nums.size();
int sum=0;
for(auto N:nums) sum+=N;
int a=(target+sum)/2;
if(a<0||(target+sum)%2!=0) return 0;
//优化dp为一维滚动数组
vector<int> dp(a+1);
//初始化
dp[0]=1;
//填表
for(int i=1;i<=n;i++)
{
for(int j=a;j>=nums[i-1];j--)
{
dp[j]+=dp[j-nums[i-1]];
}
}
//返回值
return dp[a];
}
};
52.最后一块石头的重量 II

题目解析:

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int lastStoneWeightII(vector<int>& stones)
{
//动态规划
//得将题目转换为在数组中选一些数,使之和尽可能等于sum/2
//dp[i][j]表示从前i个元素中选,总和不超过j,此时的最大和
int n=stones.size();
int sum=0;
for(auto N:stones) sum+=N;
int target=sum/2;
//dp
vector<vector<int>> dp(n+1,vector<int>(target+1));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=target;j++)
{
dp[i][j]=dp[i-1][j];
if(j>=stones[i-1])
dp[i][j]=max(dp[i][j],dp[i-1][j-stones[i-1]]+stones[i-1]);
}
}
//返回值
return sum-2*dp[n][target];
}
};
53.完全背包

算法原理:动态规划

- 第一问的初始化新开的第一行,没有物品但有背包容量时,需要不超过背包容量,那么空背包也是有意义的,填0表示最大价值为0即可(就相当于不需要初始化;而第二问的初始化,需要恰好等于背包容量,也就是一定要装满,空背包自然就没有意义,除了背包也为空的第一个位置,其他都填-1
解题代码:c++
cpp
#include <iostream>
#include<vector>
using namespace std;
int main()
{
//前置工作
int n=0,V=0;
cin>>n>>V;
//第一行表示物品体积,第二行表示物品价值
vector<vector<int>> nums(n,vector<int>(2));
for(int i=0;i<n;i++)
{
int v,w;
cin>>v>>w;
nums[i][0]=v,nums[i][1]=w;
}
//动态规划
//第一问
//dp1[i][j]表示从前i个物品中选,总体积不超过j
//所有选法中,最大的价值
vector<vector<int>> dp1(n+1,vector<int>(V+1));
//填表
for(int i=1;i<=n;i++)
{
for(int j=0;j<=V;j++)
{
dp1[i][j]=dp1[i-1][j];
if(j>=nums[i-1][0])
dp1[i][j]=max(dp1[i][j],dp1[i][j-nums[i-1][0]]+nums[i-1][1]);
}
}
cout<<dp1[n][V]<<endl;
//第二问
//dp2[i][j]表示从前i个物品中选,总体积等于j
//所有选法中,最大的价值
vector<vector<int>> dp2(n+1,vector<int>(V+1));
//初始化,没有物品但是要等于j,此时是不存在无意义的
for(int j=1;j<=V;j++) dp2[0][j]=-1;
//填表
for(int i=1;i<=n;i++)
{
for(int j=0;j<=V;j++)
{
dp2[i][j]=dp2[i-1][j];
if(j>=nums[i-1][0]&&dp2[i][j-nums[i-1][0]]!=-1)
dp2[i][j]=max(dp2[i][j],dp2[i][j-nums[i-1][0]]+nums[i-1][1]);
}
}
int ans=dp2[n][V]==-1?0:dp2[n][V];
cout<<ans;
}
关于优化:
这里要注意的是完全背包相关问题由于状态方程和01背包有点不同,那么其空间优化代码也有些不同
- 由于优化后dp[j]的值是依赖当前状态的dp[j-v[i]]的,dp[j-v[i]]在dp[j]的左边,那么我们得保证在更新dp[j]时,dp[j-v[i]]是更新好的,因此得从左往右去遍历(01是从右往左,因为01是依赖上一个状态的dp[j-v[i]],不能从左往右覆盖,因此才是从右往左)

此时的代码如下:
cpp
#include <iostream>
#include<vector>
using namespace std;
int main()
{
//前置工作
int n=0,V=0;
cin>>n>>V;
//第一行表示物品体积,第二行表示物品价值
vector<vector<int>> nums(n,vector<int>(2));
for(int i=0;i<n;i++)
{
int v,w;
cin>>v>>w;
nums[i][0]=v,nums[i][1]=w;
}
//动态规划
//第一问
//dp1[i][j]表示从前i个物品中选,总体积不超过j
//所有选法中,最大的价值
//优化
vector<int> dp1(V+1);
//填表
for(int i=1;i<=n;i++)
{
for(int j=nums[i-1][0];j<=V;j++)
{
dp1[j]=max(dp1[j],dp1[j-nums[i-1][0]]+nums[i-1][1]);
}
}
cout<<dp1[V]<<endl;
//第二问
//dp2[i][j]表示从前i个物品中选,总体积等于j
//所有选法中,最大的价值
//优化
vector<int> dp2(V+1);
//初始化,没有物品但是要等于j,要装满,此时是不存在、无意义的
for(int j=1;j<=V;j++) dp2[j]=-1;
//填表
for(int i=1;i<=n;i++)
{
for(int j=nums[i-1][0];j<=V;j++)
{
if(dp2[j-nums[i-1][0]]!=-1)
dp2[j]=max(dp2[j],dp2[j-nums[i-1][0]]+nums[i-1][1]);
}
}
int ans=dp2[V]==-1?0:dp2[V];
cout<<ans;
}
当然这里如果不像在循环中判断dp2[j-nums[i-1] [0]]!=-1的话其实可以在初始化时将其置为-0x3f3f3f3f而不是-1,因为这里要判断的本质原因是不想让无效的状态值干扰下面的状态方程,也就是max(dp2[j],dp2[j-nums[i-1] [0]]+nums[i-1] [1]);这里是求max,那么其实我们只需要让无效的状态足够小,就不会影响填表了,而前面使用的-1还不够小,所以需要判断,那么使用-0x3f3f3f3f这个足够小的数并且这个值在众多示例中大概率不会被加的超过0,那么选这个数就比较合适(如果选INT_MIN,当有减法减去正数的时候就会溢出),就不会影响填表,因此也无需判断了
54.零钱兑换

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
//动态规划
//dp[i][j]表示从前i个元素中选,能凑成j金额所需的最少硬币个数
int n=coins.size();
vector<vector<int>> dp(n+1,vector<int>(amount+1));
//初始化
for(int j=1;j<=amount;j++) dp[0][j]=0x3f3f3f3f;
//填表
for(int i=1;i<=n;i++)
{
for(int j=0;j<=amount;j++)
{
dp[i][j]=dp[i-1][j];
if(j>=coins[i-1])
dp[i][j]=min(dp[i][j],dp[i][j-coins[i-1]]+1);
}
}
//返回值
return dp[n][amount]==0x3f3f3f3f?-1:dp[n][amount];
}
};
优化后:
cpp
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
//动态规划
//dp[i][j]表示从前i个元素中选,能凑成j金额所需的最少硬币个数
int n=coins.size();
//优化后
vector<int> dp(amount+1);
//初始化
for(int j=1;j<=amount;j++) dp[j]=0x3f3f3f3f;
//填表
for(int i=1;i<=n;i++)
{
for(int j=coins[i-1];j<=amount;j++)
{
dp[j]=min(dp[j],dp[j-coins[i-1]]+1);
}
}
//返回值
return dp[amount]==0x3f3f3f3f?-1:dp[amount];
}
};
55.零钱兑换II

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int change(int amount, vector<int>& coins)
{
//动态规划
//dp[i][j]表示从前i个元素中选,能凑成j金额的组合数
int n=coins.size();
//这里在计算过程中的结果可能会溢出,所以使用double类型
vector<vector<double>> dp(n+1,vector<double>(amount+1));
//初始化
dp[0][0]=1;
//填表
for(int i=1;i<=n;i++)
{
for(int j=0;j<=amount;j++)
{
dp[i][j]=dp[i-1][j];
if(j>=coins[i-1])
dp[i][j]=dp[i][j]+dp[i][j-coins[i-1]];
}
}
//返回值
return dp[n][amount];
}
};
优化后:
cpp
class Solution {
public:
int change(int amount, vector<int>& coins)
{
//动态规划
//dp[i][j]表示从前i个元素中选,能凑成j金额的组合数
int n=coins.size();
//这里在计算过程中的结果可能会溢出,所以使用double类型
//优化
vector<double> dp(amount+1);
//初始化
dp[0]=1;
//填表
for(int i=1;i<=n;i++)
{
for(int j=coins[i-1];j<=amount;j++)
{
dp[j]=dp[j]+dp[j-coins[i-1]];
}
}
//返回值
return dp[amount];
}
};
56.完全平方数

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int numSquares(int n)
{
//动态规划
//dp[i][j]表示从前i个数中选,总和正好等于j的最少数个数
int m = (int)sqrt(n);
vector<vector<int>> dp(m+1,vector<int>(n+1));
//初始化
for(int j=1;j<=n;j++) dp[0][j]=0x3f3f3f3f;
//填表
for(int i=1;i<=m;i++)
{
for(int j=0;j<=n;j++)
{
dp[i][j]=dp[i-1][j];
if(j>=i*i)
dp[i][j]=min(dp[i][j],dp[i][j-i*i]+1);
}
}
//返回值
return dp[m][n];
}
};
优化后:
cpp
class Solution {
public:
int numSquares(int n)
{
//动态规划
//dp[i][j]表示从前i个数中选,总和正好等于j的最少数个数
//优化
int m=sqrt(n);
vector<int> dp(n+1);
//初始化
for(int j=1;j<=n;j++) dp[j]=0x3f3f3f3f;
//填表
for(int i=1;i<=m;i++)
{
for(int j=i*i;j<=n;j++)
{
dp[j]=min(dp[j],dp[j-i*i]+1);
}
}
//返回值
return dp[n];
}
};
二维费用背包问题
57. 一和零

题目解析:
二维费用背包问题就是比之前的背包问题多了一个限制,之前可能只有体积的限制,现在二维就可能会多一个重量的限制,同时二维费用背包问题也分为01和完全背包的类型

算法原理:动态规划

- 初始化时由于是关键词是不超过,新开的第一行就是表示字符串数组中没有元素时,此时dp表自然就是0,那么不需要进行初始化
解题代码:c++
cpp
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n)
{
//动态规划,二位费用背包问题
//dp[i][j][k]表示从前i个字符串中选,字符0的个数不超过j
//字符1的个数不超过k,所有的选法中最大的长度
int len=strs.size();
vector<vector<vector<int>>> dp(len+1,vector<vector<int>>(m+1,vector<int>(n+1)));
//填表
for(int i=1;i<=len;i++)
{
//设在每个位置字符串字符0的个数为a,字符1的个数为b
int a=0,b=0;
string s=strs[i-1];
for(auto c:s)
{
if(c=='0') a++;
else b++;
}
for(int j=0;j<=m;j++)
{
for(int k=0;k<=n;k++)
{
dp[i][j][k]=dp[i-1][j][k];
if(j>=a&&k>=b)
dp[i][j][k]=max(dp[i][j][k],dp[i-1][j-a][k-b]+1);
}
}
}
//返回值
return dp[len][m][n];
}
};
优化后:
cpp
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n)
{
//动态规划,二位费用背包问题
//dp[i][j][k]表示从前i个字符串中选,字符0的个数不超过j
//字符1的个数不超过k,所有的选法中最大的长度
int len=strs.size();
vector<vector<int>> dp(m+1,vector<int>(n+1));
//填表
for(int i=1;i<=len;i++)
{
//设在每个位置字符串字符0的个数为a,字符1的个数为b
int a=0,b=0;
string s=strs[i-1];
for(auto c:s)
{
if(c=='0') a++;
else b++;
}
for(int j=m;j>=a;j--)
{
for(int k=n;k>=b;k--)
{
dp[j][k]=max(dp[j][k],dp[j-a][k-b]+1);
}
}
}
//返回值
return dp[m][n];
}
};
58.盈利计划

题目解析:

算法原理:动态规划

-
在状态转移方程这里,由于题目是总利润至少为k,那么选i的情况时的k-p[i]是可以小于0的(不像j-g[i]一定要大于等于0),意思是k<p[i],i位置的利润已经大于k了,那是正好符合状态表示的,但是我们dp表数组下标是不能小于0的,所以我们就让这种情况在0位置处找,也就是dp表的k是max(0,k-p[i]),一旦出现小于0的情况就取0了,意思就是再去找至少为0的dp
-
初始化那里,当没有计划,也就是没有任务和利润时,不管我们的人数是多少,我们都可以选择一个空集,也算一种选法,也就是dp[0] [j] [0]在j在0到n的情况下都填1
解题代码:c++
cpp
class Solution {
public:
int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit)
{
// 二维费用01背包动态规划问题
//dp[i][j][k]表示从前i个计划中选,总人数不超过j
//总利润至少为k,其中一共有多少种选法
int len=group.size();
const int MOD = 1000000007;
vector<vector<vector<int>>> dp(len+1,vector<vector<int>>(n+1,vector<int>(minProfit+1)));
//初始化
for(int j=0;j<=n;j++) dp[0][j][0]=1;
//填表
for(int i=1;i<=len;i++)
{
for(int j=0;j<=n;j++)
{
for(int k=0;k<=minProfit;k++)
{
dp[i][j][k]=dp[i-1][j][k]%MOD;
if(j>=group[i-1])
dp[i][j][k]+=dp[i-1][j-group[i-1]][max(0,k-profit[i-1])]%MOD;
}
}
}
//返回值
return dp[len][n][minProfit]%MOD;
}
};
优化后:
cpp
class Solution {
public:
int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit)
{
// 二维费用01背包动态规划问题
//dp[i][j][k]表示从前i个计划中选,总人数不超过j
//总利润至少为k,其中一共有多少种选法
int len=group.size();
const int MOD = 1000000007;
//优化
vector<vector<int>> dp(n+1,vector<int>(minProfit+1));
//初始化
for(int j=0;j<=n;j++) dp[j][0]=1;
//填表
for(int i=1;i<=len;i++)
{
for(int j=n;j>=group[i-1];j--)
{
for(int k=minProfit;k>=0;k--)
{
dp[j][k]+=dp[j-group[i-1]][max(0,k-profit[i-1])];
dp[j][k]%=MOD;
}
}
}
//返回值
return dp[n][minProfit];
}
};
似包非包
59. 组合总和 Ⅳ

组合是无序的,1、1、2三个数组合是一种情况,排列是有序的,那么1、1、2就分别根据排序不同有三种情况,我们这题其实是排列的问题

算法原理:动态规划

解题代码:c++
cpp
class Solution {
public:
int combinationSum4(vector<int>& nums, int target)
{
//似包非包,动态规划,这题是一道排列问题
//dp[i]表示凑成总和为i,一共有多少种排列数(本质是线性dp)
int n=nums.size();
//填表过程中可能会出现整型溢出问题,这里采用double类型
vector<double> dp(target+1);
//初始化,当要凑成0时,在数组中选空集即为一种情况
dp[0]=1;
//填表
for(int i=1;i<=target;i++)
{
for(int j=0;j<n;j++)
{
if(i>=nums[j])
dp[i]+=dp[i-nums[j]];
}
}
//返回值
return dp[target];
}
};
卡特兰数
60.不同的二叉搜索树

算法原理:动态规划

- 这里的j当作根节点,那么j的左子树就是j-1-1+1=j-1个节点,右子树就是i-(j+1)+1=i-j个节点,那么两边每个节点两两进行组合都可以和根一起构成一颗二叉搜索树(也就是两边节点数相乘),所以dp[i]就是加等上此时j的dp[j-1]*dp[i-j]
解题代码:c++
cpp
class Solution {
public:
int numTrees(int n)
{
//动态规划,本质也是排列问题
//dp[i]表示节点个数为i时,一共有多少种二叉搜索树
vector<int> dp(n+1);
//初始化,当节点个数为i时可以有空树
dp[0]=1;
//填表
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
{
dp[i]+=dp[j-1]*dp[i-j];
}
}
//返回值
return dp[n];
}
};