动态规划---路径问题
1.不同路劲
题目链接:62. 不同路径 - 力扣(LeetCode)
题目描述:一个机器人一次只能向右移动一步或者向向下移动一步,计算并返回该机器人从左上角走到右下角有几种不同的路径。
讲解算法原理:动态规划
1.状态表示:经验+题目要求
经验就是以某位置为结尾或者以某位置为起点,什么什么什么
在这道题中,dp[i][j]表示机器人从起点走到(i,j)位置的不同路劲的总数量。
2.状态转移方程
依旧是根据最近的一步来划分问题
如果机器人要从起点走到(i,j)这个位置,由于机器人一次只能向右移动一步或者向下移动一步,所以如果机器人要走到(i,j)这个位置,必须先走向(i,j-1)这个位置或者(i-1,j)这个位置,如下图
此时,就有两种情况
第一种情况:当机器人走到(i-1,j)这个位置时,此时机器人能够走到(i,j)位置的不同路劲的数量就是走到(i-1,j)这个位置的路径不同数量,此时根据状态表示,从起点走到(i-1,j)位置不同路劲的数量就是dp[i-1][j],此时让dp[i][j]+=dp[i-1][j]
第二种情况:当机器人走到(i,j-1)这个位置时,此时机器人能够走到(i,j)位置的不同路劲的数量就是走到(i,j-1)这个位置的路径不同数量,此时根据状态表示,从起点走到(i,j-1)位置不同路劲的数量就是dp[i][j-1],此时让dp[i][j]+=dp[i][j-1]
此时根据上面两种情况就行一个总结,得到dp[i][j]=dp[i][j]+=dp[i-1][j] + dp[i][j]+=dp[i][j-1].
3.初始化
此时由于在求dp[i][j]时,要用到dp[i-1][j]和dp[i][j-1],如果此时计算的是第一列或者第一行的dp[i][j]时。就会越界,如下图
所以要初始化dp表中的第一行或者第一列的数据,根据状态表示,将第一行的数据和第一列的数据都初始化为1就行了,如下图
4.填表顺序
从上往下填写每一行,每一行从左往右
5.返回值
返回dp[m-1][n-1]即可
代码实现
class Solution {public int uniquePaths(int m, int n) {int[][] dp=new int[m][n];//处理边界情况if(m==1 || n==1) return 1;//初始化列for(int j=1;j<n;j++){dp[0][j]=1;}//初始化行for(int i=1;i<m;i++){dp[i][0]=1;}for(int i=1;i<m;i++){for(int j=1;j<n;j++){dp[i][j]=dp[i][j-1]+dp[i-1][j];}}return dp[m-1][n-1];}
}
1.1 初始化的优化
上面那种写法在初始化第一行和第一列数据的时候有点麻烦,此时,我们可以通过多开辟一行和一列的空间,把初始化放在for循环中,此时新创建的dp表如下图
此时需要注意两个问题:
第一个问题:多创建的空间里应该填什么值呢?
首先,多创建里面的空间里填的值要保证后面填dp表时不会出错,多出来的空间填的值在根据状态转移方程dp[i][j]=dp[i][j]+=dp[i-1][j] + dp[i][j]+=dp[i][j-1], 首先要有下面的效果,如下图,还是要保证原来那些位置的值都为1
此时,根据状态转移方程,我们只需将dp[0][1]初始化为1就行了,如下图
注意:此时的返回结果就是dp[m][n]了。
代码实现:
class Solution {public int uniquePaths(int m, int n) {int[][] dp=new int[m+1][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][j-1]+dp[i-1][j];}}return dp[m][n];}
}
2.不同路劲 II
题目链接: 63. 不同路径 II - 力扣(LeetCode)
题目描述:在矩阵中,0代表该位置不是障碍物,1代表该位置是障碍物,计算出机器人从左上角走到右下角的路劲有多少种,且路劲中不能有障碍物。
算法原理:
1.状态表示
由于该题与上一道题非常相似,所以根据经验和题目要求,dp[i][j]表示以(i,j)位置为终点,从起点到(i,j)位置的不同路劲的数量且路径中不能包含障碍物。
2.状态转移方程
此时因为矩阵中有障碍物,所以此时,有两种情况来划分dp[i][j]的计算。
第一种情况:当(i,j)位置是障碍物时,此时就表示机器人无法从起点做到该位置,所以根据状态表示,此时dp[i][j]==0
第二种情况:当(i,j)位置不是障碍物时,此时机器人就可以从起点走到(i,j)这个位置,则根据上道题的分析,dp[i][j]=dp[i][j-1]+dp[i-1][j].
3.初始化
由于多创建出一行和一列来完成初始化,此时就需要注意两个问题
第一个问题:保证后面填表的数据正确
第二个问题:注意下标的映射关系
由于要判断该位置是不是障碍物,所以此时还要在填dp表时,还要判断在原矩阵中的该位置是不是障碍物,由于dp多创建了一行或者一列,此时dp表中的(i-1,j-1)才是原矩阵中的(i,j)位置
4.填表顺序
从上往下按行填表,每一行按从左往有填dp表
5.返回值
返回dp[h][l]即可
代码实现
class Solution {public int uniquePathsWithObstacles(int[][] obstacleGrid) {int h=obstacleGrid.length;//行int l=obstacleGrid[0].length;//列int[][] dp=new int[h+1][l+1];dp[0][1]=1;for(int i=1;i<=h;i++){for(int j=1;j<=l;j++){//判断该位置在原矩阵中是否是障碍物if(obstacleGrid[i-1][j-1]==1) dp[i][j]=0;else dp[i][j]=dp[i][j-1]+dp[i-1][j];}}return dp[h][l];}
}
3.珠宝的最高价值
题目链接:LCR 166. 珠宝的最高价值 - 力扣(LeetCode)
题目描述:frame中的每一个值都是一个珠宝的价值,计算并返回从左上角开始拿珠宝一直到右下角的珠宝的最大总价值。
算法原理:
1.状态表示:经验+题目要求
经验就是以i位置为起点或者以i位置为结尾,什么什么什么
在这道题中,dp[i][j]就表示从起点到(i,j)这个位置的的路径上所有珠宝的最大终价值之和。
2.状态转移方程
推算状态转移方程依旧是根据最近一步去划分和分析问题
因为一次移动只能向右或者向下移动一步,此时到达(i,j)位置时有两种情况
第一种情况:从(i-1,j)到达(i,j)
此时要求出从起点到(i,j)路径上的所有珠宝的最大总价值的话,此时必须要求出从起点到(i-1,j)路劲上所有珠宝的最大总价值在加上(i,j)位置上的珠宝价值,根据状态表示,从起点到(i-1,j)路劲上所有珠宝的最大总价值可以表示为dp[i-1][j],所以此时状态转移方程为
dp[i][j]=dp[i-1][j]+frame[i][j]
第二种情况:从(i,j-1)位置到达(i,j)位置
此时要求出从起点到(i,j)路径上的所有珠宝的最大总价值的话,此时必须要求出从起点到(i,j-1)路劲上所有珠宝的最大总价值在加上(i,j)位置上的珠宝价值,根据状态表示,从起点到(i,j-1)路劲上所有珠宝的最大总价值可以表示为dp[i][j-1],所以此时状态转移方程为
dp[i][j]=dp[i][j-1]+frame[i][j]
根据两种情况总结:
因为要求出最大总价值,所以最终的状态转移方程为:
dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j])+frame[i-1][j-1];
3.初始化
此时,为了方便初始化,还是要在创建dp表时多创建一行或者一列,为防止填dp表时,填有绿色星星的位置越界,如下图
此时,有两个注意事项:
第一个注意事项:多创建的空间里面要填什么值呢?填的值要保证后面填dp时不会出错。
此时,要考虑dp[1][1]要填什么内容,根据状态表示dp[1][1]的值就要填原始矩阵的值,此时根据状态转移方程,此时只需将dp[0][1]或者dp[1][0]初始化为0就行了。
填dp[1][2]时呢?
填dp[1][2]时要用到dp[1][1]和dp[0][2],而dp[1][2]填的值应该是dp[1][1],我们要去最大值的时候要保证选到的是dp[1][1]而不是dp[0][2],此时我们应该将dp[0][2]初始化为Integer.MIN_VALUE,但是根据题目的要求,数组中所有的值都是大于0的,所以此时将dp[0][2]初始化为0即可。
第二个注意事项:注意下标的映射关系
由于创建dp表时多创建了一行和一列,所以dp表中的(i-1,j-1)才是对应到原始矩阵中的(i,j)位置
4.填表顺序
由于求dp[i][j]时要用到dp[i-1][j]和dp[i][j-1],所以此时的填表顺序为先从上往下填每一行,填每一行时从左往右填
5.返回值
返回dp[h][l]即可
代码实现
class Solution {public int jewelleryValue(int[][] frame) {int h=frame.length;//行int l=frame[0].length;//列int[][] dp=new int[h+1][l+1];//dp[0][1]=frame[0][0];for(int i=1;i<=h;i++){for(int j=1;j<=l;j++){dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j])+frame[i-1][j-1];}}return dp[h][l];}
}
4.下降路最小和
题目链接:931. 下降路径最小和 - 力扣(LeetCode)
题目描述:给定一个 n x n
的方形整数数组 matrix
,要求找出并返回通过该 matrix
的下降路径的最小和。其中下降路径的起始位置可以是第一行的任意元素,且在每一行选择元素时,下一行所选元素需与当前行所选元素最多相隔一列(即位于正下方、左下方或右下方)
算法原理:
1.状态表示:经验+题目要求
在这道题中,dp[i][j]表示到达(i,j)位置时,最小的下降路径。
2.状态转移方程
当我们选择在下一行选择下一个数据时,下一个数据的在下一行的位置是有要求的,下一个选择的数据的位置必须位于上一行选择的数字的左下方,正下方,右下方,例如上一行选择的数字的位置是(i,j),那么下一行的选择的数据位置需要在(i+1,j-1)或者(i+1,j)或者(i+1,j+1)这三个位置。
所以,此时我们在推算dp[i][j]时时,也就是如果我们要求出到达(i,j)位置时的最小路径时 ,会有3中情况。
第一种情况:从(i-1,j-1)到(i,j)
此时我们首先知道到达(i-1,j-1)的最小路径,根据状态表示, dp[i-1][j-1]就是到达(i-1,j-1)的最小路径,此时dp[i][j]=dp[i-1][j-1]+m[i][j]
第二种情况:从(i-1,j)到(i,j)
此时我们首先知道到达(i-1,j)的最小路径,根据状态表示, dp[i-1][j]就是到达(i-1,j)的最小路径,此时dp[i][j]=dp[i-1][j]+m[i][j]
第三种情况:从(i-1,j+1)到(i,j)
此时我们首先知道到达(i-1,j+1)的最小路径,根据状态表示, dp[i-1][j+1]就是到达(i-1,j+1)的最小路径,此时dp[i][j]=dp[i-1][j+1]+m[i][j]
由于我们求的是最小路径,状态转移方程最终版
dp[i][j]=min(dp[i-1][j-1],dp[i-1][j],dp[i-1][j+1]) +m[i][j]
3.初始化
由于在求dp[i][j]时会用到(i-1,j-1),(i-1,j),(i-1,j+1)这三个位置的值,此时初始化时,下图中用五角星标志的位置就会越界,所以此时在创建dp表时我们要多创建一行和多创建两列。
此时,会有两个注意事项
第一个注意事项:多创建的空间里面填的值要保证后面填表正确
看下图分析
第一种情况:
此时根据状态表示,图中dp表那个位置的值要填原本矩阵中该位置的值,由于在求dp[i][j]时,会用到三个位置的数据,如下图
所以我们要保证多出来的这三个位置填的值不能影响dp[i][j]的值,所以这3个位置的值要初始化为0,以此类推,这一行的位置都要初始化为0
第二种情况:
分析如下图,按照数字的顺序观看
此时只要将位置的值初始化为Integer.MAX_VALUE就行了,最终的初始化如下图
4.填表顺序
从上往下一次填每一行,填每一行时的顺序从左往右或者从右往左都行
5.返回值
返回dp表中最后一行中的最小值
代码实现:
class Solution {public int minFallingPathSum(int[][] matrix) {int n=matrix.length;int[][] dp = new int[n+1][n+2];//行for(int i=0;i<n+2;i++){dp[0][i]=0;}//列for(int i=1;i<=n;i++){dp[i][0]=Integer.MAX_VALUE;dp[i][n+1]=Integer.MAX_VALUE;}for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){dp[i][j]=Math.min(dp[i-1][j-1],Math.min(dp[i-1][j],dp[i-1][j+1]))+matrix[i-1][j-1];}}int minNum=Integer.MAX_VALUE;for(int i=1;i<=n+1;i++){if(dp[n][i]<minNum){minNum=dp[n][i];}}return minNum;}
}
5.最小路径和
题目链接:64. 最小路径和 - 力扣(LeetCode)
题目描述:计算并返回从左上角到右下角的最小路径之和
算法原理:
1.状态表示:经验+题目要求
在路径问题中,经验就是以i位置为起点或者以i位置为结尾,什么什么什么
该题中,dp[i][j]可以表示到达(i,j)位置时的最小路径
2.状态转移方程
在推算状态转移方程时,还是根据最近一步来分析问题
因为一次只能向右移动一步或者向下移动一步,当我们要求到达(i,j)位置时的最小路径时,就必须要知道到达(i-1,j)位置时的最小路径或者到达(i,j-1)位置时的最小路径,也就是有两种情况进行分析
第一种情况:从(i-1,j)位置到(i,j)位置
此时要求出dp[i][j],也就是要求到达(i,j)位置时的最小路径时,首先要知道到达(i-1,j)时的最小路径,然后再加上原本矩阵上(i,j)位置上的值就是dp[i][j]了,此时得出状态转移方程
dp[i][j]=dp[i-1][j]+grid[i][j]
第二种情况:从(i,j-1)位置到(i,j)位置
此时要求出dp[i][j],也就是要求到达(i,j)位置时的最小路径时,首先要知道到达(i,j-1)时的最小路径,然后再加上原本矩阵上(i,j)位置上的值就是dp[i][j]了,此时得出状态转移方程
dp[i][j]=dp[i][j-1]+grid[i][j]
总结状态转移方程:由于我们要求的是最小路径,所以,状态转移方程为:
dp[i][j]=Math.min(dp[i][j-1],dp[i-1][j])+grid[i-1][j-1]
3.初始化
由于在求dp[i][j]时用到了dp[i-1][j]和dp[i][j-1],此时填表时,填下面用五角星标志的那些位置的值会越界,所以我们要对这些位置的值进行初始化,如下图
初始化时,我们会用到一个技巧,就是在创建dp表时会多创建爱一行和一列的空间,此时会有两个注意事项
第一个注意事项: 多创建的空间里面填的值要保证后面填表正确
根据状态表示,这个位置初始化的值应该是原本矩阵中该位置的值,所以此时(i,j-1)和(i-1,j)这两个位置的值不能影响这种情况,此时只需将(i,j-1)或者(i-1,j)其中一个位置的值填为0就行了
当我们要填下图的那个位置的值时,也会设计到两个位置的值
所以在比较选值的时候,必须要保证dp[i][j-1]被选到,由于是求最小路径,此时将dp[i-1][j]初始化为Integer.MAX_VALUE就行了,最终的多出来的空间填的值如下图
第二个注意事项:注意下标的映射关系,因为我们多创建了一行和一列,所以在dp表中的(i-1,j-1)的位置才是原来矩阵中的(i,j)位置
4.填表顺序
首先从上往下填每一行,在填每一行时从左往右填dp表
5.返回值
返回dp[h][l]即可
代码实现:
class Solution {public int minPathSum(int[][] grid) {int h=grid.length;int l=grid[0].length;int[][] dp=new int[h+1][l+1];for(int i=2;i<=l;i++){dp[0][i]=Integer.MAX_VALUE;}for(int i=1;i<=h;i++){dp[i][0]=Integer.MAX_VALUE;}for(int i=1;i<=h;i++)for(int j=1;j<=l;j++)dp[i][j]=Math.min(dp[i][j-1],dp[i-1][j])+grid[i-1][j-1];return dp[h][l];}
}
6.地下城游戏
题目链接:174. 地下城游戏 - 力扣(LeetCode)
题目描述:公主被恶魔关在的矩阵的右下角,骑士要从左上角出发,去矩阵的右下角拯救公主,骑士一次只能往右或者往下移动一步,且成功到达右下角时,骑士不能死亡,返回确保骑士能够拯救到公主所需的最低初始健康点。
算法原理:
1.状态表示:经验+题目要求
经验就是以某某位置为起点或者某某位置为结尾,什么什么什么
在这一道题中,不能以某某位置为结尾,如果这道题是以某某位置为结尾,此时的状态表示就是:dp[i][j]表示:从起点出发,到达(i,j)位置时,所需的最低初始健康点数
此时因为骑士一次只能向下或者向上走一步,所以此时如果求dp[i][j]时按我们惯性的思维,会用到dp[i-1][j]和dp[i-1][j],但是此时不能只考虑dp[i-1][j]和dp[i-1][j],因为此时我们是要成功走到右下角的且还要确保走到右下角时不会死亡,所以此时,当我们在计算dp[i][j]时,还要考虑从(i,j)位置走到下一个位置时,骑士不会死亡,所以此时计算dp[i][j]时还要考虑下一个位置的等等位置
但是此道题中,可以以某某位置为起点,此时状态表示为
dp[i][j]表示:从(i,j)位置出发,达到终点时所需的最低健康点数
2.推算状态转移方程
由于骑士一次只能向下移动一步或者向右移动一步,由于此时是从(i,j)位置为起点出发,此时要到达终点,就必须先到达(i,j+1)位置(向右走一步)或者(i+1,j)位置(向下走一步)
第一种情况:先从(i,j)位置向右走一步到达(i,j+1)位置,然后再从(i,j+1)位置出发,经过重复操作走到终点
此时假设dp[i][j]为x,也就是走到(i,j)位置时的初始健康点数为x,此时骑士继续向右移动一步,接着骑士就会到达(i,j+1)这个位置,此时到达(i,j+1)这个位置的初始健康点数为x+bungeon[i][j],但是此时我们也要保证骑士在走过(i,j+1)这个位置之后,骑士不会死亡,所以此时x+bungeon[i][j]>=dp[i][j+1](dp[i][j+1]就是到到达(i,j+1)位置时所需的最低健康点数) ,所以此时x>=dp[i][j+1]-bungeon[i][j],由于我们求的是最低健康点数,所以x=dp[i][j+1]-bungeon[i][j],x是上面dp[i][j]假设的,所以此时状态转移方程为:
dp[i][j]=dp[i][j+1]-bungeon[i][j]
第二种情况:先从(i,j)位置向右走一步到达(i+1,j)位置,然后再从(i+1,j)位置出发,经过重复操作走到终点
此时假设dp[i][j]为x,也就是走到(i,j)位置时的初始健康点数为x,此时骑士继续向右移动一步,接着骑士就会到达(i+1,j)这个位置,此时到达(i+1,j)这个位置的初始健康点数为x+bungeon[i][j],但是此时我们也要保证骑士在走过(i+1,j)这个位置之后,骑士不会死亡,所以此时x+bungeon[i][j]>=dp[i]+1[j](dp[i+1][j]就是到到达(i+1,j)位置时所需的最低健康点数) ,所以此时x>=dp[i+1][j]-bungeon[i][j],由于我们求的是最低健康点数,所以x=dp[i+1][j]-bungeon[i][j],x是上面dp[i][j]假设的,所以此时状态转移方程为:
dp[i][j]=dp[i+1][j]-bungeon[i][j]
3.初始化
由于求dp[i][j]时是用到(i,j)位置的右边位置(i,j+1)的dp[i][j+1]和右边位置(i,j+1)的dp[i][j+1],所以此时在创建dp表时是以最后一行和最后一列为多出来的创建空间,此时的初始化如下图
由于骑士到达终点时不能死亡,由于是要求最小健康点数,所以dp[h][l]要初始化为1
因为此时增加的空间是最后一行或者最后一列,所以此时不用注意下标的映射关系
4.填表顺序
此时的填表顺序应该是从右往左填每一行,在填每一行时从下往上填dp表
5.返回值
直接返回dp[0][0]即可
代码实现:
class Solution {public int calculateMinimumHP(int[][] dungeon) {int h=dungeon.length;int l=dungeon[0].length;int[][] dp=new int[h+1][l+1];//初始化最后一列for(int i=0;i<=h;i++) dp[i][l]=Integer.MAX_VALUE;//初始化最后一行for(int i=0;i<=l;i++) dp[h][i]=Integer.MAX_VALUE;dp[h-1][l]=1;dp[h][l-1]=1;for(int i=h-1;i>=0;i--){for(int j=l-1;j>=0;j--){dp[i][j]=Math.min(dp[i][j+1],dp[i+1][j])-dungeon[i][j];dp[i][j]=Math.max(1,dp[i][j]);}}return dp[0][0];}
}