前提
本专题开始,注重整理与动态规划相关的题目,从简单的动归开始切入,慢慢掌握和练习动态规划这一重要的算法思想,部分相对复杂的题目会结合画图和代码分析去解释
一、第N个泰波纳契数列
1.链接
1137. 第 N 个泰波那契数 - 力扣(LeetCode)
2.描述
3.思路
关于动态规划的题目,往后都是按照五步走的统一思路去进行分析
(1)状态表示
状态表示我们通常是根据经验加对具体题目的判断去设置的,通常的设置思路有:
以第i个位置为底,结合题目条件...
以第i个位置开始,结合题目条件...
...
本题状态表示:f(i)表示以i位置为底,泰波那契数列的值
(2)状态转移方程
状态方程的判断一般需要我们根据具体的题目条件去进行推导,这题目的方程式就是状态转移方程
f(i) = f(i-1)+ f(i-2) + f(i-3)
(3)初始化
初始化是为了确定边界条件,确定起始位置
f(0)= 0,f(1)= 1,f(2) = 1;
(4)填表顺序
写代码时,我们需要一个dp表,我们需要根据状态方程去判断填表顺序是从如何推导,本题的推导顺序很明显是从头往后推导记录
(5)返回值
确定返回的值,这个返回值根据题目不同,可能返回的不同,本题是返回dp[ n ]
4.参考代码
class Solution {
public:int tribonacci(int n) {if(n == 0) return 0;if(n == 1 || n == 2) return 1;//考虑边界情况vector<int> dp(n+1);//创建dp表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];}
};
二、三步走问题
1.链接
面试题 08.01. 三步问题 - 力扣(LeetCode)
2.描述
3.思路
(1)状态表示
以第n个台阶为底(一共有n个台阶),小孩一共有多少种走法
(2)状态转移方程
dp[ n ] = dp[n - 1] + dp[n - 2] + dp[n - 3]
(3)初始化
dp[1] = 1; dp[2] = 2; dp[3] = 4
(4)填表顺序
从左到右
(5)返回值
dp[ n ]
4.参考代码
class Solution {
public:const int num = 1e9+7;int waysToStep(int n) {if(n == 1 || n == 2) return n;if(n == 3) return 4;vector<int> dp(n+1);dp[1] = 1; dp[2] = 2; dp[3] = 4;for(int i = 4;i <= n;i++){dp[i] = ((dp[i-1] + dp[i-2])%num + dp[i-3])%num;}return dp[n];}
};
三、使用最小花费爬楼梯
1.链接
746. 使用最小花费爬楼梯 - 力扣(LeetCode)
2.描述
3.思路
(1)状态表示
思路一:以i为结束,...
f(i)表示爬到第i层,花费最少的费用
思路二:以i为开始,...
f(i)表示从第i层开始,到楼顶需要花费最少的费用
(2)状态表示方程
思路一: f(i)= min ( f(i -1)+cost[i-1] ,f(i-2)+cost[i-2] )
思路二: f(i)= min( f(i+1) , f(i+2) ) + cost [ i ]
(3)初始化
思路一:f(0)= 0 ; f( 1 ) = 0
思路二:f(n)= cost[ n ] ; f(n-1)= cost[n-1]
(4)填表顺序
思路一:从左到右
思路二:从右到左
(5)返回值
思路一:dp[n]
思路二:dp[0]
4.参考代码
思路一:
class Solution {
public:int minCostClimbingStairs(vector<int>& cost) {vector<int> dp(cost.size()+1);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 dp[cost.size()];}
};
思路二:
class Solution {
public:int minCostClimbingStairs(vector<int>& cost) {int n = cost.size()-1;vector<int> dp(n+1);dp[n] = cost[n];dp[n-1] = cost[n-1];for(int i = n-2;i>=0;i--){dp[i] = min(dp[i+1],dp[i+2])+cost[i];}return min(dp[0],dp[1]);}
};
四、解码方法
1.链接
. - 力扣(LeetCode)
2.描述
3.思路
4.参考代码
class Solution
{
public:int numDecodings(string s) {int n = s.size();//由于dp[0]要作为辅助位置,因此映射关系整体向后移//也可以认为此刻dp[i]对应的就是从1开始往后数的第i个字母vector<int> dp(n+1,0);dp[0] = 1;dp[1] = s[0] != '0';for(int i = 2;i<=n;i++){if(s[i-1]!='0')//i位置的数字单独解码成功dp[i]+=dp[i-1];int second = (s[i-2]-'0')*10+(s[i-1]-'0');if(second>=10 && second<=26)//第二种情况解码成功dp[i]+=dp[i-2];}return dp[n];}
};
五、不同路径
1.链接
62. 不同路径 - 力扣(LeetCode)
2.描述
3.思路
(1)状态表示
从左上角出发,走到[ i , j ]位置时一共有dp(i,j)种办法
(以[i,j]为结束,...)
(2)状态表示方程
(3)初始化
初始化需要考虑边界问题,通常我们会选择给路径多加上一层边框,然后对边框进行合理的赋值
(4)填表顺序
从左到右,从上到下,就是遍历二维数组的顺序
(5)返回值
dp[m,n]
4.参考代码
class Solution {
public:int uniquePaths(int m, int n) {vector<vector<int>> dp(m+1,vector(n+1,0));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];}
};
六、不同路径||
1.链接
63. 不同路径 II - 力扣(LeetCode)
2.描述
3.思路
(1)状态表示
dp[i,j] : 以[i,j]位置结束,一共有dp[i,j]种路线
(2)状态表示方程
(3)初始化
和上题的初始化一样,不过这里就需要注意映射关系了,在题目给的障碍表格中要注意映射关系
(4)填表顺序
从左上角到右下角
(5)返回值
dp[m,n]
4.参考代码
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(n+1,0));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];}
};
七、珠宝的最高价值
1.链接
LCR 166. 珠宝的最高价值 - 力扣(LeetCode)
2.描述
3.思路
(1)状态表示
dp[i,j] 表示 以[i,j]位置结束,所获得的最大珠宝价值
(2)状态表示方程
有了上面两题的经验,不难分析出,[i,j]位置获得的最大珠宝价值,会等于当前位置的珠宝价值加上max(dp[i-1,j],dp[i,j-1])
dp[i,j] = max(dp[i-1,j],dp[i,j-1])+frame[ i ][ j ] (实际代码中要注意具体映射位置)
(3)初始化
和前两题一样的初始化方式,但是本题中不需要对第一个位置进行赋值为1,因为此时dp记录的是珠宝的价值,因此起始是不需要赋值的,全部初始化为0即可
(4)填表顺序
从左上角到右下角
(5)返回值
return dp[m][n]
4.参考代码
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(n+1,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];}
};
八、下降路径最小和
1.链接
931. 下降路径最小和 - 力扣(LeetCode)
2.描述
3.思路
(1)状态表示
这次我们换一个思路,“以i位置开始,...”
以当前位置[ i ][ j ] 开始,走到最底下的最小路径和为dp[ i ][ j ]
(2)状态表示方程
(3)初始化
我们通过题目可以看到,每次dp遍历选择时,最左边那一列和最右边那一列存在越界的问题,因此我们可以给左右两边各加上一列去解决边界越界的问题,同时由于题目计算的是最小路径和,辅助列相当于非法路径,我们可以给其赋值为最大值,即可保证dp不会选择非法路径,再往最后一行添加一行辅助行,实现最后一列dp的赋值
(4)填表顺序
从最后一行往上填表,从下到上,从左往右
(5)返回值
取第一行中最小值返回
4.参考代码
这种思路到这来在映射关系上会比较复杂,需要画图,这题是为了练习另一种思路,推荐使用:
以[i][j]位置结束,所需的最小路径和为dp[i][j]的思路会更好写代码
class Solution {
public:int minFallingPathSum(vector<vector<int>>& matrix) {int m = matrix.size();int n = matrix[0].size();vector<vector<int>> dp(m+1,vector<int>(n+2,INT_MAX));//初始化for(int i = 0;i<=n+1;i++){dp[m][i] = 0;}for(int i = m-1;i>=0;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][j-1];}}int ret = dp[0][1];for(int i = 2;i<=n;i++){ret = min(ret,dp[0][i]);}return ret;}
};
九、最小路径和
1.链接
64. 最小路径和 - 力扣(LeetCode)
2.描述
3.思路
(1)状态表示
以[i][j]位置为结束,最小的路径和为dp[i][j]
(2)状态表示方程
dp[i][j] = min(dp[i-1][j],dp[i][j-1]) + grid[i][j]
(3)初始化
多加一行和一列的辅助行和辅助列,初始化为最大值,然后让dp[0][1] = 0,确保第一个左上角位置的路径值是正确的,最大值则是保证其他路径不会选择非法路径
(4)填表顺序
从左上角到右下角
(5)返回值
dp[m][n]
4.参考代码
class Solution {
public:int minPathSum(vector<vector<int>>& grid) {int m = grid.size();int 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];}
};
十、地下城游戏
1.链接
174. 地下城游戏 - 力扣(LeetCode)
2.描述
3.思路
(1)状态表示
根据题意,这里若是使用以[i][j]位置结束的思路,则不能保证后续路程可以走完,要采用另一种思路
dp[i][j] : 以[ i ][ j ]位置开始,走到最后所需要消耗的最小健康值
(2)状态表示方程
dp[i][j] = min( dp[i+1][j],dp[i][j+1] ) - dungeon[ i ][ j ]
我们可以认为,某个位置的点数如果是正数则就是回血,可以减少消耗,若是负数则加上增加了消耗,并且,还要考虑到消耗不能为负数,负数意味着有一个大血包,但勇士不能从0血以下开始走,当遇到大血包时,我们认为该点走到结尾的消耗为0
(3)初始化
我们需要辅助层去保证不越界的问题,dp起始位置是在末尾(右下角),为了不越界,我们需要给辅助层初始化为最大值,而为了让第一个dp填对,我们要让其相邻的其中一个为0(dp[i+1][j] =0)
(4)填表顺序
从右下角开始,从右往左,从下往上
(5)返回值
return dp[0][0]+1;
4.参考代码
class Solution {
public:int calculateMinimumHP(vector<vector<int>>& dungeon) {int m = dungeon.size();int n = dungeon[0].size();vector<vector<int>> dp(m+1,vector<int>(n+1,INT_MAX));dp[m][n-1] = 0;for(int i = m-1;i>=0;--i){for(int j = n-1;j>=0;--j){dp[i][j] = min(dp[i+1][j],dp[i][j+1]) - dungeon[i][j];dp[i][j] = max(0,dp[i][j]);}}return dp[0][0] + 1;}
};
总结
本篇总结了关于动态规划的一些题目,斐波那契数列类型的和路径问题相关的动态规划经典题目