前言:
哈哈哈哈哈哈哈哈哈,今天开个新坑,以后不定期更新算法,正好动态规划我以前有学习过一些,所以今天给大家更新动态规划这个算法:
那么我们要讲的动态规划问题有以下几类:
①:斐波那契模型:最简单的一类,作为引入
②:路径问题
③:多状态dp问题
④:子数组,子串系列
⑤:子序列问题:
⑥:回文串问题
⑦:双数组dp
⑧:01背包
⑨:完全背包
⑩:二维费用的背包问题
当然,我们也会给出相应的做题方法.......
废话少说,直接开始吧:
正文:
一般做题方法:
这种问题呢,我们分为五步:
①:状态表示:
即dp表中的值所代表的含义
从哪里来呢?
1.题目要求
2.经验+题目要求
3.重复出现的子问题
②:状态转移方程:
即 dpi 如何计算:
当然计算的时候要用到前面的数据,比如说dpi-1
③:初始化
为了保证填表的时候不会越界
④:填表顺序:
为了保证当我们计算当前状态的时候,所需的状态已经计算过了
⑤:返回值
题目要求+状态表示
那么,接下来正式开始题目:
①:斐波那契模型
1:第N个泰波那契数(leetcode 1137)

那么遇到这么一道题,我们该如何思考呢?
①:状态表示:
首先,我们要求的是第n个泰波那契数的值,所以我们根据题目要求,就知道了我们最后需要的是值,所以我们的状态表示:
第n个泰波那契数的值
②:状态转移方程:
题目已经给出了:
dpi=dpi-1+dpi-2+dpi-3
③:初始化:
题目已经给出前三个数,我们直接填好即可;
④:填表顺序:
直接从左向右填表即可
⑤:返回值:
dpn
所以我们进行编码:
class Solution {
public:
int tribonacci(int n) {
//先来个dp表,首先dp表有n个数
vector<int> dp(38);
dp[0]=0;
dp[1]=1;
dp[2]=1;
for(int i=3;i<=n;i++)
{
dp[i]=dp[i-1]+dp[i-2]+dp[i-3];
}
return dp[n];
}
};
运行一下试试呢?

OK,结果如我所料:那如果我想优化一下呢?
3我发现,我似乎一共只需要四个连续的空间就可以完成这道题了,通过连续的三个空间算出第四个数,然后后面的数再把自己的值给前面的数,那么我们只需四个空间一直滚动下去,不就可以完成这道题么,这就叫做滚动数组
class Solution {
public:
int tribonacci(int n) {
if(n==0)
return 0;
if(n==1 || n==2)
return 1;
int a=0;
int b=1;
int c=1;
int d=0;
for(int i=3;i<=n;i++)
{
d=a+b+c;
a=b;
b=c;
c=d;
}
return d;
}
};
因为这是第一道题,所以我会给大家讲的啰嗦一些,接下来我们挑重点讲:
2.使用最小花费爬楼梯(746)

这道题呢,显然,要记录最小花费,是一个和,所以我们的状态表示就是:
到达第i个位置,花了多少钱
而状态转移方程,是需要前面两个状态,dpi-1+costi-1,dpi-2+costi-2的最小值
初始化前面两位初始为0即可,填表顺序从左向右,编码如下:
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size());
dp[0]=0;
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 min(dp[cost.size()-1]+cost[cost.size()-1],dp[cost.size()-2]+cost[cost.size()-2]);
}
};
3.解码方法(91)

OK,现在开始上强度咯,相信大家看到这道题有点懵,所以我要画图了

ok,相信这么画一下大家的思路就清楚了,所以我们开始编码
class Solution {
public:
int numDecodings(string s) {
vector<int> dp(s.size());
int e=s[0]-'0';
if(1<=e && e<=9)
{
dp[0]=1;
}
if(s.size()==1)
{
return dp[0];
}
e=s[1]-'0';
int count=0;
if(1<=e && e<=9)
{
count++;
}
int dou=(s[0]-'0')*10+e;
if(dou>=10 && dou<=26)
{
count++;
}
dp[1]=count;
for(int i=2;i<s.size();i++)
{
count=0;
e=s[i]-'0';
if(e>=1 && e<=9)
{
count+=dp[i-1];
}
dou=(s[i-1]-'0')*10+e;
if(dou>=10 && dou<=26)
{
count+=dp[i-2];
}
dp[i]=count;
}
for(auto e:dp)
{
if(e==0)
return 0;
}
return dp[s.size()-1];
}
};
同时呢,通过这道题,希望大家可以记住一个状态表示确立的经验:
以i位置为结尾,.......
毕竟嘛,我们的经验也是确定状态表示的关键依据之一,做得多了才能积累经验,越来越顺手
②:路径问题
1.不同路径(62)

那么看到这道题呢,它是一个二维的数组,我们想要解决它,我们可以凭借我们的经验,在某个位置,当前有多少种走法?这不就是我们的状态表示么
所以:我们就需要创建一个二维的dp表;
那么,状态转移方程呢?
显然,我们在一个位置的时候,只能向下或者向右走,所以我们上一个状态是上面一格或者左边一格,二者相加就得到了状态转移方程
dpij=dpi-1j+dpij-1
初始化:
这题的初始化是我最想说的
我们在初始化的时候,vector默认会给你初始化成0的,这个无需考虑,但是
我们在填表第一行或者第一列的时候,那么就会越界,所以我们可以多创建一行,再多创建一列,来作为缓冲,而整个表初始都是0,不会对我们的填表造成错误,同时我们把第一行第二个元素或者第一列第二个元素设置为1,以供dp11初始化
填表:
从上往下,从左往右就可以
返回值:
我们只需返回最后一个元素即可,即dpmn
接下来进行编码:
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m+1,vector<int>(n+1));
if(m==1 && n==1)
{
return 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];
}
};
2.不同路径II(63)

至于这道题嘛,不就是上一道题加了一个有障碍物么,那么就相当于新增了一个判断条件,如果当前所处位置是障碍物,那我为0不就好了,这样它之后的位置,相当于忽略了它。
但是注意,由于我们在创建dp表的时候,多创建了一行一列,所以映射到原来的题目给出的表一定不要忘了给下标分别减一;
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m=obstacleGrid.size();
int n=obstacleGrid[0].size();
vector<vector<int>> dp(m+1,vector<int>(n+1));
if(m==n && m==0)
{
return 1;
}
dp[0][1]=1;
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
if(obstacleGrid[i-1][j-1]!=1)
{
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
return dp[m][n];
}
};
3、珠宝的最高价值(166)
这道题也很简单,与上面两道题的差别也就是状态表示了,这道题的状态表示是,走到了i.j,自己身上有多少钱的珠宝,我们直接编码了
class Solution {
public:
int jewelleryValue(vector<vector<int>>& frame) {
int m=frame.size();
int n=frame[0].size();
vector<vector<int>> dp(m+1,vector<int>(n+1));
if(m==1 && n==1)
{
return frame[0][0];
}
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];
}
};
4.下降路径最小和(931)

看着唬人,其实不就也是从上往下,可以找下面的三个与自己相邻的元素,直到最底下求最小值么,非常简单,只有初始化的时候需要考虑一下,因为我们找的是下面的三个与自己相邻的元素,那么最左边和最右边的,我们就会越界咯,所以我们左右各增加一列,同是我们在填第一行dp表的时候,需要上层的一行,所以我们还要在最上面多加一行。
上面多加的那一行呢,为了不影响结果,我们设为0;
至于左右新增的那两列,因为我们求的是三者的最小值,所以为了不影响结果,我们设置为INT_MAX;
ok,别的地方就没有什么要强调的了,直接编码
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& matrix) {
int row=matrix.size();
int col=matrix[0].size();
vector<vector<int>> dp(row+1,vector<int>(col+2));
for(int i=0;i<col+2;i++)
{
dp[0][i]=0;
}
for(int j=0;j<=row;j++)
{
dp[j][0]=INT_MAX;
dp[j][col+1]=INT_MAX;
}
for(int i=1;i<=row;i++)
{
for(int j=1;j<=col;j++)
{
dp[i][j]=min(dp[i-1][j-1],min(dp[i-1][j],dp[i-1][j+1]))+matrix[i-1][j-1];
}
}
int min=INT_MAX;
for(int j=1;j<=row;j++)
{
if(dp[row][j]<min)
{
min=dp[row][j];
}
}
return min;
}
};
5.最小路径和(64)

非常简单,需要注意的只有,由于我要最小值,所以我们把上面和左边新增加的那一行与那一列都设为INT_MAX。而我们第一个数,在填的时候,需要dp01和dp10,所以我们把这俩初始化为0,别的没什么好说的了
直接编码:
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int row=grid.size();
int col=grid[0].size();
vector<vector<int>> dp(row+1,vector<int>(col+1,INT_MAX));
dp[0][1]=0;
dp[1][0]=0;
for(int i=1;i<=row;i++)
{
for(int j=1;j<=col;j++)
{
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i-1][j-1];
}
}
return dp[row][col];
}
};
6.地下城游戏(174)

本系列第一道困难题,但其实一点也不困难,别被他吓到;
上面叽里咕噜说一大堆,不还是要我求什么最小路径么;
但是我们注意,这道题一定要倒着求,别正着做;
如果我正着做,那我第一步如果就扣血呢?那我直接死了啊,所以此时就必然要有个初始血量吧,这是我们最终要求的啊?!然后如果我在路上死了呢?那我此刻就得对我的初始血量继续动刀了,太麻烦了
所以我们倒着做的话,从右下角的格子出发,我们首先要确定状态表示:
那么不就是我们走到i,j所需要的最小血量么?
然后状态转移方程呢?dpij=min(dpij+1,dpi+1j)-dungeonij
并且,如果我们此时需要的最小血量如果为负数的话,注意我如果血量为负数会直接暴毙,所以如果我血量为负,我直接设为1
切记,我们此时初始化,是在右边和下面增加了一列一行
而我们要的是最小值,所以初始值设为INT_MAX,而我初始化的时候,需要dpmn-1,dpm-1n,所以我们把它们两个设为1
最后呢,我们要返回的,自然就是初始位置dp00
所以直接编码:
class Solution {
public:
int calculateMinimumHP(vector<vector<int>>& dungeon) {
int row=dungeon.size();
int col=dungeon[0].size();
vector<vector<int>> dp(row+1,vector<int>(col+1,INT_MAX));
dp[row-1][col]=1;
dp[row][col-1]=1;
for(int i=row-1;i>=0;i--)
{
for(int j=col-1;j>=0;j--)
{
dp[i][j]=min(dp[i+1][j],dp[i][j+1])-dungeon[i][j];
//为什么是min?
//因为我要求的是可以救到公主的最小的
dp[i][j]=max(dp[i][j],1);
//若需要生命值为负数,那么置为一
}
}
return dp[0][0];
}
};
③.多状态dp
1.打家劫舍II(213)

这个偷东西嘛....讲究还挺多,不能同时连着偷,而且村子还是首尾相连的,看上去也是非常的麻烦啊,但是我们想想,既然他相连,我们可不可以通过分类讨论来把它转化为不相连?
还是画个图吧
如果我偷第一家,那么我第二家和最后一家就不许我偷了
如果我不偷第一家,那么我可以偷第二家和最后一家
所以我们通过偷不偷第一家的分类讨论,得到了两个范围!
一个是2------n-2,一个是1-----n-1
那不就变成两个普通的打家劫舍了么,所以我们可以写个函数
咦?那普通打家劫舍怎么写呀?没错,我们要分类当前是否要偷,而一个dp表似乎满足不了我们了
所以,我们来两个dp表
一个dp表,我们记录偷了这家以后的此时拥有的最大财产
一个dp表,我们记录不偷这家,此时我们所拥有的最大财产
那我们的状态转移方程呢?画个图吧

至于填表顺序,自然就是从左往右,两个表一起填咯
初始化的话,没什么需要在意的,不管了
返回值的话,也没什么吧?直接编码了
class Solution {
public:
int steal(vector<int>& nums,int left,int right)
{
if(left>right)
{
return 0;
}
int n=nums.size();
vector<int> dpf(n);
vector<int> dpg(n);
dpf[left]=nums[left];
for(int i=left+1;i<=right;i++)
{
dpf[i]=dpg[i-1]+nums[i];
dpg[i]=max(dpf[i-1],dpg[i-1]);
}
return max(dpf[right],dpg[right]);
}
int rob(vector<int>& nums) {
int n=nums.size();
//状态
//以i为终点,能偷到的最多金额
//dpf,是偷第i家
//dpg,不偷这家
return max(steal(nums,2,n-2)+nums[0],steal(nums,1,n-1));
}
};
2.买卖股票的最佳时间包含冷冻期(309)

老师赚钱枯燥就来看看这接下来的几道题,好好看,好好学,你也会成为下一个天才交易员

嗯.....这道题状态挺多呀,买入,可交易,冷冻期
所以我们需要三个dp表咯
我们来画个图

所以状态转移方程不是很好写么
既然我要最大的,那么:
dpi0=max(dpi-10.dpi-12-pi)
dpi1=max(dpi-11,dpi-12)
dpi2=dpi-10+pi
此时初始化无需考虑太多,只要注意dp00既然是买入,所以要减了当日股票价格
填表顺序自然就是从左向右,三个表一起填
返回值返回最后一天冷冻期和可交易期二者的最大值
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];
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][2],dp[i-1][1]);
dp[i][2]=dp[i-1][0]+prices[i];
}
return max(dp[n-1][1],dp[n-1][2]);
}
};
3.买卖股票的最佳时机含手续费(714)


那这道题不就更简单了?都没有冷冻状态了,所以少一个dp表,两个状态之间的关系一画就出来了
我们直接编码
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n=prices.size();
vector<vector<int>> dp(n,vector<int>(2));
dp[0][0]-=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]-fee);
}
return max(dp[n-1][0],dp[n-1][1]);
}
};
4.买卖股票的最佳时机III(123)

就这还困难题,别逗泥梅笑了
首先这道题的大致方向,和前一道大差不差吧?
只是这里有一个新的要求,要求我只能最多完成两笔交易,也就是说,我要限制交易次数,怎么办?
很简单啊,那我记录下这是第几次交易不就是了?

那么怎么表示这是第几次交易呢?
我给dp表整成二维的不就好了?
dp1ij:在第i天,我处于买入状态,我是第j次交易了,此时我的最大利润
dp2ij: 在第i天,我处于卖出状态,此时我是第j次交易了,此时我的最大利润
至于初始化,我要求最大值,那么全部设成INT_MIN就是。
在这里我要再说一句,动态规划会把所有情况都考虑到的,所以我们要更加注重的是逻辑上,如果有想不通的地方,我们不妨考虑一下别的我们认为"没有用的状态"
class Solution {
public:
#define MIN 0x3f3f3f3f
int maxProfit(vector<int>& prices) {
int n=prices.size();
vector<vector<int>> dp1(n,vector<int>(3,-MIN));
//dp1是身上有股票没卖出去
vector<vector<int>> dp2(n,vector<int>(3,-MIN));
//dp2是身上没股票
dp1[0][0]=-prices[0];
dp2[0][0]=0;
for(int i=1;i<n;i++)
{
for(int j=0;j<3;j++)
{
dp1[i][j]=max(dp1[i-1][j],dp2[i-1][j]-prices[i]);
dp2[i][j]=dp2[i-1][j];
if(j-1>=0)
{
dp2[i][j]=max(dp2[i-1][j],dp1[i-1][j-1]+prices[i]);
}
}
}
return max(dp2[n-1][0],max(dp2[n-1][1],dp2[n-1][2]));
}
};
5.买卖股票的最佳时机IV(188)

虽然这是我们的最终boss了,但和我们上道题是不是差不多啊?我们把上道题稍微改一下,直接结束了
0class Solution {
public:
#define MIN 0x3f3f3f3f
int maxProfit(int k, vector<int>& prices) {
int n=prices.size();
vector<vector<int>> buy(n,vector<int>(k+1,-MIN));
vector<vector<int>> tradable(n,vector<int>(k+1,-MIN));
buy[0][0]=-prices[0];
//tradable[0][0]=0;
for(int i=1;i<n;i++)
{
for(int j=0;j<=k;j++)
{
buy[i][j]=max(buy[i-1][j],tradable[i-1][j]-prices[i]);
tradable[i][j]=tradable[i-1][j];
if(j>=1)
{
tradable[i][j]=max(buy[i-1][j-1]+prices[i],tradable[i-1][j]);
}
}
}
int ans=-MIN;
for(int j=0;j<=k;j++)
{
if(tradable[n-1][j]>ans)
{
ans=tradable[n-1][j];
}
}
return ans;
}
};
④.子数组,子串
1.最大子数组和(53)

这......难度是不是瞎标的,这还中等么
我直接从前往后遍历一遍加着,一直让和和当前数取最大值,谁大取谁不就行了
不讲不讲
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int ans=nums[0];
int cur_num=nums[0];
for(int i=1;i<nums.size();i++)
{
cur_num=max(nums[i],cur_num+nums[i]);
ans=max(ans,cur_num);
}
return ans;
}
};
2.环形子数组的最大和(918)

这题倒是有些说道,不过不也是和上道题差不多么?
无非就是有一个:可以取两边连续的,我们那不是可以看做中间挖去了一块么?
那么......我们如果一边要求一个最大的连续子数组,一边再求一个连续的最小子数组
注意:此时不考虑数组是环形的,我们就看做直的
,而我题目一旦给出,我的nums的和就确定了,那么我的和减去中间那块连续的最小子数组,不就是两边连续的最大子数组么?
那我们开始编码:
class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums) {
int n=nums.size();
vector<int> find_max(n+1);
vector<int> find_min(n+1);
int fmax=INT_MIN;
int fmin=INT_MAX;
for(int i=1;i<=n;i++)
{
find_max[i]=max(find_max[i-1]+nums[i-1],nums[i-1]);
find_min[i]=min(find_min[i-1]+nums[i-1],nums[i-1]);
fmax=max(fmax,find_max[i]);
fmin=min(fmin,find_min[i]);
}
int sum=0;
for(int i=0;i<n;i++)
{
sum+=nums[i];
}
return fmin==sum?fmax:max(fmax,sum-fmin);
}
};
3.乘积最大子数组(152)

这道题值得注意的地方是,负数,如果我的绝对值是最大的,但是我有个负号,那我岂不是变成最小的数了?而后面我一旦再乘一个负数,那我又变成最大的。正数同理:
所以在这道题中,我们正数和负数的最大值均要记录下来。
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n=nums.size();
vector<int> max_product(n+1);
vector<int> min_product(n+1);
max_product[0]=1;
min_product[0]=1;
int ans=INT_MIN;
for(int i=1;i<n+1;i++)
{
max_product[i]=max(nums[i-1],max(nums[i-1]*max_product[i-1],nums[i-1]*min_product[i-1]));
min_product[i]=min(nums[i-1],min(nums[i-1]*max_product[i-1],nums[i-1]*min_product[i-1]));
if(max_product[i]>ans)
{
ans=max_product[i];
}
}
return ans;
}
};
4.乘积位正数的最长子数组(1567)

这道题也简单吧?要求乘积为正数,那么不是还是要同时记录最长的正数和负数数组么?
只是这道题要多判断一下是否为0罢了
class Solution {
public:
int getMaxLen(vector<int>& nums) {
int n=nums.size();
vector<int> just(n+1);
vector<int> lose(n+1);
int ans=INT_MIN;
for(int i=1;i<=n;i++)
{
if(nums[i-1]>0)
{
just[i]=just[i-1]+1;
lose[i]=lose[i-1]==0?0:lose[i-1]+1;
}
else if(nums[i-1]<0)
{
just[i]=lose[i-1]==0?0:lose[i-1]+1;
lose[i]=just[i-1]+1;
}
else{
just[i]=0;
lose[i]=0;
}
if(just[i]>ans)
{
ans=just[i];
}
}
return ans;
}
};
5.等差数列划分(413)


那么也非常简单咯?
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n);
//以i位置为结尾,等差数列的个数
for(int i=2;i<n;i++)
{
dp[i]=nums[i]-nums[i-1]==nums[i-1]-nums[i-2]?dp[i-1]+1:0;
}
int ans=0;
for(int i=2;i<n;i++)
{
ans+=dp[i];
}
return ans;
}
};
6.最长湍流子数组(978)

这道题有意思啊,我们来好好说说
你看他叽里咕噜说了一大堆,什么意思啊?
不就是:

那不就是长这样么?
所以求这个怎么办?
只要找到状态转移,一切都迎刃而解,我们发现,不管这图如何,最终只有两种可能:
最后趋势向上,最后趋势向下,所以我们的两张dp表:
dp1i:当我以i结束的时候,我最后一个元素向上趋势,此时我数组最大长度
dp2i:当我以i结束的时候,我最后一个元素向下趋势,此时我数组最大长度
嘶,这不就是我们的多状态dp么?
状态转移方程:

虽说比较潦草,但还是可以看出来变化关系的
那么接下来就是编码环节:
class Solution {
public:
int maxTurbulenceSize(vector<int>& arr) {
int n=arr.size();
vector<int> rise(n,1);
vector<int> decline(n,1);
int rise_max=INT_MIN;
int decline_max=INT_MIN;
if(n==1)
{
return 1;
}
for(int i=1;i<n;i++)
{
rise[i]=arr[i]>arr[i-1]?decline[i-1]+1:1;
decline[i]=arr[i]<arr[i-1]?rise[i-1]+1:1;
if(rise_max<rise[i])
{
rise_max=rise[i];
}
if(decline[i]>decline_max)
{
decline_max=decline[i];
}
}
return decline_max>rise_max?decline_max:rise_max;
}
};
7.环绕字符串中唯一的子字符串(467)

这道题啊,依旧经验发力即可,依旧是以第i个位置为结尾,我有多少个非空子串在base中出现
但是但是.......
比如说我是个bab
我的b呢,它算作以b为结尾的子串,算一个
我bab,其中包含了b,那么我最后其实,要把以各种字符为结尾的字符串加起来的,就会重复很多次,那我怎么办?
哈希表!
我们直接建立一个有26个pair的哈希表即可,若有最后一位重复,那么大的覆盖小的即可;
class Solution {
public:
int findSubstringInWraproundString(string s) {
int n=s.size();
vector<int> dp(n,1);
for(int i=1;i<n;i++)
{
if(s[i]-s[i-1]==1 || (s[i]=='a' && s[i-1]=='z'))
dp[i]=dp[i-1]+1;
}
//但是,会有重复的,例如bab,这里直接取那个大的b即可覆盖小的b全部情况
map<char,int> statis;
for(int i=0;i<n;i++)
{
if(statis.count(s[i]))
{
if(statis[s[i]]<dp[i])
{
statis[s[i]]=dp[i];
}
}
else
{
statis.insert(make_pair(s[i],dp[i]));
}
}
int ans=0;
for(auto pair:statis)
{
ans+=pair.second;
}
return ans;
}
};
⑤.子序列
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序:
人话版:只要顺序对即可,无需保证连续
1.最长递增子序列(300)


并且呢,由于子序列的特性,它并不要求连续,所以在子序列中,对以前的dp表进行遍历的操作是非常常见的
至于判断条件推导状态转移方程的话,此题非常简单,相信大家可以直接得出
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
if(n==1)
{
return 1;
}
vector<int> dp(n,1);
int dp_max=INT_MIN;
for(int i=1;i<n;i++)
{
int j_max=INT_MIN;
for(int j=0;j<i;j++)
{
if(nums[i]>nums[j] && dp[j]>j_max)
{
j_max=dp[j];
}
}
dp[i]=j_max==INT_MIN?1:j_max+1;
if(dp[i]>dp_max)
{
dp_max=dp[i];
}
}
return dp_max;
}
};
2.摆动序列(376)

叽里咕噜说了一大堆,什么意思?

那不就是这个意思么
所以轻轻松松,状态转移方程就出来了,那么这道题不就是在多状态dp的基础之上,再引入了子序列(多遍历一遍)么?
所以我们轻松得出:
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n=nums.size();
vector<int> rise(n,1);
vector<int> decline(n,1);
int maxr=INT_MIN;
int maxd=INT_MIN;
if(n==1)
{
return 1;
}
for(int i=1;i<n;i++)
{
for(int j=0;j<i;j++)
{
if(nums[j]<nums[i])
{
//升序
rise[i]=max(rise[i],decline[j]+1);
}
if(nums[j]>nums[i])
{
decline[i]=max(decline[i],rise[j]+1);
}
if(maxr<rise[i])
maxr=rise[i];
if(maxd<decline[i])
maxd=decline[i];
}
}
return max(maxd,maxr);
}
};
3.最长递增子序列的个数(673)

至于这道题啊......他要我返回的是最长递增子序列的个数?
最简单的办法,自然是先找出最长的那个递增子序列,得到长度
那么问题此时转化为:
找出长度为N的递增子序列的个数
然后我继续遍历.......
但是这种做法,有点挫了,我想一步到位怎么办?
我可以在找的时候,就实时记录最长子序列的长度,此时就需要引入另一张dp表,用来标记以i为结尾,最长子序列的数目:
同时这是一道子序列的问题,子序列问题我要n*n遍历的,我在内部的循环中,引入判断递增子序列长度的判断,当满足最长子序列发生改变的时候,我直接将标记数目的dp表的counti赋值为countj即可,我们直接编码:
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int> len(n,1);
//以i为结尾,最长子序列的长度
vector<int> count(n,1);
//以i为结尾,最长子序列的数目
int anslen=1;
int anscount=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(len[i]==anslen)
anscount+=count[i];
if(len[i]>anslen)
{
anslen=len[i];
anscount=count[i];
}
}
return anscount;
}
};
4.最长定差子序列(1218)

这道题倒是有个地方值得注意:
我们发现这是个等差数列哎?等差数列的特点不就是,知道公差,还知道当前这个数,那我可以推得上一个数么?
那我如果以一个数它自身的数值为key,把以它为结尾的等差数列长度为value,那么不就可以通过一次遍历,从而得到我们的最长子序列么?
class Solution {
public:
int longestSubsequence(vector<int>& arr, int difference) {
int n=arr.size();
unordered_map<int,int> hash;
hash[arr[0]]=1;
int ans=1;
for(int i=1;i<n;i++)
{
if(hash.count(arr[i]-difference))
{
hash[arr[i]]=hash[arr[i]-difference]+1;
}
else
{
hash[arr[i]]=1;
}
if(hash[arr[i]]>ans)
{
ans=hash[arr[i]];
}
}
return ans;
}
};
5、最长的斐波那契子序列的长度(873)

此题注意一下,一维的dp表是解决不了这个问题的,因为我的dp表如果是一维的,那我存储的是以i位置这个元素为结尾的斐波那契子序列的最长长度,那我发现,一个数我也确定不了我的斐波那契序列啊?
所以我需要两个数,一个i位置,一个j位置,来表示我的斐波那契子序列:
class Solution {
public:
int lenLongestFibSubseq(vector<int>& arr) {
int n=arr.size();
unordered_map<int,int> hash;
for(int i=0;i<n;i++)
{
hash[arr[i]]=i;
//严格递增,不会重复
}
vector<vector<int>> dp(n,vector<int>(n,2));
//i>j>k
//为什么二维数组?因为一维数组无法确定斐波那契具体数字,而二维可以
int ret=2;
for(int i=2;i<n;i++)
{
for(int j=1;j<i;j++)
{
int pre=arr[i]-arr[j];
if(hash.count(pre) && pre<arr[j])
{
dp[j][i]=dp[hash[pre]][j]+1;
}
if(ret<dp[j][i])
{
ret=dp[j][i];
}
}
}
return ret==2?0:ret;
}
};
6,最长等差数列(1027)

这题和以前的那一道差不多吧?
嗯。。。不讲了不讲了,我直接给出代码吧?
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);
}
int ans=2;
vector<vector<int>> dp(n,vector<int>(n,2));
for(int i=2;i<n;i++)
{
for(int j=1;j<i;j++)
{
//i>j
int sub=nums[i]-nums[j];
int pre=nums[j]-sub;
if(hash.count(pre) )
{
auto it=hash[pre].rbegin();
while(it!=hash[pre].rend())
{
if(*it<j)
{
dp[j][i]=dp[*it][j]+1;
break;
}
it++;
}
}
if(ans<dp[j][i])
{
ans=dp[j][i];
}
}
}
return ans;
}
};
7.等差数列划分(446)

虽然标的困难题,其实很简单
和我们之前做过的差不多,还是注意一个一维的dp表是确定不了等差数列的,所以需要一个二维dp表,需要注意的也只有这个了
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
int n=nums.size();
vector<vector<int>> dp(n,vector<int>(n,0));
unordered_map<long long,vector<int>> hash;
for(int i=0;i<n;i++)
{
hash[nums[i]].push_back(i);
}
int sum=0;
for(int i=2;i<n;i++)
{
for(int j=1;j<i;j++)
{
//j<i,所以k<j
long long tmp=(long long)nums[j]*2-nums[i];
if(hash.count(tmp))
{
auto it=hash[tmp].rbegin();
while(it!=hash[tmp].rend())
{
if(*it<j)
{
dp[j][i]+=dp[*it][j]+1;
}
it++;
}
}
sum+=dp[j][i];
}
}
return sum;
}
};
当然,不想用哈希也是可以的,哈希属于优化之后为了优化时间的解法,直接使用普通的动态规划思路也可以?
⑥. 回文串问题
1.回文子串(647)

至于这道题的话,我们又要使用我们的经验了:
以第i个位置为结尾.......
但是不够啊,以一个位置为结尾,谁知道能有多少子串?
所以我们需要一个二维的dp表了,所以dpij表示字符串i,j区间内,是否是回文子串;
所以我们的dp表类型是vector<vector<bool>>
同时我们需要注意:

回文子串的判断,有这三种情况,我们需要细细讨论......
同时。填表顺序也有些讲究:

我需要的是左下方的数据,所以在我填此地的时候,我的左边好了,下面也好了,所以我的填表顺序是:
从下往上,从左向右
class Solution {
public:
int countSubstrings(string s) {
int n=s.size();
vector<vector<bool>> dp(n,vector<bool>(n,false));
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;
continue;
}
if(s[i]==s[j])
{
if(i==j)
{
dp[i][j]=true;
}
else if(i+1==j)
{
dp[i][j]=true;
}
else{
dp[i][j]=dp[i+1][j-1];
}
}
if(dp[i][j]==true)
ans++;
}
}
return ans;
}
};
2.最长回文子串(5)

这道题就和上道题是不是一模一样咯?
我们只需最后把最长的回文子串取出来即可
怎么取?
先确定一个头,一个长度,我们再通过长度的改变来判断头该变到什么地方
最后头确定了,长度也已知,只需用库函数substr即可
class Solution {
public:
string longestPalindrome(string s) {
int n=s.size();
vector<vector<bool>> dp(n,vector<bool>(n,false));
int len=0;
int 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]=false;
continue;
}
if(s[i]==s[j])
{
if(i==j)
{
dp[i][j]=true;
}
else if(i+1==j)
{
dp[i][j]=true;
}
else{
dp[i][j]=dp[i+1][j-1];
}
}
if(dp[i][j]==true && j-i+1>len)
{
len=j-i+1;
begin=i;
}
}
}
string ans=s.substr(begin,len);
return ans;
}
};
3.分割回文串IV(1745)

要我们把一个串分成三个回文子串啊。。。。
怎么分?
我们若有一个长为n的串,把分割点定为i,j
那么三个区域分别为0----i i+1------j j+1------n-1
那不是只需两个变量即可咯?从此,它与上题一样,只需找到两个点,,使这三个范围内皆为true即可
class Solution {
public:
bool checkPartitioning(string s) {
int n=s.size();
vector<vector<bool>> dp(n,vector<bool>(n,false));
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;
}
}
}
//分为三块0-i,i+1-j,j+1-n-1;
for(int i=0;i<n-2;i++)
{
for(int j=i+1;j<n-1;j++)
{
if(dp[0][i] && dp[i+1][j] && dp[j+1][n-1])
return true;
}
}
return false;
}
};
4.分割回文串II(132)


由此,我们编码得:
class Solution {
public:
#define MAX 0x3f3f3f3f
int minCut(string s) {
int n=s.size();
vector<vector<bool>> palin(n,vector<bool>(n,false));
for(int i=n-1;i>=0;i--)
{
for(int j=i;j<n;j++)
{
if(s[i]==s[j])
palin[i][j]=i+1<j?palin[i+1][j-1]:true;
}
}
vector<int> dp(n,MAX);
for(int i=0;i<n;i++)
{
if(palin[0][i])
{
dp[i]=0;
}
else
{
for(int j=1;j<=i;j++)
{
if(palin[j][i])
{
dp[i]=min(dp[i],dp[j-1]+1);
}
}
}
}
return dp[n-1];
}
};
5,最长回文子序列(516)

至于这一道么.....由于不是子数组那种连续的,所以我们在状态转移方程的时候要多考虑一下,其余的话,倒是全都一样了
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n=s.size();
vector<vector<int>> dp(n,vector<int>(n,0));
//i,j代表某一范围内回文子序列最大值,这里不是回文子串,所以不创建那种存储
//所有子串是否回文的二维数组
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];
}
};
6.让字符串成为回文串的最少插入次数(1312)


所以,万变不离其宗嘛,还是和前面几道题差不多的套路
class Solution {
public:
int minInsertions(string s) {
int n=s.size();
vector<vector<int>> dp(n,vector<int>(n,0));
for(int i=n-1;i>=0;i--)
{
dp[i][i]=0;
for(int j=i+1;j<n;j++)
{
if(s[i]==s[j])
dp[i][j]=dp[i+1][j-1];
else
{
dp[i][j]=min(dp[i][j-1],dp[i+1][j])+1;
}
}
}
return dp[0][n-1];
}
};
⑦.双数组dp
1.最长公共子序列(1143)

两个字符串哎,在此之前呢,我先来给大家说说一个新的初始化方式:
在dp中,字符串前面加一个空格,可以帮助我们更好的对齐。
在这道题中就会有所涉及,大家可以体会一下

class Solution {
public:
int longestCommonSubsequence(string s1, string s2) {
s1=" "+s1;
s2=" "+s2;
int m=s1.size();
int n=s2.size();
vector<vector<int>> dp(m,vector<int>(n));
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
if(s1[i]==s2[j])
{
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-1][n-1];
}
};
2.不相交的线(1035)

这什么乐乐题目:
不能相交,那不就是公共子序列么
我直接一边编码一边讲咯:
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
//状态表示:i,j范围内的最长公共子序列;
//状态转移方程式:nums1[i]==nums[j],那么是dp[i-][j-1]+1;
//初始化:要用到左,左上,上,没有就初始化为0;
int m=nums1.size();
int n=nums2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,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;
}
else
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[m][n];
}
};
3,不同的子序列(115)

class Solution {
public:
int numDistinct(string s, string t) {
int n=s.size();
int m=t.size();//目标子串,列,第一列为1
vector<vector<double>> dp(n+1,vector<double>(m+1,0));
for(int i=0;i<=n;i++)
{
dp[i][0]=1;
}
s=' '+s;
t=' '+t;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(s[i]==t[j])
{
dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
}
else
{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[n][m];
}
};
4.通配符匹配(44)

干讲太麻烦,我直接画图咯:

所以我们就可以编码了:
class Solution {
public:
bool isMatch(string s, string p) {
//首先,p是用来匹配s的
int m=s.size();
int n=p.size();
vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));
s=' '+s;
p=' '+p;
//m是每一行,代表s,n是每一列,代表p
dp[0][0]=true;
for(int i=1;i<=n;i++)
{
if(p[i]=='*')
{
dp[0][i]=true;
}
else
{
break;
}
}
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
//i代表s(被匹配的)
//三种情况,相等,?,*
if(p[j]=='*')
{
dp[i][j]=dp[i-1][j] || dp[i][j-1];
}
else{
dp[i][j]=(s[i]==p[j] || p[j]=='?') && dp[i-1][j-1];
}
}
}
return dp[m][n];
}
};
5.正则表达式匹配(10)

这一道题和前面一道也差不了太多,所以我们直接编码咯:
class Solution {
public:
bool isMatch(string s, string p) {
int m=s.size();
int n=p.size();
s=' '+s;
p=' '+p;
vector<vector<bool>> dp(m+1,vector<bool>(n+1));
//m行,n列,上面第一行表示s大小为0,左边第一列表示p大小为0
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];
}
};
6.交错字符串(97)

那么:
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
int m=s1.size();
int n=s2.size();
vector<vector<bool>> dp(m+1,vector<bool>(n+1));
if(m+n!=s3.size()) return false;
s1=' '+s1;
s2=' '+s2;
s3=' '+s3;
dp[0][0]=true;
for(int i=1;i<=m;i++)
{
if(s1[i]==s3[i])
dp[i][0]=true;
else
break;
}
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++)
{
for(int j=1;j<=n;j++)
{
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];
}
};
7.两个字符串的最小ASCII删除和(712)

都看到这里了,相信各位都能一眼丁真,直接看出来状态转移方程怎么写:
如果最后两位相同,不同......
class Solution {
public:
int minimumDeleteSum(string s1, string s2) {
//i,j表示0到i,0到j范围内,两个字序列最大的公共子序列ASCII码大小
int m=s1.size();
int n=s2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
int count=0;
for(auto e:s1)
{
count+=e;
}
for(auto e:s2)
{
count+=e;
}
s1=' '+s1;
s2=' '+s2;
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
if(s1[i]==s2[j])
{
dp[i][j]=dp[i-1][j-1]+s1[i];
}
else
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
}
return count-dp[m][n]*2;
}
};
8.最长重复子数组(718)

我们在讲最长公共子序列,大家是不是想如果是最长重复子数组会怎么样,这不就来了?
知道为什么把最长公共子序列放前面么,因为最长重复子数组根本没难度,随便做做就出来了
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
//i,j表示以i,j为结尾的的公共子数组
int m=nums1.size();
int n=nums2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1));
int ans=INT_MIN;
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]=0;
}
if(dp[i][j]>ans)
{
ans=dp[i][j];
}
}
}
return ans;
}
};
⑧. 01背包
1、分割等和子集(416)


所以:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n=nums.size();
int sum=0;
for(auto e:nums)
{
sum+=e;
}
if(sum%2!=0)
{
return false;
}
//i代表,0到i区间内,满足子序列的和等于j
vector<vector<bool>> dp(n+1,vector<bool>(sum/2+1));
for(int i=0;i<=n;i++)
{
dp[i][0]=true;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=sum/2;j++)
{
dp[i][j]=dp[i-1][j];
if(j-nums[i-1]>=0)
{
dp[i][j]=dp[i-1][j] || dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][sum/2];
}
};
2.目标和(494)

当年的步骤写的比较详细,那我就边写边给大家讲咯:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int n=nums.size();
int tar=target;
for(auto e:nums)
{
tar+=e;
}
if(tar%2 || tar<0)
{
return 0;
}
tar/=2;
//i,j,代表0到i范围内,满足和等于j的子序列的个数;
vector<int> dp(tar+1);
dp[0]=1;
for(int i=1;i<=n;i++)
{
for(int j=tar;j>=nums[i-1];j--)
{
//最后一个位置要不要
dp[j]=dp[j]+dp[j-nums[i-1]];
}
}
return dp[tar];
}
};
⑨.完全背包
1.零钱兑换(322)


轻轻松松嘛
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
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];
}
};
2.零钱兑换II(518)

注意,此时变成了,凑成目标金额的组合数,但是总体与上一道题还是类似的,只不过状态转移方程后面不+1了
class Solution {
public:
int change(int amount, vector<int>& coins) {
#define INF 0x3f3f3f3f
int n=coins.size();
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-1][j]+dp[i][j-coins[i-1]];
}
}
}
return dp[n][amount];
}
};
⑩.二维费用的背包问题:
1.盈利计划(879)


class Solution {
public:
int profitableSchemes(int n, int minprofit, vector<int>& group, vector<int>& profit) {
//i,j,k分别表示0到i范围内,不超过n个员工,所获得的利润不小于k,的计划
const int MOD=1e9+7;
int m=group.size();
vector<vector<vector<int>>> dp(m+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<=m;i++)
{
for(int j=0;j<=n;j++)
{
for(int k=0;k<=minprofit;k++)
{
dp[i][j][k]=dp[i-1][j][k];
if(group[i-1]<=j)
{
dp[i][j][k]=(dp[i-1][j][k]+dp[i-1][j-group[i-1]][max(0,k-profit[i-1])]);
dp[i][j][k]%=MOD;
}
}
}
}
return dp[m][n][minprofit];
}
};
尾:
终于结束了,这篇写了十个小时,第一次花了两天才写出一篇文章,应该一共有五十道题左右吧?
希望可以帮助大家理解动态规划这一个算法。
同时,也想提高一下大家的信心,很多看上去很麻烦的算法题呢,有时候我们不需要彻彻底底的从代码上了解它,这很难的,但是我们可以在逻辑上把它理解透,这样写的时候,答案会自己蹦出来
最后呢,我想说的是:
月缺不改光
剑折不改刚
月缺魄易满
剑折铸复良