参考内容
https://leetcode-cn.com/circle/article/NfHhXD/
https://leetcode-cn.com/circle/article/2Xxlw3/
非常感谢上述内容提供者无私的分享
目录
动态规划做题一般步骤
1、状态(dp)
状态的定义,先尝试「题目问什么,就把什么设置为状态」;
然后思考「状态如何转移」,如果「状态转移方程」不容易得到,尝试修改定义,目的依然是为了方便得到「状态转移方程」。
「状态转移方程」是原始问题的不同规模的子问题的联系。即大问题的最优解如何由小问题的最优解得到。
对状态的定义,在不同类型的题目中,各不相同,有些题目dp维数低,有些题目中使用高维数dp
动态规划是非常灵活的一种思维模式,很难像其他算法一样找到通用的模板,在同一类型题目中,dp的设置比较相似,如果再次遇到相似的问题,可以进行参考。
2、状态转移方程
状态转移方程是非常重要的,是动态规划的核心,也是难点;
常见的推导技巧是:分类讨论。即:对状态空间进行分类;
归纳「状态转移方程」是一个很灵活的事情,通常是具体问题具体分析;
除了掌握经典的动态规划问题以外,还需要多做题;
如果是针对面试,请自行把握难度。掌握常见问题的动态规划解法,理解动态规划解决问题,是从一个小规模问题出发,逐步得到大问题的解,并记录中间过程;
「动态规划」方法依然是「空间换时间」思想的体现,常见的解决问题的过程很像在「填表」。
此处liweiwei1419提及到的填表,是初学者理解动态规划运算过程非常好的一种方式,对于比较难的题目,不仅难在dp方程的定义,也难在初始化和动态规划整个过程多个循环嵌套时,遍历的方向,内外循环彼此位置的安排,有时将内循环变成外循环,很多问题就迎刃而解,有时将正序变成倒数,题目就会得到正确答案,
比如01背包和完全背包,01背包必须倒序进行,完全背包可以正序进行,
同样的,关于股票问题,在base case正确的情况下,将交易次数作为外循环,显然要简单很多,
这并不是一个玄学的过程,通过填表操作,就能很好的理解动态规划的运算过程,也能写出更高质量的代码。
3、初始化
初始化是非常重要的,一步错,步步错。初始化状态一定要设置对,才可能得到正确的结果。
角度 1:直接从状态的语义出发;
角度 2:如果状态的语义不好思考,就考虑「状态转移方程」的边界需要什么样初始化的条件;
角度 3:从「状态转移方程」方程的下标看是否需要多设置一行、一列表示「哨兵」(sentinel),这样可以避免一些特殊情况的讨论。
填表也是解决初始化问题的方法之一。
4、输出
有些时候是最后一个状态,有些时候可能会综合之前所有计算过的状态。
动态规划的题目之所以难,是因为题目本身非常考验做题者的抽象思维能力,如何把抽象问题具象化。
以上内容参考liweiwei1419所述,进行了部分的修改和简化
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-
以下内容分类参考leetcode上DP题目分类总结:
https://leetcode-cn.com/circle/article/NfHhXD/
https://leetcode-cn.com/circle/article/2Xxlw3/
1. 数塔
从斐波那契数列 到杨辉三角,其中的dp非常好设置,这些数列本身就很有规律性,按着规律就行进行操作,属于动态规划中比较简单和直观的题目
同样,我们会发现,题目要求我们让结果对1e9+7取模,原因大致如下:(来源:知乎@EntropyIncreaser)
面试题10- I. 斐波那契数列 https://leetcode-cn.com/problems/fei-bo-na-qi-shu-lie-lcof/
斐波那契数列的状态方程,初值就是其本身的计算公式,直接进行设置即可
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
class Solution {
public:
int fib(int n) {
if(n == 0) return 0;
if(n == 1) return 1;
int mod = 1e9+7;
int F1 = 0,F2 = 1,Fres = 0;
for(int i = 2;i<=n;++i)
{
Fres = (F1 + F2)%mod;
F1 = F2;
F2 = Fres;
}
return Fres;
}
};
类似问题:
1137. 第 N 个泰波那契数 https://leetcode-cn.com/problems/fibonacci-number/
118. 杨辉三角 https://leetcode-cn.com/problems/pascals-triangle/
119. 杨辉三角 II https://leetcode-cn.com/problems/pascals-triangle-ii/
杨辉三角和斐波那契数列如出一辙,初值也好,状态方程也好,都是题目给定好的,直接计算即可:
class Solution {
public:
vector<int> getRow(int rowIndex) {
vector<int> dp(rowIndex+1,1);//行数和列数关系
for(int i = 2;i<=rowIndex;++i)//行数遍历
{
for(int j = 2;j<=i;++j)
{
dp[j-1] = dp[j-1] + dp[j-2];
}
}
return dp;
}
};
(错误版本)
class Solution {
public:
vector<int> getRow(int rowIndex) {
vector<int> dp(rowIndex+1,1);//行数和列数关系
for(int i = 2;i<=rowIndex;++i)//行数遍历
{
for(int j = i;j>1;--j)
{
dp[j-1] = dp[j-1] + dp[j-2];
}
}
return dp;
}
};
杨辉三角有个很有意思的地方,就是我们需要倒序(可进行内外循环关系)
二维动态规划的状态方程很好给出
二维的动态规划随意循环,都是没有问题的,但是如果压缩到一维,内循环必须是倒序,从上面的程序输出结果我们也可以看出,从前往后正序计算,i= 3时,计算dp[2]的时候,dp[2] = dp[2](此时为1) + dp[1](此时为3,不是2,因为正序计算的时候将dp[1]更新了);
这就是在不同状态方程下,我们需要注意的地方,有时候要采用不一样的循环策略
279. 完全平方数 https://leetcode-cn.com/problems/perfect-squares/
此题不如说是一道数学题,题目怎么要求咱们怎么设,状态方程如下:
下面我们需要给平方数一个取值上限,当然直接给n也可以,但是我们知道,一个数值一般都不会大于自身一般的平方
当a大于4的时候确实是如此,那么我们就让a/2为平方数的上限,下面的解法是直接开方,当然效果都是一样的,直接开方的循环次数会更小。
class Solution {
public:
int numSquares(int n) {
if(n == 0) return 0;
vector<int> dp(n+1,n);
dp[0] = 0;//dp[i]表示组成i,需要最少个数的完全平方数
for(int i = 1;i<=n;++i)
{
for(int j = 1;j*j<=i&&j<=sqrt(n);++j)
{
dp[i] = min(dp[i],dp[i-j*j]+1);
}
}
return dp[n];
}
};
https://leetcode-cn.com/problems/jian-sheng-zi-lcof/
动态规划算法:
class Solution {
public:
int cuttingRope(int n) {
if(n<=0) return 0;
vector<int> dp(n+1,0);//长度学n的绳子,剪m段后,最大乘积
//本题比较特殊,因为必须要切一刀
if(n == 1) return 1;
if(n == 2) return 1;
dp[1] = 1;
dp[2] = 1;
for(int i = 3;i<=n;++i)
{
for(int j = 1;j<i;++j)
{
dp[i] = max(dp[i],max(j*(i-j),j*dp[i-j]));
}
}
return dp[n];
}
};
常规的动态规划思维,设dp,找父问题和子问题之间的联系
我们假设dp为:i米长的绳子,切任意刀,最长乘积为dp[i];
长度为i,怎么切乘积最大呢?那么我们试试就知道了,在1的位置切一刀,再乘上dp[i-1],即为j*dp[i-1]
但是我们还要考虑一种情况,就是直切一刀,即j*(i-1)
dp状态方程如下:
https://leetcode-cn.com/problems/ugly-number-ii/
第n个丑数,比如第10个丑数12,是怎么算的呢?
1的丑数有几个?2,3,5
2的丑数呢?4,6,10
3的丑数:6,9,15
4的丑数:8,12,20
class Solution {
public:
int nthUglyNumber(int n) {
vector<int>dp(n,0);
dp[0] = 1;
int a = 0,b = 0,c = 0;//三个指针,因为是三个因子,分别记录2,3,5对应的数值内容
for(int i = 1;i<n;++i)
{
int temp2 = dp[a]*2,temp3 = dp[b]*3,temp5 = dp[c]*5;
dp[i] = min(temp2,min(temp3,temp5));
if(dp[i] == temp2) a++;
if(dp[i] == temp3) b++;
if(dp[i] == temp5) c++;
}
return dp[n-1];
}
};
https://leetcode-cn.com/problems/maximum-subarray/
本题很简单,我们利用动态规划,假设以i结尾的连续子数组,和最大
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if(nums.empty()) return 0;
int size = nums.size();
int MAX = INT_MIN;
vector<int>dp(size+1,INT_MIN);//以i结尾,子序列的最大和
dp[0] = 0;
for(int i = 1;i<=size;++i)
{
dp[i] = max(dp[i-1] + nums[i-1],nums[i-1]);
MAX = max(MAX,dp[i]);
}
return MAX;
}
};
对本题进行升级:
https://leetcode-cn.com/problems/maximum-product-subarray/
我们关心思维,继续设以i结尾,最长连续子数组最乘积,但是
class Solution {
public:
int maxProduct(vector<int>& nums) {
if(nums.empty()) return 0;
int size = nums.size();
vector<int>dp(size+1,0);//以i结尾的,最长连续子数组最大乘积
//base case
dp[0] = 0;
dp[1] = nums[0];
int MAX = dp[1];
for(int i = 2;i<=size;++i)
{
dp[i] = max(nums[i-1],nums[i-1]*dp[i-1]);
//选择自身,还是选择乘上前者
MAX = max(MAX,dp[i]);
}
return MAX;
}
};
我们需要改变策略:
本题是典型的要求解决无后效性的动态规划,我们对dp进行重新的设置:我们必须让正负号和结果产生联系
那么我们分情况讨论问题
- nums【i】>0:最大值依旧最大值,最小值依旧最小值
- nums【i】<0:那么最大值将会在乘完后变成最小值,最小值会变成最大值
- nums【i】=0:二者直接归零
同样,整个过程需要考虑,nums【i】本身会不会代替最大值和最小值
状态方程如下:
完整版本程序如下:
class Solution {
public:
int maxProduct(vector<int>& nums) {
if(nums.empty()) return 0;
int size = nums.size();
vector<vector<int>>dp(size+1,vector<int>(2,0));//以i结尾的,最长连续子数组最大乘积
//base case
dp[1][0] = nums[0];
dp[1][1] = nums[0];
int MAX = dp[1][1];
for(int i = 2;i<=size;++i)
{
if(nums[i-1]>0)//最大值还是最大值
{
dp[i][1] = max(nums[i-1],nums[i-1]*dp[i-1][1]);//最大值
dp[i][0] = min(nums[i-1],nums[i-1]*dp[i-1][0]);//最大值
}
else //最大值和最小值进行转换
{
dp[i][1] = max(nums[i-1],nums[i-1]*dp[i-1][0]);//最大值
dp[i][0] = min(nums[i-1],nums[i-1]*dp[i-1][1]);//最大值
}
//选择自身,还是选择乘上前者
MAX = max(MAX,dp[i][1]);
}
return MAX;
}
};
非常好的一道题
2. 背包
Leetcode动态规划——01背包问题:https://blog.csdn.net/qq_41605114/article/details/106059876
Leetcode动态规划—完全背包问题:https://blog.csdn.net/qq_41605114/article/details/106086262
3.路径
往后的几道路径问题,题目要求都很相似,都是在棋盘格上,要求从某点出发,最终到达某点,求路径总数,最短路径等等
路径问题的状态设置都非常类似,解题思路也大同小异,下面我们来总结一下:
(1)状态定义:一般都是二维dp,一般定义都是dp[i][j]:从起点到坐标为i,j的点,路径总数/最小路径和/出界概率/.....为dp[i][j]
(2)状态方程:从哪儿来?触发什么内容?
从哪儿来?有些题目(62/63)是从目标点[i][j]点的上面和左侧,有些题目则复杂一些(688/935),是以目标点[i][j]点为中心的八个
位置出发,都可以到目标点[i][j],但是这些都不重要,状态方程就和这些能够到达目标点[i][j]的位置有关,有时候是全部加和,有时
候是比大小,这就要看题意了。
(3)初始值
设置按照逻辑和dp table来就好,路径问题的初值都很直观,很好设置主要分为以下部分:
边界处:需要根据题意进行设置,一般都非常明显
边界外:一般都是0,或者无效状态
边界内部:根据题意,有时需要将全部设置为1或者其他内容,路径问题的初值终点还是在边界外和边界处
(4)走N步/移动N次
参考688,935,576等题目,旗子的步长一定要作为外循环,为了保证dp table的前一个状态处于完整状态,不至于漏算少算一些情况!
62. 不同路径 https://leetcode-cn.com/problems/unique-paths/
求路径总数
class Solution {
public:
int uniquePaths(int m, int n) {
if(m < 0||n < 0) return 0;
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
//设置dp[i][j]为,从起点出发,到该点的路径数量
for(int i = 0;i<=m;++i) dp[i][1] = 1;
for(int j = 0;j<=n;++j) dp[1][j] = 1;
for(int i = 2;i<=m;++i)
{
for(int j = 2;j<=n;++j)
{
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m][n];
}
};
从哪儿来?
本题要求,只能从往下和往右,那么任意一个具有普世意义的点,要得到该点的路径和,那么只需要知道到达该点上面一个点和左侧一个点的路径和即可。
正如状态方程所示。
63. 不同路径 II https://leetcode-cn.com/problems/unique-paths-ii/
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
if(obstacleGrid.size() == 0) return 0;
int row = obstacleGrid.size();
int col = obstacleGrid[0].size();
vector<vector<int>>dp(row+1,vector<int>(col+1,0));
for(int i = 1;i<=row;++i){
for(int j = 1;j<=col;++j){
if(i == 1&&j == 1) {
if(obstacleGrid[i-1][j-1] == 0) dp[i][j] = 1;
else return 0;//开始就是障碍,一步都走不了
}
else {
if(obstacleGrid[i-1][j-1] == 0) {
if(i == 1) dp[i][j] = dp[i][j-1];//只能从左侧来
else if(j == 1) dp[i][j] = dp[i-1][j];//只能从右边来
else dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
else dp[i][j] = 0;
}
}
}
return dp[row][col];
}
};
解法二:
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
if(obstacleGrid.empty()) return 0;
int m = obstacleGrid.size(),n = obstacleGrid[0].size();
if(obstacleGrid[0][0] == 1||obstacleGrid[m-1][n-1]) return 0;
vector<vector<int>>dp(m+1,vector<int>(n+1,0));
//base case
for(int i = 1;i<=m;++i){
if(obstacleGrid[i-1][0] == 1) break;//当前列,一旦遇到阻碍,后面全是0
dp[i][1] = 1;
}
for(int j = 1;j<=n;++j){
if(obstacleGrid[0][j-1] == 1) break;//当前行,一旦遇到阻碍,后面全是0
dp[1][j] = 1;
}
for(int i = 2;i<=m;++i){
for(int j = 2;j<=n;++j){
if(obstacleGrid[i-1][j-1] == 1) dp[i][j] = 0;
else if(obstacleGrid[i-2][j-1] == 1)//如果该点上面是障碍物
dp[i][j] = dp[i][j-1];
else if(obstacleGrid[i-1][j-2] == 1)//左侧
dp[i][j] = dp[i-1][j];
else
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m][n];
}
};
官方解答:https://leetcode-cn.com/problems/unique-paths-ii/solution/bu-tong-lu-jing-ii-by-leetcode/
此题加入了障碍物,那么整个dp的定义思路和状态方程的定义思路还是不变,只是在细节部分作出修改
64. 最小路径和 https://leetcode-cn.com/problems/minimum-path-sum/
题目特别指出,从左上角到右下角,告诉了起点和终点,此题和62题没有什么不一样。
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size(),n = grid[0].size();
if(m == 0||n == 0) return 0;
//本题规定了起点和终点,起点是左上角,终点是右下角
vector<vector<int>>dp(m+1,vector<int>(n+1,0));
//dp表示以[1][1]为起点,以ij为结尾,最小路径和
// dp[1][1] = grid[0][0];
for(int i = 1;i<=m;++i)
{
for(int j = 1;j<=n;++j)
{
if(i == 1)//第一行
{dp[i][j] = dp[i][j-1]+grid[i-1][j-1];continue;}
if(j == 1)//第一列
{dp[i][j] = dp[i-1][j]+grid[i-1][j-1];continue;}
dp[i][j] = min(dp[i][j-1],dp[i-1][j])+grid[i-1][j-1];
}
}
return dp[m][n];
}
};
解法二:
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
if(grid.empty()) return 0;
int row = grid.size(),col = grid[0].size();
vector<vector<int>>dp(row+1,vector<int>(col+1,1000));//第一列只能从上面来,所以外界必须是大值
dp[1][1] = grid[0][0];
for(int j = 2;j<=col;++j)//第一行只能从左侧过来
{
dp[1][j] = grid[0][j-1] + dp[1][j-1];
}
for(int i = 2;i<=row;++i)
{
for(int j = 1;j<=col;++j)
{
// if(j>1)
dp[i][j] = min(dp[i-1][j]+grid[i-1][j-1],dp[i][j-1]+grid[i-1][j-1]);
// cout<<i<<" "<<j<<" "<<dp[i][j]<<endl;
}
}
return dp[row][col];
}
};
参考代码:
https://leetcode-cn.com/problems/minimum-path-sum/solution/zui-xiao-lu-jing-he-by-leetcode/
如果我们对上面的题目增加难度,不去规定起点和终点,那么我们应该怎么做?
931. 下降路径最小和 https://leetcode-cn.com/problems/minimum-falling-path-sum/
此题一看,和64题非常相似,但是还是有本质上的差别的,64题规定了起点和终点,931并没有在严格意义上要求起点和终点。
本题有一个问题,路径很多,dp最好的定义方式如下:
从哪儿来?
要到达指定点,出去边界,从三个方向,j,j+1,j-1。加上题目要求最小值,那么直接对比三者之中谁最小即可。
最后再次对比到达最后一行的最小路径中谁最小,完成输出
这种情况,在后面买票问题中也会出现,正序倒序都能解决问题。
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& A) {
int depth = A.size(),size = A[0].size();
if(depth == 0||size == 0) return 0;
vector<vector<int>> dp(depth+1,vector<int>(size+1,0));
dp[1][1] = A[0][0];
for(int i = 1;i<=depth;++i)
{
for(int j = 1;j<=size;++j)
{
if(j == 1&&j == size)
{dp[i][j] = dp[i-1][j]+A[i-1][j-1];continue;}
if(j == 1)
{dp[i][j] = min(dp[i-1][j],dp[i-1][j+1])+A[i-1][j-1];continue;}
if(j == size)
{dp[i][j] = min(dp[i-1][j-1],dp[i-1][j])+A[i-1][j-1];;continue;}
dp[i][j] = min(dp[i-1][j],min(dp[i-1][j-1],dp[i-1][j+1]))+A[i-1][j-1];
}
}
int MIN = INT_MAX;
for(int j = 1;j<=size;++j)
{
MIN = min(MIN,dp[depth][j]);
}
return MIN;
}
};
120. 三角形最小路径和 https://leetcode-cn.com/problems/triangle/
如果对931有了深刻的认识,那么120这道题也就没有什么难度了。
状态及状态方程的定义,都是如出一辙,只是边界条件稍有区别而已。
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
if(triangle.empty()) return -1;
int endsize = triangle[triangle.size()-1].size();
vector<int>dp(endsize+1,10000);
int MAX = INT_MAX;
dp[0] = 0;dp[1] = triangle[0][0];
for(int i = 2;i<=triangle.size();++i){//外循环从前到后
for(int j = triangle[i-1].size();j>=1;--j){//内循环注意,因为是一维的,所以要从后往前算
if(j == 1) dp[j] = dp[j] + triangle[i-1][j-1];
else dp[j] = min(dp[j],dp[j-1]) + triangle[i-1][j-1];
if(i == triangle.size()) MAX = min(MAX,dp[j]);
}
}
return MAX == INT_MAX?dp[1]:MAX;
}
};
主要参考:
本题使用一维dp务必注意循环的起点和终点
935. 骑士拨号器 https://leetcode-cn.com/problems/knight-dialer/
此题可以说是检验上面几道题目的综合版本,虽然不再是简单的从上或者从右到达指定点,但是本质上是一样的。
本题增加了步数限制,所以需要对dp进行进一步约束,增加步数限制
和其他路径题目一样,只不过增加了步数限制
dp定义如下:
vector<vector<vector<unsigned int>>> dp(4+4,vector<vector<unsigned int>>(3+4,vector<unsigned int>(N+1,0)));
//dp表示ij为起点,摁下了N个数字,方案有多少种,根据题意
至于范围为什么如此设置也是有原因的,和状态方程的设置很有关系。
将拨号键范围内的内容,初始化为1,也就是图中蓝色底纹部分。
如果只摁下一个摁键,那么,那么到达每个数字对应的位置的方案就各为1,最终将所有拨号键范围内数字步数加和即可。
最外侧循环是摁下的数字个数,摁下一个摁键的情况已经包含在初始化中,所有从摁下两次开始。
下面分析一些dp状态方程的推导过程,
如果想到到达数字1,可以从图中绿色标记的各个点出发,那么状态方程就很好理解,要第N次到达(摁下)的数字是a,那么第N-1次到达(摁下)的数字只肯是是图中绿色标记的位置,方案总数或者路径总数就是这些可能性的总和。
当然也可以换个思路,从i,j出发,走k步,摁下的号码个数,也是一种理解状态方程的办法
那么我们第k步走出去,能够到达的位置有八个,再从这些位置作为新的起点,走第k-1步,那么就变成了如下的状态:
dp[i][j][k] = (dp[i-1][j-2][k-1]+dp[i-1][j+2][k-1]+dp[i-2][j-1][k-1]+dp[i-2][j+1][k-1]+dp[i+1][j-2][k-1]+dp[i+1][j+2][k-1]+dp[i+2][j+1][k-1]+dp[i+2][j-1][k-1])%mod;
完整代码如下:
class Solution {
public:
int knightDialer(int N) {
if(N == 0) return 1;
if(N == 1) return 10;
int mod = 1e9+7;
vector<vector<vector<unsigned int>>>dp(3+4+1,vector<vector<unsigned int>>(4+4+1,vector<unsigned int>(N,0)));
//di,j,走k步,摁下拨号键的组合
//base case
for(int i = 2;i<=4;++i)
for(int j = 2;j<=4;++j) dp[i][j][0] = 1;
dp[5][3][0]=1;
//
int SUM = 0;
for(int k = 1;k<=N-1;++k)//走的步数
{
for(int i = 2;i<=5;++i)
{
for(int j = 2;j<=4;++j)
{
if(i == 5&&(j == 2||j == 4)) continue;
dp[i][j][k] = (dp[i-1][j-2][k-1]+dp[i-1][j+2][k-1]+dp[i-2][j-1][k-1]+dp[i-2][j+1][k-1]+dp[i+1][j-2][k-1]+dp[i+1][j+2][k-1]+dp[i+2][j+1][k-1]+dp[i+2][j-1][k-1])%mod;
if(k == N-1) SUM=( SUM + dp[i][j][N-1] )%mod;
}
}
}
return SUM;
}
};
下面是同类型的一道题目
688. “马”在棋盘上的概率 https://leetcode-cn.com/problems/knight-probability-in-chessboard/
国际象棋类型的题目,dp[i][j][k]可以理解为从i,j出发,走k步还在棋盘格上的概率。
也可以理解成走了k步,到达i,j的概率。
class Solution {
public:
double knightProbability(int N, int K, int r, int c) {
int Size = N+4;
vector<vector<vector<double>>>dp(Size,vector<vector<double>>(Size,vector<double>(K+1,0)));
//从i,j出发,走k步,还在棋盘上的概率
//base case
for(int i = 2;i<=Size-3;++i)
for(int j = 2;j<=Size-3;++j) dp[i][j][0] = 1;//一步不走,就在棋盘上
for(int n = 1;n <= K;n++)
{
for(int i = 2;i<=Size-3;++i)
{
for(int j = 2;j<=Size-3;++j)
{
dp[i][j][n] = (dp[i-1][j-2][n-1]+dp[i-1][j+2][n-1]+dp[i-2][j-1][n-1]+dp[i-2][j+1][n-1]+dp[i+1][j-2][n-1]+dp[i+1][j+2][n-1]+dp[i+2][j+1][n-1]+dp[i+2][j-1][n-1])/8.0;
}
}
}
return dp[r+2][c+2][K];//务必注意输出,你增加了两行两列,自然此处也要增加
}
};
以上两道题目特别注意,旗子的步长一定要作为外循环,为了保证dp table的前一个状态处于完整状态,不至于漏算少算一些情况!
主要参考
576. 出界的路径数 https://leetcode-cn.com/problems/out-of-boundary-paths/
此题也是一样的,能计算留着界内路径数,也就能计算走出界限路径数,从哪儿来,上下左右都能来,那么就定义dp为该点上下左右出界路径的总和,base case就是将外界一圈,步数为0的区域,不用走就在界外的这些情况设置为零。
class Solution {
public:
int findPaths(int m, int n, int N, int begin, int end) {
int mod = 1e9+7;
vector<vector<vector<unsigned long long>>>dp(m+2,vector<vector<unsigned long long>>(n+2,vector<unsigned long long>(N+1,0)));
//初始化base case将范围内的,也就是mxn四周的值全部初始化为1
//dp[i][j][k]从ij出发,走k步走到外界
for(int j = 0;j<n+2;++j)
{
dp[0][j][0] = 1;//第一行
dp[m+1][j][0] = 1;//最后一行
}
for(int i = 0;i<m+2;++i)
{
dp[i][0][0] = 1;//第一列
dp[i][n+1][0] = 1;//最后一列
}
for(int k = 1;k<=N;++k)//步长需要放在外侧循环,因为要考虑到所有组合可能性,也是为了让表的其他部分填写完整
{
for(int i = 1;i<=m;++i)
{
for(int j = 1;j<=n;++j)
{
dp[i][j][k] = (dp[i-1][j][k-1]+dp[i+1][j][k-1]+dp[i][j-1][k-1]+dp[i][j+1][k-1])%mod;
}
}
}
int sum = 0;
for(int k = 1;k<=N;++k)
sum = (sum+dp[begin+1][end+1][k])%mod;
return sum;
}
};
上面的题目都是一本道,一路走到黑,我们来看看迂回型路径题目,走到终点之后,还要走回去
https://leetcode-cn.com/problems/cherry-pickup/
此类题目,我们该怎么设置dp呢?走一条路我们清楚怎么解决,这么来回折腾,还要去往返,怎么下手?
class Solution {
public:
int cherryPickup(vector<vector<int>>& grid) {
int N = grid.size();
vector<vector<int>>dp(N+1,vector<int>(N+1,INT_MIN));
//从i,j出发到N-1
//base case
dp[N-1][N-1] = grid[N-1][N-1];//零位置
for(int sum = 2*N - 3; sum >= 0; --sum)
for(int i1 = max(0, sum - N + 1); i1 <= min(N-1,sum); ++i1)
for(int i2 = i1; i2 <= min(N-1,sum); ++i2)
{
int j1 = sum - i1, j2 = sum - i2;
if(grid[i1][j1] == -1 || grid[i2][j2] == -1)
dp[i1][i2] = INT_MIN;
else
dp[i1][i2] = grid[i1][j1] + (i1 != i2 || j1 != j2)*grid[i2][j2] + max(
max(dp[i1][i2+1], dp[i1+1][i2]),
max(dp[i1+1][i2+1], dp[i1][i2])
);
}
return max(0, dp[0][0]);
// 作者:newhar
// 链接:https://leetcode-cn.com/problems/cherry-pickup/solution/dong-tai-gui-hua-xiang-xi-jie-xi-tu-jie-by-newhar/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
https://leetcode-cn.com/problems/cherry-pickup-ii/
那么状态方程如下:
要达到的点的值,加上能达到此点的所有路径的最值
我们利用下面的代码实现:
int Val = grid[i-1][j1-1]+grid[i-1][j2-1];
for(int r1 = 0;r1<3;++r1)
{
for(int r2 = 0;r2<3;++r2)
{
int lastR1 = j1 + D[r1],lastR2 = j2 + D[r2];
if(lastR1<=0||lastR1>col||lastR2<=0||lastR2>col) continue;//越界
dp[i][j1][j2] = max(dp[i][j1][j2],Val+dp[i-1][lastR1][lastR2]);
}
}
之后就是base case,出发点就是base case
dp[1][1][col] = grid[0][0]+grid[0][col-1];//出发点(0,0)(0,col-1)
本题还有一个最大的雷区,就是有些点,是不能到达的
我们的行,其实也是每个机器人走的步数,就是一直斜着都,如图中的蓝色路径,也到不了橘黄色的禁区,我们不能算禁区的值
需要跳过,那么对j1来说,不能大于i,那么对于j2来说,必须完全大于col-i,这点非常重要,否则在一些示例中会出错
那么下面就是完整版本的代码,我们按照行进行遍历,然后防止两个机器人相交,不断遍历列的内容,计算最值。
class Solution {
public:
int cherryPickup(vector<vector<int>>& grid) {
if(grid.empty()) return 0;
int row = grid.size(),col = grid[0].size();
int D[3] = {-1,0,1};
vector<vector<vector<int>>> dp(row+1,vector<vector<int>>(col+1,vector<int>(col+1,0)));
//两个机器人在第i行,第j1和j2列的时候,取的樱桃数目最多
//base case
dp[1][1][col] = grid[0][0]+grid[0][col-1];//出发点(0,0)(0,col-1)
for(int i = 2;i<=row;++i)//从第二行开始,因为从第一行为起点,只能走到第二行
for(int j1 = 1;j1<=col;++j1)//从第1列到第col-1列
{
if (j1 > i) continue;//剔除不可能的位置!非常重要
for(int j2 = j1+1;j2<=col;++j2)//从第2列到第col列,避开二者相遇的情况
{
//9种组合开始
if (j2 <= col - i) continue;//剔除不可能的位置!非常重要
int Val = grid[i-1][j1-1]+grid[i-1][j2-1];
for(int r1 = 0;r1<3;++r1)
{
for(int r2 = 0;r2<3;++r2)
{
int lastR1 = j1 + D[r1],lastR2 = j2 + D[r2];
if(lastR1<=0||lastR1>col||lastR2<=0||lastR2>col) continue;//越界
dp[i][j1][j2] = max(dp[i][j1][j2],Val+dp[i-1][lastR1][lastR2]);
}
}
}
}
//最后一行求最值
int Res = 0;
for(int j1 = 1;j1<col;++j1)
{
for(int j2 = j1+1;j2<=col;++j2)
{Res = max(Res,dp[row][j1][j2]);
// cout<<dp[2][j1][j2]<<endl;
}
}
return Res;
}
};
算法参考:https://leetcode-cn.com/problems/cherry-pickup-ii/solution/dong-tai-gui-hua-you-hua-by-over-lord/
1220. 统计元音字母序列的数目 https://leetcode-cn.com/problems/count-vowels-permutation/
class Solution {
public:
int countVowelPermutation(int n) {
int mod = 1e9+7;
vector<vector<unsigned long long int>>dp(n+1,vector<unsigned long long int>(6,0));
//dp含义:字符串长度为n,以i结尾的字符串,有几种组合方式
for(int i= 1;i<=5;++i)
dp[1][i] = 1;
for(int size = 2;size<=n;++size)
{
for(int i = 1;i<=5;++i)
{
if(i == 1)//a
dp[size][i] = (dp[size-1][2] + dp[size-1][5] + dp[size-1][3])%mod ;//前面可以是2e和5u和3i
else if(i == 2)//e
dp[size][i] = (dp[size-1][1] + dp[size-1][3])%mod;//前面可以是1a和3i
else if(i == 3)//i
dp[size][i] = (dp[size-1][2] + dp[size-1][4])%mod;//前面可以是2e和4o
else if(i == 5)//u
dp[size][i] = (dp[size-1][4] + dp[size-1][3])%mod;//前面可以是4o
else //0
dp[size][i] = dp[size-1][3];//前面可以是3i
}
}
int sum = 0;
for(int i = 1;i<=5;++i)
{
sum = (sum + dp[n][i])%mod;
}
return sum;
}
};
https://leetcode-cn.com/problems/li-wu-de-zui-da-jie-zhi-lcof/
class Solution {
public:
int maxValue(vector<vector<int>>& grid) {
int m = grid.size(),n = grid[0].size();
vector<vector<int>>dp(m+2,vector<int>(n+2,0));
for(int i = 1;i<=m;++i)
{
for(int j = 1;j<=n;++j)
{
if(i == 1&&j == 1) dp[i][j] = grid[0][0];
dp[i][j] = max(dp[i-1][j],dp[i][j-1])+grid[i-1][j-1];
}
}
return dp[m][n];
}
};
4骰子
1223. 掷骰子模拟 https://leetcode-cn.com/problems/dice-roll-simulation/
我们先将dp设为如下情况,并推导出状态方程:
代码也很好写,完整代码如下:
for(int i = 2;i<=n;++i)//骰子次数
{
for(int j = 1;j<=6;++j)//本次投掷出的点数
{
for(int k = 1;k<=6;++k)//上次投掷出的点数
{
dp[i][j][x] += dp[i-1][k][x];//本次投掷以j结尾,那么就要加上一次是1~6的全部可能
}
}
}
现在有约束,某个数字连续出现的次数有了限制,我们和股票问题一样,在状态方程后面增加一个状态量:
下面这个部分不好理解:
假设现在1不能连续出现2次,那算第三次的时候,其实只是排除了三个1同时出现的情况,除去次情况
还有连续一次以1结尾的情况,我们必须要考虑,图中黄色线条的组合我们不考虑,但是蓝色我们必须考虑
所以就有了下面的情况
我们现在要判断,本次点数和上次投掷点数是否相同,如果相同,本次就是第t+1次,t从1开始,小于最大出现次数
本次点数j连续出现次数从2一直到最大出现次数为止,所有情况都要考虑一次,程序如下
if(j == k)//本次点数和上次一样
{
for(int t = 1;t<rollMax[k-1];++t)//此时,是k是j无所谓
//循环出现次数,因为大前提是连续出现,所以不能等于rollMax[j]
dp[i][j][t+1] = (dp[i][j][t+1] + dp[i-1][k][t])%mod;
}
如果本次是第一次,那连续出现次数只能是1,那么就加上上次结尾点数的各个出现次数的组合情况即可
for(int t = 1;t<=rollMax[k-1];++t)//此时,必须是k,因为此时循环的目的是上一个点连续出现不同次数的全部清空
//循环出现次数,因为上次出现的点数和本次点数不一样
dp[i][j][1] = (dp[i][j][1] + dp[i-1][k][t])%mod;
//相当于连续第一次出现
}
代码完整版本:
class Solution {
public:
int dieSimulator(int n, vector<int>& rollMax) {
if(rollMax.empty()||n == 0) return 0;
int MAX = 0,mod = 1e9+7;
for(auto item:rollMax) MAX = max(MAX,item);
vector<vector<vector<int>>>dp(n+1,vector<vector<int>>(7,vector<int>(MAX+1,0)));
//投掷n次,本次是j,连续出现次数为k次
for(int i = 1;i<=n;++i){
//投掷次数
for(int j = 1;j<=6;++j){
//本次点数
if(i == 1&&rollMax[j-1]) {dp[i][j][1] = 1;continue;}//base case
for(int l = 1;l<=6;++l){
//上次点数
if(j == l){
//上次和本次一样
for(int k = 1;k<=rollMax[l-1]-1;++k)
dp[i][j][k+1] = (dp[i][j][k+1] + dp[i-1][l][k])%mod;
}
else if(j != l){
//不一样,从0开始连续
for(int k = 1;k<=rollMax[l-1];++k)
dp[i][j][1] = ( dp[i][j][1] + dp[i-1][l][k])%mod;
//本次点数的组合:上次点数各种出现次数的全部情况的总和,所以加的是l,不是j
}
}
}
}
int sum = 0;
for(int j = 1;j<=6;++j){
//不同的点数
for(int t = 1;t<=rollMax[j-1];++t)//以不同点数结尾,连续出现的次数
sum = (sum + dp[n][j][t])%mod;
}
return sum;
}
};
反之,如果不一样,那么就当连续出现1次处理
https://leetcode-cn.com/problems/number-of-dice-rolls-with-target-sum/
最初的想法:
class Solution {
public:
int numRollsToTarget(int d, int f, int target) {
if(target == 0||(d*f)< target) return 0;
int mod = 1e9+7;
vector<vector<int>> dp(d+1,vector<int>(target+1,0));
//dp[i][j] = i个骰子,凑成target的情况数(一个骰子只能投一次)
for(int k = 1;k<=f&&k<=target;++k)//base case初始化
{
dp[1][k] = 1;
}
for(int i = 2;i<=d;++i)//骰子个数
{
for(int j = 1;j<=target;++j)//能凑成的金额
{
for(int k = 1;k<=f;++k)//骰子面额
if(j-k>=0)
dp[i][j] = (dp[i][j]+dp[i-1][j-k])%mod;
}
}
return dp[d][target];
}
};
也很好理解,第i个骰子的情况,要从第i-1个骰子的情况导出,第i个骰子点数为k,那么第i-1个骰子的点数为j-k即可完成目标。
5约束
https://leetcode-cn.com/problems/min-cost-climbing-stairs/
参考内容:
还是分两种情况,是否要踩上目前的所在的台阶
踩上:dp[i-1] + cost[i]
不踩上:dp[i-2] + cost[i-1]
此题务必注意初值的选择,题意也稍不好理解,写代码的时候也要注意。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
//增加一个第0层和第Top层,根据题意,最终要到达Top层
//到达每一层的方式有两种,从i-1走一步,从i-2走两步,二者求最小即可
if(cost.empty()) return 0;
int size = cost.size();
vector<int>dp(size+2,0);//到底第i层你能获得的花费
dp[0] = 0;//起点
dp[1] = cost[0];//到第一层你就没得选
for(int i = 2;i<=size;++i)
{
dp[i] = min(dp[i-2],dp[i-1])+cost[i-1];
}
dp[size+1] = min(dp[size+1-1],dp[size+1-2]);//最后一层,及顶层
return dp[size+1];
}
};
6股票问题
本文关于股票问题,主要参考一下内容
股票问题模板如下:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 选择 rest , 选择 sell )
解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )
解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。
作者:labuladong
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-l-3/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
@liweiwei1419对股票问题进行了极其详尽的分析
@labuladong对股票问题进行了整理,给出了一套完整的模板(链接如下:)
其中也对初值提出了要求:
dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。
dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。
作者:labuladong
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-l-3/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
作者:labuladong
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-l-3/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
下面我们来看题目:
121. 买卖股票的最佳时机 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/
只能交易一次
class Solution {
public:
int maxProfit(vector<int>& prices) {
int size = prices.size();
if(size < 2) return 0;
vector<vector<int>> dp(size+1,vector<int>(2,0));
//dp表示在第i天,持有或未持有的状态下,所得到的最大利润
// for(int i = 1;i<=size;++i) dp[i][] = ;
dp[0][1] = -prices[0];dp[0][0] = 0;
for(int i = 1;i<=size;++i)
{
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i-1]);
//今天未持有,表示今天卖了,或者昨天就没有持有
dp[i][1] = max(dp[i-1][1],0-prices[i-1]);
//今天持有,表示今天买了了,或者昨天就持有
}
return dp[size][0];//最大利润,就是在规定天数内完成交易,而不是留着手里
}
};
本题有两种状态:天数,持有还是未持有,dp状态方程一定是要将所有的状态覆盖的,不然不能称之为状态方程
尽可能多的交易
122. 买卖股票的最佳时机 II https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/
class Solution {
public:
int maxProfit(vector<int>& prices) {
int size = prices.size();
if(size < 2) return 0;
vector<vector<int>> dp(size+1,vector<int>(2,0));
//dp表示在第i天,持有或未持有的状态下,所得到的最大利润
dp[0][1] = -prices[0];//第0天持有股票,不可能,因为后面求最值,所以负无穷
dp[0][0] = 0;//没有持有股票,利润为零
for(int i = 1;i<=size;++i)
{
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i-1]);
//今天未持有,1表示今天卖了,2昨天就没有持有
dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i-1]);
//本题允许完成无数次交易,买入必须在卖出之前,dp[i-1][0]-prices[i-1]表示今天买了
//今天持有,2表示今天买了了,1昨天就持有
}
return dp[size][0];//最大利润,就是在规定天数内完成交易,而不是留着手里
}
};
从初始状态方程定base case
增加约束
309. 最佳买卖股票时机含冷冻期 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/
冷冻期
class Solution {
public:
int maxProfit(vector<int>& prices) {
int size = prices.size();
if(size < 2) return 0;
vector<vector<int>> dp(size+1,vector<int>(2,0));
//dp表示在第i天,持有或未持有的状态下,所得到的最大利润
dp[0][1] = -prices[0];//第0天持有股票,不可能,因为后面求最值,所以负无穷
dp[0][0] = 0;//没有持有股票,利润为零
dp[1][1] = -prices[0];//第1天持有股票,只能买了
dp[1][0] = 0;//没有持有股票,利润为零
for(int i = 2;i<=size;++i)
{
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i-1]);
//今天未持有,1表示今天卖了,2昨天就没有持有
dp[i][1] = max(dp[i-1][1],dp[i-2][0]-prices[i-1]);
//本题允许完成无数次交易,买入必须在卖出之前,加上要考虑冷冻期,dp[i-2][0]-prices[i-1]表示今天买了
//今天持有,2表示今天买了了,1昨天就持有
}
return dp[size][0];//最大利润,就是在规定天数内完成交易,而不是留着手里
}
};
手续费
714. 买卖股票的最佳时机含手续费 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int size = prices.size();
if(size < 2) return 0;
vector<vector<int>> dp(size+1,vector<int>(2,0));
//dp表示在第i天,持有或未持有的状态下,所得到的最大利润
dp[0][1] = -prices[0];//第0天持有股票,不可能,因为后面求最值,所以负无穷
dp[0][0] = 0;//没有持有股票,利润为零
for(int i = 1;i<=size;++i)
{
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i-1]-fee);
//今天未持有,1表示今天卖了,2昨天就没有持有
dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i-1]);
//本题允许完成无数次交易,买入必须在卖出之前,dp[i-1][0]-prices[i-1]表示今天买了
//今天持有,2表示今天买了了,1昨天就持有
}
return dp[size][0];//最大利润,就是在规定天数内完成交易,而不是留着手里
}
};
限制交易次数
下面会列出dp table,我们从中就可以看出,外循环是天数还是交易次数,都无关紧要。
从状态方程中我们也能看出,先按k = 1计算一行,还是先按照i=1计算一列,问题都不会受到影响
绿色部分对应的dp状态方程如下:
dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k][1]+prices[i-1]);
灰色部分对应的dp状态方程如下:
dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i-1]);
123. 买卖股票的最佳时机 III https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/
base case 的情况还是一样的,第0天,未持有利润为0,持有为无效状态
class Solution {
public:
int maxProfit(vector<int>& prices) {
int size = prices.size(),K = 2;
if(size < 2) return 0;
vector<vector<vector<int>>>dp(size+1,vector<vector<int>>(K+1,vector<int>(2,0)));
//表示在第i天,完成k次交易,目前手中是否持有股票的情况下,最大利润
for(int k = 1;k<=K;++k)
{
dp[0][k][1] = -prices[0];//第0天持有股票,不可能,因为后面求最值,所以负无穷
dp[0][k][0] = 0;//没有持有股票,利润为零
}
for(int i = 1;i<=size;++i)//天数
{
for(int k = 1;k<=K;++k)//交易次数
{
dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k][1]+prices[i-1]);
// cout<<"i:"<<i<<"0:"<<dp[i][k][0]<<endl;
//未持有,昨天就未持有,今天才卖的
dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i-1]);
// cout<<"i:"<<i<<"1:"<<dp[i][k][1]<<endl;
//持有,昨天就持有,今天才买的
}
}
return dp[size][K][0];
}
};
其中内外循环可以颠倒:
for(int k = 1;k<=K;++k)//交易次数
{
for(int i = 1;i<=size;++i)//天数
{
dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k][1]+prices[i-1]);
// cout<<"i:"<<i<<"0:"<<dp[i][k][0]<<endl;
//未持有,昨天就未持有,今天才卖的
dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i-1]);
// cout<<"i:"<<i<<"1:"<<dp[i][k][1]<<endl;
//持有,昨天就持有,今天才买的
}
}
188. 买卖股票的最佳时机 IV https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/
此处的初值最后是对天数为0的状态开始设置,比较好理解,要去初始化天数为1的情况,不好理解,如果要初始化天数为1,循环从第二天开始,初始化部分如下:
dp[1][2][0] = -prices[0];//不可能状态
dp[1][2][1] = -prices[0];//不可能状态
dp[1][1][0] = 0;//交易一次但是没有持有
dp[1][1][1] = -prices[0];//不可能状态
dp[1][0][0] = 0;//不交易不持有,固然是0
dp[1][0][1] = -prices[0];//不可能状态
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int size = prices.size();
if(size < 2) return 0;
if(k >= prices.size()/2)//加一个保险措施,交易次数大于可交易天数的一半
{
int sum = 0;
for(int i = 1; i < prices.size(); i++)
{
int val = prices[i] - prices[i - 1];
sum += (val > 0? val: 0);
}
return sum;
}
vector<vector<vector<int>>>dp(size+1,vector<vector<int>>(k+1,vector<int>(2,0)));
//表示在第i天,完成k次交易,目前手中是否持有股票的情况下,所取得的最大利润
for(int j = 1;j<=k;++j)
{
dp[0][j][1] = -prices[0];//第0天持有股票,不可能,因为后面求的是最大值,所以负无穷
dp[0][j][0] = 0;//没有持有股票,利润为零
}
for(int j = 1;j<=k;++j)//交易次数
{
for(int i = 1;i<=size;++i)//天数
{
dp[i][j][0] = max(dp[i-1][j][0],dp[i-1][j][1]+prices[i-1]);
//未持有,昨天就未持有,今天才卖的
cout<<"i :"<<i<<" "<<dp[i][k][0]<<endl;
dp[i][j][1] = max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i-1]);
//持有,昨天就持有,今天才买的
cout<<"i :"<<i<<" "<<dp[i][k][1]<<endl;
}
}
return dp[size][k][0];
}
};
那么k=2也可以这么写
class Solution {
public:
int maxProfit(vector<int>& prices) {
int size = prices.size(),K = 2;
if(size < 2) return 0;
vector<vector<vector<int>>>dp(size+1,vector<vector<int>>(K+1,vector<int>(2,0)));
//表示在第i天,完成k次交易,目前手中是否持有股票的情况下,所取得的最大利润
for(int k = 1;k<=K;++k)
{
dp[0][k][1] = -prices[0];//第0天持有股票,不可能,因为后面求的是最大值,所以负无穷
dp[0][k][0] = 0;//没有持有股票,利润为零
}
for(int k = 1;k<=K;++k)//交易次数
{
for(int i = 1;i<=size;++i)//天数
{
dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k][1]+prices[i-1]);
//未持有,昨天就未持有,今天才卖的
cout<<"i :"<<i<<" "<<dp[i][k][0]<<endl;
dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i-1]);
//持有,昨天就持有,今天才买的
cout<<"i :"<<i<<" "<<dp[i][k][1]<<endl;
}
}
return dp[size][K][0];
}
};
让交易次数为外循环
6区间 DP
都是鬼才解法,不看答案鬼才能知道怎么解
https://leetcode-cn.com/problems/count-square-submatrices-with-all-ones/
参考内容:
(来源:leetcode官方)
相当巧妙的方式方法,而此解法,也成为了区间DP题目和核心状态方程
第一行第一列的值就是上面表达式的第一行,如果矩阵的值是0,那么也没的算了
我们再次理解一下状态方程:
无非两种情况,dp[i-1][j-1]最小,或者dp[i-1][j]或者dp[i][j-1]最小:
只要三者不为0,最小边长起码是2。
class Solution {
public:
int countSquares(vector<vector<int>>& matrix) {
int m = matrix.size(),n = matrix[0].size();
vector<vector<int>>dp(m+1,vector<int>(n+1,0));
//dp[i][j]以ij为右下角的正方形,个数
int sum = 0;
for(int i = 1;i<=m;++i)
{
for(int j = 1;j<=n;++j)
{
if(i == 1||j == 1)//第一行第一列
dp[i][j] = matrix[i-1][j-1];
else if(matrix[i-1][j-1] ==0)
dp[i][j] = 0;
else
dp[i][j] = min(dp[i-1][j],min(dp[i-1][j-1],dp[i][j-1]))+1;
sum += dp[i][j];
}
}
return sum;
}
};
https://leetcode-cn.com/problems/maximal-square/
参考内容:
https://leetcode-cn.com/problems/maximal-square/solution/li-jie-san-zhe-qu-zui-xiao-1-by-lzhlyle/
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
if(matrix.empty()||matrix[0].empty()) return 0;
int row = matrix.size(),col = matrix[0].size();
vector<vector<int>>dp(row+1,vector<int>(col+1,0));
// 以i,j为右下角的正方形最大的边长
int L = 0;//记录最大边长
for(int i = 1;i<=row;++i)
{
for(int j = 1;j<=col;++j)
{
if(matrix[i-1][j-1] == '1')//只要当目前点是1的时候,才有意义
{
if(i==1||j==1)
dp[i][j] = matrix[i-1][j-1] - '0';
//第一行和第一列,组成正方形边长就是自己本身的值
else
{
//一旦三个接触的地方,有
if(dp[i - 1][j] == 0|| dp[i][j - 1] == 0||dp[i - 1][j - 1] == 0)
dp[i][j] = matrix[i-1][j-1] - '0';
else
dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
}
L = max(L,dp[i][j]);
}
}
return L*L;
}
};
以上两个题非常的相似,其实dp定义好了就很好计算这种题目,dp都是以ij为右下角的正方形最大的边长
注意,核心是以ij为正方形的右下角,这是非常关键的一个点,可以说是结题的核心了
https://leetcode-cn.com/problems/minimum-score-triangulation-of-polygon/
参考内容:
本题技巧性极强。
class Solution {
public:
int numTrees(int n) {
if(n == 0) return 0;
vector<int> dp(n+1,0);
//长度为n的序列的不同二叉搜索树个数(和序列内容无关,之和长度有关)
dp[0] = 1;
dp[1] = 1;
for(int i = 2;i<=n;++i)//长度为i的序列
{
for(int j = 1;j<=i;++j)//长度为n的序列元素
{
dp[i] += dp[j-1]*dp[i-j];//j为根时,以j为根,将序列分为两个部分
}
}
return dp[n];
}
};
5419. 两个子序列的最大点积 https://leetcode-cn.com/problems/max-dot-product-of-two-subsequences/
来自于一道周赛的题目:
class Solution {
public:
int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
int sz1=nums1.size(),sz2=nums2.size();
vector<vector<int>> dp(sz1+1,vector<int>(sz2+1,-1e8));
//初值是-1e8
for(int i=1;i<=sz1;i++){
for(int j=1;j<=sz2;j++){
//1.1
dp[i][j]=nums1[i-1]*nums2[j-1];
//1.2
dp[i][j]=max(dp[i][j],dp[i][j]+dp[i-1][j-1]);
//2
dp[i][j]=max(dp[i][j],dp[i][j-1]);
//3
dp[i][j]=max(dp[i][j],dp[i-1][j]);
//4
dp[i][j]=max(dp[i][j],dp[i-1][j-1]);
}
}
return dp[sz1][sz2];
}
};
// 作者:smilyt_
// 链接:https://leetcode-cn.com/problems/max-dot-product-of-two-subsequences/solution/c-dong-tai-gui-hua-yi-dong-by-smilyt_/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我们可以这么定义dp:
dp[i][j]:第一个序列从1到i,第二个序列从1到j,选择元素相乘得到的最大点积
那么状态方程是什么呢?
1:只选择第i和第j个:dp[i][j] = nums[i]*nums[j]
2:选择第i和第j个:dp[i][j] = dp[i-1][j-1]+nums[i]*nums[j]
3:不选择第i和第j个:dp[i][j] = dp[i-1][j-1];
因为可以自由组合,那么有:
4:选择第i个,不选择第j个:dp[i][j] = dp[i][j-1];
5:不选择第i个,选择第j个:dp[i][j] = dp[i-1][j];
那么我们综合考虑以上四种情况,选择最值即可。
base case全部是-1e8,因为后面要比大小,需要设置为无效模式
https://leetcode-cn.com/problems/burst-balloons/
本题难在dp的定义上,初看此题,根本无法入手,一般都是想到回溯法:
从dp table我们能看出我们遍历的方式,j从底层到顶层,i反之
注意dp base case
本题初见,几乎是很难拿下,逆向思维太难想到,因为戳最后一个气球,对比选第一个戳,容易很多,此处因为是最后一个气球,两侧也就只有i和j了,状态方程很好给。
class Solution {
public:
int maxCoins(vector<int>& nums) {
//令dp[start][end]表示从start到end之间的气球戳破之后能得到的最大值
//头尾增加虚拟结点,都是1
if(!nums.size()) return 0;
int NewSize = nums.size() + 2;
vector<int> Newnums(NewSize);
Newnums[0] = Newnums[NewSize-1] = 1;//收尾是1
int index = 1;
for(auto item:nums) {Newnums[index++] = item;}
//dp设置
vector<vector<int>>dp(NewSize,vector<int>(NewSize,0));
//dp:以i开始,j结束,在这个开区间内,气球被戳破后得到硬币的最大数量
//戳破气球 i 和气球 j 之间(开区间,不包括 i 和 j)的所有气球,可以获得的最高分数为 x
// i 应该从下往上
for (int i = NewSize - 2; i >= 0; i--)
{
for (int j = i + 1; j < NewSize; j++)
{
// 最后戳破的气球是哪个?
for (int k = i + 1; k < j; k++)
{
dp[i][j] = max(dp[i][j],dp[i][k] + dp[k][j] + Newnums[i]*Newnums[j]*Newnums[k]);
}
}
}
return dp[0][NewSize-1];
}
};
7最长子序列/子字符串
子序列默认不连续,子数组默认连续
718. 最长重复子数组 https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/
本题要求重叠子数组,那么显然,默认是连续的,本题并不难,我们直接看代码:
class Solution {
public:
int findLength(vector<int>& A, vector<int>& B) {
if(A.empty()||B.empty()) return 0;
int size1 = A.size(),size2 = B.size();
vector<vector<int>>dp(size1+1,vector<int>(size2+1,0));
//第一个数组的以i结尾,第二个数组以j结尾,最长子数组长度
// 本题此处指的是连续子数组(子数组默认是连续的)
int MAX = 0;
for(int i = 1;i<=size1;++i){
for(int j = 1;j<=size2;++j){
if(A[i-1] != B[j-1]) dp[i][j] = 0;
else if(A[i-1] == B[j-1]){
if(dp[i-1][j-1] != 0) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = 1;
}
MAX = max(MAX,dp[i][j]);
}
}
return MAX;
}
};
https://leetcode-cn.com/problems/longest-increasing-subsequence/
给定一个无序的整数数组,找到其中最长上升子序列的长度。
这题,暴力能不能算,那是当然,从第一个元素遍历,不断和后面的数值比较,大于,更新指针位置顺便更改能组成的长度。
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < n; ++j)
{
if (cur < nums[j])
{
cur = nums[j];
length++;
}
}
}
大致逻辑如上,遇到比cur目前指向的大,增加长度,更新cur指向。
那么我们在这个过程中,怎么标记我们找到的序列长度?是以某元素开头的最长序列为x,还是以某元素结尾的最长序列长度为x
这两种方式我们应该选择哪儿一种?选以某元素结尾的最长序列长度为x。循环的次数最少,以第一个元素开头,要循环n次,以第一个元素结尾,只需要循环1次。
那么整个过程中有没有重复的计算呢?暴力解法当然有,下面我们就利用
状态转移方程
dp表是为了解决重叠子问题
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int size = nums.size();
if(size == 0) return 0;
vector<int> dp(size,1);//存放以某个元素结尾,能组成最长的序列的长度,默认长度就是1
int MAX = 0;
for(int i = 0;i<size;++i)
{
for(int j = 0;j<i;++j)//j 的上限是 i,因为是以i结尾,只看i前面的数值
{
if(nums[j]<nums[i])//前面的数值一旦小于i本身,那么就可以加入i结尾的序列
{
dp[i] = max(dp[i],dp[j]+1);
}
}
MAX = max(MAX,dp[i]);//最终要找最长的,那么找到dp的最大值即可。
}
return MAX;
}
};
https://leetcode-cn.com/problems/word-break/
dp[i]表示,第1到i个子字符串,能不能被拆分成字典中的元素。
给两个指针,i和j,i从1开始,终点为字符串长度,意义为每次给出原始字符串的子字符串,看看能不能被拆解为字典中的元素
j表示所给定子字符串的分割位置,将子字符串分割为s1(0,j-1)和s2(j,i-j)
以leetcode举例子,当j=4的时候,s1(0,j-1) = leet,s2(j,i-j) = code;
使用C++库函数substr(pos,n),返回一个string,包含s中从pos开始的n个字符的拷贝
我们两个循环,外循环控制整个字符串中子字符串的分割情况,内循环控制子字符串内部的分割情况
当s1在能被字典中的元素组成的时候,dp[j] == true
s2也能够被字典中的元素组成的时候,words.find(s.substr(j, i-j)) != words.end()
二者同时存在,说明长度为i的子字符串dp[i] == true。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
set<string> words;
for(int i=0; i<wordDict.size(); i++){
words.insert(wordDict[i]);
}
vector<bool> dp(s.length()+1);
dp[0] = true;
for (int i = 1; i <= s.length(); i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && words.find(s.substr(j, i-j)) != words.end() ) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
};
https://leetcode-cn.com/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof/
53. 最大子序和 https://leetcode-cn.com/problems/maximum-subarray/
dp设置的好,题就好解
求子序列,因为连续,所以有难度,下面我们看看dp和状态方程
以i结尾,要么a重新开头,要么跟在i-1后面,我们在整个过程中记录最大值即可。
以下为各个dp中保存的子序列的长度
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if(nums.empty()) return 0;
int size = nums.size();
vector<int>dp(size+1,0);
//以i结尾的连续子数组,最大和
//base case
int MAX = INT_MIN;//因为有负数,所以需要设置为INT_MIN
for(int i = 1;i<=size;++i)
{
dp[i] = max(nums[i-1],nums[i-1]+dp[i-1]);//比较自己本身,和前者加和,谁更大
MAX = max(MAX,dp[i]);//记录最大和
}
return MAX;
}
};
https://leetcode-cn.com/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/
那么状态方程如下,当i-1和i可以翻译的时候,考虑翻译i-1&i,和单独翻译i的情况
翻译不了的时候就只能考虑翻译i了
本题务必注意base case的情况,还有特殊情况,比如506,只有一种情况,因为06如果算了一次,单独的6再算一次,重复了,所以只能算一次
class Solution {
public:
int translateNum(int num) {
string nums = to_string(num);
int size = nums.size();
vector<int>dp(size+1,0);//以i结尾,能有几种组合方式
//base case
dp[1]=1;
for(int i = 2;i<=size;i++)
{
string temp = nums.substr(i-2,2);//此处是i-2
if(nums[i-2]!='0'&&temp<="25"&&temp>="0")
//如果是06,那么就不能组成数字,只能按照6算,所以起点不能是0
{
if(i == 2) dp[i] = dp[i-1] + 1;
//base case初始位置下,第一个和第二个要是可以组成,那就要加1
else dp[i] = dp[i-1] + dp[i-2];
}
else dp[i] = dp[i-1];
}
return dp[size];
}
};
解法二:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
vector<bool>dp(s.size()+1,false);
//长度为 i 的子串前缀,可以完全被分割成指定字符
unordered_map<string,int>M;
for(auto item:wordDict) M[item]++;
dp[0] = true;
//长度为j的可以被分割,j到i也可以被分割,那么0-i也可以被分割
//有:dp[j]&&M.find(s.substr(j,i-j)!=M.end()
for(int i = 1;i<=s.size();++i)//长度
{
for(int j = 0;j<i;++j)//起点
{
if(dp[j]&&M.find(s.substr(j,i-j))!=M.end())
{
dp[i] = true;
break;
}
}
}
return dp[s.size()];
}
};
8回文子序列/子字符串
回文子序列或子串
在动态规划中,回文有一个较为通用的模板:
dp[i][j]以ij做收尾的子串是回文的情况
下面是一道非动态规划,但是也有助于我们理解回文:
回文还有一种处理思路:
https://leetcode-cn.com/problems/valid-parentheses/
class Solution {
public:
bool isValid(string s) {
if(s.empty()) return true;
stack<char> S;//将相关内容压入栈中
for(char item:s)
{
//遇到右值,全部进行记录
if(item == '(') S.push('(');
if(item == '{') S.push('{');
if(item == '[') S.push('[');
if(item == ')')
{
//栈为空或者不匹配,直接return false
if(S.empty()) return false;
if(S.top()!='(') return false;
else S.pop();//对比完记着一定要弹出
}
if(item == '}')
{
if(S.empty()) return false;
if(S.top()!='{') return false;
else S.pop();
}
if(item == ']')
{
if(S.empty()) return false;
if(S.top()!='[') return false;
else S.pop();
}
}
return S.size()?false:true;//同样的,对比完成后栈必须是空的,不能有遗留
}
};
在判断回文的时候也要务必注意:子串和子序列是有区别的,子串必须是连续的,子序列可以是离散的
所以判断子串是不是回文,一般两端,及起点和终点不相等,基本可以判断不是回文
子序列不同,子序列只要保证元素相对的顺序,不需要是连续的
https://leetcode-cn.com/problems/longest-palindromic-substring/
所以,我们设dp如下:
状态方程也很好列出:
本题还有个细节,就是dp table的情况,看下图
所以需要控制后限,如下程序所示,保证都算了
我们再来看看完整版本的dp table
如果我们要求1到6子串是不是回文,此时1和6相等,那么剩下的就完全取决于2到5了
所以首先要计算2到5,也就是控制终点,从1到size,起点在1到终点j之间即可。
从图上也很清晰的可以看出,我们应该怎么进行计算
class Solution {
public:
string longestPalindrome(string s) {
int size = s.size();
if(size == 0) return "";
int MAX = 0;
string Res;
vector<vector<bool>>dp(size+1,vector<bool>(size+1,false));
for(int i = 1;i<=size;++i) dp[i][i] = true;//单个元素算回文
for(int j = 1;j<=size;++j)//必须固定后限进行循环
{
for(int i = 1;i<=j;++i)
{
if(s[i-1] == s[j-1])
{
// cout<<"123"<<endl;
if(j-i+1<=3) dp[i][j] = true;
else dp[i][j] = dp[i+1][j-1];
}
else
dp[i][j] = false;
if(dp[i][j]&&MAX<(j-i+1))
{
MAX = j-i+1;
Res = s.substr(i-1,MAX);
// cout<<"MAX "<<MAX<<endl;
}
}
}
return Res;
}
};
解法二:计算距离
class Solution {
public:
string longestPalindrome(string s) {
if(s.empty()) return "";
vector<vector<int>>dp(s.size()+1,vector<int>(s.size()+1,0));
string res;
int MAX = INT_MIN;
for(int i = s.size();i>=1;--i){
for(int j = i;j<=s.size();++j){
if(i == j) dp[i][j] = 1;
else if(s[i-1] == s[j-1]){
if(j-i<3) dp[i][j] =j-i+1;
else if(dp[i+1][j-1] == j-i-1) dp[i][j] = 2+dp[i+1][j-1];
else dp[i][j] = dp[i+1][j-1];
}
else dp[i][j] = dp[i+1][j-1];
if(MAX<dp[i][j]){
MAX = dp[i][j];
res = s.substr(i-1,j-i+1);
}
}
}
return res;
}
};
https://leetcode-cn.com/problems/palindromic-substrings/
本题为上一道题目的扩展,我们增加一个计数功能即可,其他从dp到base case都一样
class Solution {
public:
int countSubstrings(string s) {
if(s.empty()) return 0;
int size = s.size();
int Res=0;
vector<vector<bool>>dp(size+1,vector<bool>(size+1,false));
//base case
for(int i = 1;i<=size;++i) dp[i][i] = true;
for(int i = size;i>=1;--i)//起点
{
for(int j = i;j<=size;++j)//终点
{
if(i == j&&dp[i][j]) {Res++;continue;}
if(s[i-1] == s[j-1]&&j-i < 2) dp[i][j] = true;
else if(s[i-1] != s[j-1]) dp[i][j] = false;
else dp[i][j] = dp[i+1][j-1];
//统计结果
if(dp[i][j]) Res++;
}
}
return Res;
}
};
https://leetcode-cn.com/problems/longest-palindromic-subsequence/
我们把子串改成子序列,难度升级。
就是状态方程有所改变,因为是求子序列的最长长度,求啥设啥,就让dp为长度
我们继续按照上一题的思路思考,此时是子序列,而之前是子串,子串是必须连续的,那么我们只考虑i+1和j-1的情况就可以了
但是子序列内容,需要考虑情况就自然要多
当i和j不相等的时候,我们要考虑整个组合的情况,三者比大小,最大的作为dp[i][j]的值。下面我们列出状态方程:
下面我们看看dp table,看看如何计算:
本题还有一个要点,就是当i和j相等的时候,dp[i+1][j-1]+2
这也是关键,举例:bbbab
因为是子序列,完全忽视了子串的连续性,只要相等,起码两个
j要从底端晚上算,i要从小到大算,base case不用计算已经给好了
完整代码如下:
class Solution {
public:
int longestPalindromeSubseq(string s) {
int size = s.size();
if(size == 0) return 0;
vector<vector<int>> dp(size+1,vector<int>(size+1,0));
//以i开始,j结束,字符串在此区间内最长的回文子序列
for(int i = 1;i<=size;++i) dp[i][i] = 1;
for(int j = 1;j<=size;++j)
{
for(int i = j-1;i>=1;--i)
{
if(s[i-1] == s[j-1]) dp[i][j] = dp[i+1][j-1]+2;
else
dp[i][j] = max(dp[i+1][j-1],max(dp[i+1][j],dp[i][j-1]));
}
}
return dp[1][size];
}
};
https://leetcode-cn.com/problems/palindrome-partitioning-ii/
参考解法:
分割回文串的第一题是用回溯算法解决的,本题难度升级,我们需要使用动态规划,不同于上面两道题目的设法,那样太复杂了,本题dp这么定义:
我们下面看这么一种情况
base case要谨慎处理:
//长度为n的字符,最多分割n-1次,以此作为base case
for(int i = 1;i<=size;++i)
dp[i] = i-1;
核心代码:
//本题的核心代码
for(int i = 1;i<=size;++i)
{
if(checkPalindrome[1][i]) {dp[i]=0;continue;}//如果从头到i,都是回文,那就不用分割
for(int j = 1;j<i;++j)
{
//开始进行分割,0到j,j+1到i,检查j+1到i是不是回文,如果是,就切一刀
if(checkPalindrome[j+1][i]) dp[i]=min(dp[i],dp[j]+1);
}
}
完整代码:
class Solution {
public:
int minCut(string s) {
int size = s.size();
if(size == 0) return 0;
vector<int>dp(size+1,0);//从1到i(前i个字符),字符串分割为回文,需要最少的切割次数
//长度为n的字符,最多分割n-1次,以此作为base case
for(int i = 1;i<=size;++i)
dp[i] = i-1;
//创建二维数组用于记录子串s[a:b]是否为回文串,且一开始全部初始化为false(可以发现a<=b)
//此部分和5的核心代码是一样的
vector<vector<bool>> checkPalindrome(size+1, vector<bool>(size+1, false));
for(int i = 1;i<=size;++i) checkPalindrome[i][i] = true;//单个元素算回文
for(int j = 1;j<=size;++j)//必须固定后限进行循环
{
for(int i = 1;i<=j;++i)
{
if(s[i-1] == s[j-1])
{
if(j-i+1<=3) checkPalindrome[i][j] = true;
else checkPalindrome[i][j] = checkPalindrome[i+1][j-1];
}
}
}
//本题的核心代码
for(int i = 1;i<=size;++i)
{
if(checkPalindrome[1][i]) {dp[i]=0;continue;}
for(int j = 1;j<i;++j)
{
//开始进行分割,0到j,j+1到i,检查j+1到i是不是回文,如果是,就切一刀
if(checkPalindrome[j+1][i]) dp[i]=min(dp[i],dp[j]+1);
}
}
return dp[size];
}
};
https://leetcode-cn.com/problems/count-different-palindromic-subsequences/
https://leetcode-cn.com/problems/zheng-ze-biao-da-shi-pi-pei-lcof/
整体逻辑:
1 正常情况:那就是看二者相等不相等就可以了
2 ‘.’:和上面一样,遇到‘.’就是遇到万能内容
3 ‘*’:此时我们需要分类讨论,*可以取怎么的值?我们后续讨论
如果字符相等,那么当然是能匹配了
如果是.,万能字符,也是当然没有问题了
以上两种情况,很容易就写出来了:
if(s[i-1] == p[j-1]||p[j-1] == '.')//相等或者是.,那么就没有问题
dp[i][j] = dp[i-1][j-1];
那么如果是'*',那么该怎么办:
就要看*前面的字符
我们来看i和j-1的情况,不匹配,那么直接跳过,也可以认为是*前面的元素出现了0次
if(s[i-1]!=p[j-2] && p[j-2]!='.') dp[i][j]=dp[i][j-2];
那么我们继续讨论剩下的情况,如果二者相等,或者p为万能元素.呢?
那么就要分三种情况来讨论了
*可以让前面的元素出现0,1,无数次,我们就这么分类来比较
第一种情况,*让前面的元素出现0次
第二种情况,*让前面的元素出现一次
第三种情况,*让前面的元素出现多次
三种一种是true就可以
dp[i][j]=dp[i][j-1] || dp[i][j-2] || dp[i-1][j];
细节1:
s=" "+s;//防止该案例:"aab" "c*a*b"
p=" "+p;
一上来就是*,如下:
细节2:不需要保护程序
if(s.empty()||p.empty()) return false;
完整代码:
class Solution {
public:
bool isMatch(string s, string p) {
s=" "+s;//防止该案例:"aab" "c*a*b"
p=" "+p;
int m=s.size(),n=p.size();
vector<vector<int>>dp(m+1,vector<int>(n+1,false));
//dp含义:s中的前i个,和p中的前j个,是否匹配
//base case
dp[0][0]=true;
for(int i = 1;i<=m;++i)
{
for(int j = 1;j<=n;++j)
{
if(s[i-1] == p[j-1]||p[j-1] == '.')//相等或者是.,那么就没有问题
dp[i][j] = dp[i-1][j-1];
else if(p[j-1]=='*')//最难处理的部分
{
if(s[i-1]!=p[j-2] && p[j-2]!='.') dp[i][j]=dp[i][j-2];
else dp[i][j]=dp[i][j-1] || dp[i][j-2] || dp[i-1][j];
}
}
}
return dp[m][n];
}
};
https://leetcode-cn.com/problems/edit-distance/
字符串类型的dp一般设法都显示,我们dp定义如下:
那么有三种情况需要注意:也就是题目中所述的替换,插入,删除
那么我们的状态方程就是求上面三种操作的最小值
下面是base case
如果一个字符串长度为0,那么要将其变成一个非零的字符串,需要的操作数就是非零字符串的长度
完整代码:
class Solution {
public:
int minDistance(string word1, string word2) {
int s = word1.size(),p = word2.size();
if(s == 0&&p == 0) return 0;
//一个字符串为0,那么操作数就是另外一个字符串的长度
if(s == 0&&p!=0) return p;
if(s!=0&&p==0) return s;
vector<vector<int>>dp(s+1,vector<int>(p+1,0));
//word1前i个,及word2前j个,匹配需要的最少操作数
//base case 一个字符串为0,那么操作数就是另外一个字符串的长度
for(int i = 0;i<=s;++i) dp[i][0] = i;
for(int j = 1;j<=p;++j) dp[0][j] = j;
for(int i = 1;i<=s;++i)
{
for(int j = 1;j<=p;++j)
{
if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];//不操作
else
dp[i][j] = 1+min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j]));
}
}
return dp[s][p];
}
};
https://leetcode-cn.com/problems/longest-valid-parentheses/
那么第一种情况很好想到:
那么下面这种情况呢?
那么我们不知道之前的情况,因为要给i处的)找到(,我们需要看dp[i-1]的情况,如果是dp[i-1]不为0,如图,那么跨过整个dp[i-1]覆盖的区域,找到i-1-dp[i-1],如果可以和i处的匹配,那么显然是可以组成的
本题最难的地方,也是本题最为巧妙的地方
class Solution {
public:
int longestValidParentheses(string s) {
//动态规划
if(s.empty()) return 0;
int size = s.size();
vector<int>dp(size,0);//以i结尾的最长合法子串长度
int MAX = 0;
dp[0] = 0;
for(int i = 1;i<size;++i)
{
if(s[i] == ')')
{
if(s[i-1] == '(')
{
if(i-2>0&&dp[i-2]!=0) dp[i] = dp[i-2] + 2;
else dp[i] = 2;
}
else if(i - dp[i - 1] > 0 && s[i - dp[i - 1] - 1] == '(')
//我们查看dp[i-1]的情况,如果是0,那么s[i-1]就是),不进行任何操作
//如果dp有值,也就是说以i-1结尾是能组成有效括号的,那么我们去查看
//索引为i - 1 - dp[i-1]元素的情况,如果是(,那么恰好可以和i的括号组成有效括号
//最后我们依旧要加上i-1-dp[i-1]-1索引下的dp
{
if(i-1-dp[i-1]-1>0)
dp[i] = 2+dp[i-1]+dp[i-1-dp[i-1]-1];
else dp[i] = 2+dp[i-1];
}
}
else dp[i] = 0;
MAX = max(MAX,dp[i]);
}
return MAX;
}
};
9概率
概率问题,要不就设分子,要不就设整体,哪儿个好来用哪儿个
https://leetcode-cn.com/problems/nge-tou-zi-de-dian-shu-lcof/
本题难点在于dp的定义
定义dp为:
class Solution {
public:
vector<double> twoSum(int n) {
vector<double> Res;
vector<vector<int>>dp(n+1,vector<int>(6*n+1,0));
//投掷j个骰子,面数是i出现的次数
for(int i = 1;i<=n;++i)//投掷次数
{
for(int j = i;j<=6*i;++j)//出现点数和,注意起点,2个骰子不可能有1,所以j = i起步
{
if(i == 1) {dp[i][j] = 1;continue;}//base case
for(int val = 1;val<=6;++val)//第n个骰子投出的点数
{
if(j - val >0)//保护程序
dp[i][j] += dp[i-1][j-val];
}
}
}
for(int j = 1;j<=6*n;++j)
{
if(dp[n][j]!=0)
{Res.push_back(dp[n][j]*pow(1.0/6, n));}
}
return Res;
}
};
程序中有很对的细节需要注意。
写法二:
class Solution {
public:
vector<double> twoSum(int n) {
if(n == 0) return {};
vector<double>Res;//n~6*n之间
vector<vector<double>>dp(n+1,vector<double>((n+1)*6,0));
//i个筛子,组成和为j的概率
//base case
double g = (1.0/6);
// cout<<g<<endl;
for(int j = 1;j<=6;++j)
{
dp[1][j] = g;//初始化
}
for(int i = 2;i<=n;++i)//个数
{
for(int j = i;j<=i*6;++j)
{
for(int l = 1;l<=6;++l)
{
if(j-l>=0)
{
dp[i][j] += (g)*dp[i-1][j-l];//第i个显示l,那么前面就要组成j-l才行
}
else break;
}
// cout<<i<<" "<<j<<endl;
}
}
for(int j = n;j<=n*6;++j)
{
Res.push_back(dp[n][j]);
}
return Res;
}
};
https://leetcode-cn.com/problems/new-21-game/
https://leetcode-cn.com/problems/new-21-game/solution/xin-21dian-by-leetcode-solution/
下面是leetcode官网的解答:
假设dp[x]为她手上牌面为x时,能获胜的概率,那么这个概率应该是:
dp[x]=1/w * (dp[x+1]+dp[x+2]+dp[x+3]...+dp[x+w])
来源:https://leetcode-cn.com/problems/new-21-game/solution/huan-you-bi-zhe-geng-jian-dan-de-ti-jie-ma-tian-ge/
本题颇有难度,而且属于不常见的从后往前递推的情况,具体讲解和代码请看官网的解答。
10树状DP
https://leetcode-cn.com/problems/house-robber/
可以参考下文,给出了整个过程的动态流程
本题还是比较简单的,我们这dp如下:
状态方程很好列
偷了,那么第i-1家你就不能碰,不偷,就看i-1家的情况
此题的base case:dp[0] = 0;因为有dp[i-2]所以需要衡量第一家偷不偷,那么如果只有一家,当然是偷了,dp[1] = nums[0]
下面是完整版本的代码:
class Solution {
public:
int rob(vector<int>& nums) {
int size = nums.size();
if(size == 0) return 0;
vector<int> dp(size+1,0);
dp[0] = 0;
dp[1] = nums[0];
for(int i = 2;i<=size;++i)
{
dp[i] = max(dp[i-1],dp[i-2]+nums[i-1]);
}
return dp[size];
}
};
213. 打家劫舍 II https://leetcode-cn.com/problems/house-robber-ii/
最初未优化版本:比较两次,重复两次打家劫舍1,这也是本题最稳妥的解法
class Solution {
public:
int rob(vector<int>& nums) {
int size = nums.size();
if(size == 0) return 0;
if(size == 1) return nums[0];
vector<int> dp(size+1,0);
return max(begin(dp,nums[0],nums),begin(dp,0,nums));//两种情况,第一家偷与不偷
}
int begin(vector<int> & dp,int beginval,vector<int> & nums)
{
int size = nums.size();
dp[1] = beginval;
for(int i = 2;i<=size;++i)
{
if(i == size&&dp[1] != 0) dp[i] = dp[i-1];//最后一家只能不偷
else dp[i] = max(dp[i-1],dp[i-2]+nums[i-1]);//正常情况
}
return dp[size];
}
};
https://leetcode-cn.com/problems/house-robber-iii/
本以为是一道BFS的题目,直接开始写:
class Solution {
public:
int rob(TreeNode* root) {
//BFS
if(root == nullptr) return 0;
queue<TreeNode*>Q;
Q.push(root);
int level = 0,Sum1 = 0,Sum2 = 0;
while(Q.size())
{
int size = Q.size();
level++;
for(int i = 0;i<size;++i)
{
TreeNode* temp = Q.front();Q.pop();
if(temp->left) Q.push(temp->left);
if(temp->right) Q.push(temp->right);
if(level%2!=0) {Sum1+=temp->val;cout<<"1"<<endl;}
else {Sum2+=temp->val;cout<<"0"<<endl;}
}
}
return Sum1>Sum2?Sum1:Sum2;
}
};
最后发现还是年轻了
本题的dp设法和股票问题有些相似,股票问题设置买不买,本题设置偷不偷
那么该对该根结点只有两个选择:
- 偷了!那么显然,该结点的孩子结点,不能偷
- 不偷,那么我们就去偷孩子结点
直接看程序:
class Solution {
public:
int rob(TreeNode* root) {
vector<int> Rob(2,0);
Rob = DFS(root);
return max(Rob[0],Rob[1]);
}
vector<int> DFS(TreeNode* root)
{
vector<int> Res(2,0);
if(root == nullptr) return Res;//这就是本题的base case,当没有时,也就没有的偷,返回两种状态都是0
vector<int>L(2,0);
L = DFS(root->left);
vector<int>R(2,0);
R = DFS(root->right);
//选择不偷,那么选出左右孩子结点偷与不偷的最大值即可
Res[0] = max(L[0],L[1]) + max(R[0],R[1]);
//选择偷,那么根结点的值,加上不偷孩子结点的选择
Res[1] = root->val + L[0] + R[0];
return Res;
}
};
卡特兰数
https://leetcode-cn.com/problems/unique-binary-search-trees/
本题是借着树的名义,进行动态规划
class Solution {
public:
int numTrees(int n) {
if(n == 0) return 0;
vector<int> dp(n+1,0);
//长度为n的序列的不同二叉搜索树个数(和序列内容无关,只和长度有关)
dp[0] = 1;
dp[1] = 1;
for(int i = 2;i<=n;++i)//长度为i的序列
{
for(int j = 1;j<=i;++j)//长度为n的序列元素
{
dp[i] += dp[j-1]*dp[i-j];
//j为根时,以j为根,将序列分为两个部分,两部分的所有可能相乘
}
}
return dp[n];
}
};
可以参考一下参考解法中的内容,本题就是一个数学问题,以第j个元素为根,然后左右子树的组合相互乘积得到的结果就是以
第j个元素为根所能组成二叉搜索树的结果。
本题其实也应用到了二叉搜索树的性质,以j为根,j前面的都比j小,都在j的左孩子树,右边都比j大,都是j的右孩子树。
https://leetcode-cn.com/problems/unique-binary-search-trees-ii/
本题难度升级,你现在需要写出全部的可能性
class Solution {
public:
vector<TreeNode*> generateTrees(int n) {
if(n == 0) return {};
return GetRes(1,n);
}
//函数的功能,输入起点和终点,然后返回所有组合的数组
vector<TreeNode*> GetRes(int begin,int end){
//输入起点和终点,表示这段二叉搜索树的开始元素和结束元素
if(begin > end) return {nullptr};//begin == end 相等的情况 要做保留
vector<TreeNode*>Res;//此处要压入结果
for(int i = begin;i<=end;++i){//i表示根结点的元素
vector<TreeNode*>Left = GetRes(begin,i-1);//从根结点开始左右分开
vector<TreeNode*>Right = GetRes(i+1,end);//从跟结点开始分割
//开始排列组合
for(auto L:Left){
for(auto R:Right){
TreeNode* node = new TreeNode(i);//每种组合都必须新创建一个结点
node->left = L;
node->right = R;
Res.push_back(node);
}
}
}
return Res;
}
};
再次非常感谢上述内容提供者无私的分享!