文章目录:
今天我们开始动态规划例题的第二个模型——路径问题
叠甲叠甲叠甲:今天的题目全部取自力扣!!!
在开始之前,我们重新回忆一下动态规划的基本步骤
1. 动态规划基本步骤
1. 状态表示:
a. 创建一个dp表(通常是一个数组)
b. 填满dp表,dp表的每一个值就是一个状态
c. 定义状态表示的方法:
i. 题目要求
ii. 经验 + 题目要求:
1)以某一个位置作为起始
2)以某一个位置作为结束
iii. 分析问题的过程中,发现了重复子问题
2. 状态转移方程:根据状态表示得出状态转移方程
用之前或之后的状态推导出当前状态的值
通常:根据最近的一步来划分问题
3. 初始化:保证填表的时候不发生越界
只需要初始化可能发生越界的状态
4. 填表顺序:根据状态转移方程和边界条件,确定子问题的求解顺序
通常是从下到上、从左到右(或称为从小到大)地计算状态值。
5. 返回值
2. 路径问题模型例题
2.1:不同路径
2.1.1:算法原理
1. 状态表示:
a. 创建一个dp表(本题是一个二维数组)
i. 经验 + 题目要求:
1)以某一个位置作为结束
dp[i][j]表示走到(i,j)位置的时候一共有多少方式
2. 状态转移方程:
dp[i][j]:
从上面来:(i-1,j)——> (i,j) :dp[i-1][j]
从左面来:(i,j-1)——> (i,j) :dp[i][j-1]
状态转移方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]
3. 初始化:
本题:填写一个状态时,会用到上面和左面的节点,最上一行和最左一列会发生越界
使用虚拟节点法:
虚拟节点的注意事项:
a. 虚拟节点的值要保证后续填表的结果是正确的
本题需要保证dp[1][1]=1,而dp[1][1] = dp[0][1] + dp[1][0]
需要将dp[0[1] or dp[1][0]初始化为1
b. 注意下标的映射:
上文的dp[1][1]其实就是原始数组的(0,0)位置
4. 填表顺序:
从上到下、从左到右
5. 返回值:dp[m][n]
2.1.2:代码实现
//不同路径问题
public int uniquePaths(int m, int n) {
// 创建dp表
//初始化
//填表
//返回值
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-1][j]+dp[i][j-1];
}
}
return dp[m][n];
//时间/空间复杂度 O(m*n)
}
2.2:不同路径 2
2.2.1:算法原理
1. 状态表示:
a. 创建一个dp表(本题是一个二维数组)
i. 经验 + 题目要求:
1)以某一个位置作为结束
dp[i][j]表示走到(i,j)位置的时候一共有多少方式
2. 状态转移方程:
dp[i][j]:
有障碍:0
无障碍:
从上面来:(i-1,j)——> (i,j) :dp[i-1][j]
从左面来:(i,j-1)——> (i,j) :dp[i][j-1]
dp[i][j] = dp[i-1][j] + dp[i][j-1]
3. 初始化:
使用虚拟节点法:
本题同上题,需要保证dp[1][1]=1,而dp[1][1] = dp[0][1] + dp[1][0]
所以需要将dp[0[1] or dp[1][0]初始化为1
4. 填表顺序:
从上到下、从左到右
5. 返回值:dp[m][n]
2.2.2:代码实现
//不同路径 2
public int uniquePathsWithObstacles(int[][] ob) {
// 创建dp表
//初始化
//填表
//返回值
int m = ob.length;
int n = ob[0].length;
int[][] dp=new int[m+1][n+1];
dp[1][0]=1;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(ob[i-1][j-1]==0){//注意下标的映射关系
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
return dp[m][n];
}
2.3:礼物的最大价值
2.3.1:算法原理
1. 状态表示:
a. 创建一个dp表(本题是一个二维数组)
i. 经验 + 题目要求:
1)以某一个位置作为结束
dp[i][j]表示走到(i,j)位置时的最大价值
2. 状态转移方程:
dp[i][j]:
从上面来:(i-1,j)——> (i,j) :dp[i-1][j] + (i,j)位置的价值
从左面来:(i,j-1)——> (i,j) :dp[i][j-1] + (i,j)位置的价值
dp[i][j] = max(dp[i-1][j] , dp[i][j-1]) + (i,j)位置的价值
3. 初始化:
使用虚拟节点法:
因为每走到一个位置都会加上当前位置的价值
所以辅助节点全部初始化为0即可(就是默认状态,不需要做处理)
4. 填表顺序:
从上到下、从左到右
5. 返回值:dp[m][n]
2.3.2:代码实现
//礼物的最大价值
public int jewelleryValue(int[][] frame) {
// 创建dp表
//初始化
//填表
//返回值
int m = frame.length;
int n = frame[0].length;
int[][] dp=new int[m+1][n+1];
dp[1][0]=1;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1])+frame[i-1][j-1];
}
}
return dp[m][n];
}
2.4:下降路径最小和
2.4.1:算法原理
1. 状态表示:
a. 创建一个dp表(本题是一个二维数组)
i. 经验 + 题目要求:
1)以某一个位置作为结束
dp[i][j]表示走到(i,j)位置时的最小下降路径
2. 状态转移方程:
dp[i][j]:
从左上面来:(i-1,j-1)——> (i,j) :dp[i-1][j-1] + (i,j)位置的值
从正上面来:(i-1,j)——> (i,j) :dp[i-1][j] + (i,j)位置的值
从右上面来:(i-1,j+1)——> (i,j) :dp[i-1][j+1] + (i,j)位置的值
dp[i][j] = min(dp[i-1][j-1] , dp[i-1][j], dp[i-1][j+1]) + (i,j)位置的值
3. 初始化:
填表时会用到当前节点的左上、正上、右上的状态
所以:最上一行、最左一列、最右一列会发生越界
使用虚拟节点法:
只需要将所有默认节点初始化为+∞,再将最上一行初始化为0
第一行的值要和matrix数组一致,不能受虚拟节点影响,所以最上一行初始化为0
因为要取最小值,不能让左列和右列的值产生影响,所以将左列和右列初始化为+∞
4. 填表顺序:
从上到下
5. 返回值:最后一行的最小值
2.4.2:代码实现
//下降路径最小和
public int minFallingPathSum(int[][] matrix) {
// 创建dp表
//初始化
//填表
//返回值
int n = matrix.length;
int[][] dp = new int[n + 1][n + 2];
for (int i = 1; i <= n; i++) {
dp[i][0] = 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], Math.min(dp[i - 1][j - 1], dp[i - 1][j + 1])) + matrix[i-1][j-1];
}
}
//找出最后一行的最小值
int ret = Integer.MAX_VALUE;
//求一系列数的最小值时,要把ret设置得尽可能大,使其不影响最终结果
//如果使ret=0;而其他数都比0大,则影响了最终结果
for (int j = 1; j <= n; j++) {
ret = Math.min(ret, dp[n][j]);
}
return ret;
//时间/空间复杂度:O(n^2)
}
2.5:最小路径和
2.5.1:算法原理
1. 状态表示:
a. 创建一个dp表(本题是一个二维数组)
i. 经验 + 题目要求:
1)以某一个位置作为结束
dp[i][j]表示走到(i,j)位置时的最小路径和
2. 状态转移方程:
dp[i][j]:
从上面来:(i-1,j)——> (i,j) :dp[i-1][j] + (i,j)位置的值
从左面来:(i,j-1)——> (i,j) :dp[i][j-1] + (i,j)位置的值
dp[i][j] = min(dp[i-1][j] , dp[i][j-1]) + (i,j)位置的值
3. 初始化:
最上一行、最左一列会发生越界
使用虚拟节点法:
dp[0][1]和dp[1][0]初始化为0,其他虚拟节点初始化为+∞
4. 填表顺序:
从上到下、从左到右
5. 返回值:dp[m][n]
2.5.2:代码实现
//最小路径和
public int minPathSum(int[][] grid) {
// 创建dp表
//初始化
//填表
//返回值
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m+1][n+1];
for(int i=0;i<=m;i++){
dp[i][0]=Integer.MAX_VALUE;
}
for(int j=0;j<=m;j++){
dp[0][j]=Integer.MAX_VALUE;
}
dp[0][1]=dp[1][0]=0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1])+grid[i][j];
}
}
return dp[m][n];
}
2.6:地下城游戏
2.6.1:算法原理
1. 状态表示:
a. 创建一个dp表(本题是一个二维数组)
i. 经验 + 题目要求:
1)以某一个位置作为结束
dp[i][j]表示走到(i,j)位置时所需的最小初始健康值
当前状态受到之后状态的影响,无法确定当前状态
(涉及无后效性问题)所以该方案行不通
2)以某一个位置作为开始
dp[i][j]表示从(i,j)位置出发,到达终点所需的最低初始健康值
2. 状态转移方程:
dp[i][j]:
往右走:(i,j)——> (i,j+1) :dp[i][j+1] - dp[i][j]
往下走:(i,j)——> (i+1,j) :dp[i+1][j] - dp[i][j]
dp[i][j] = min(dp[i][j+1] - dp[i][j] , dp[i+1][j] - dp[i][j])
dp[i][j] = max( dp[i][j],1) 保证血量不会低于1
3. 初始化:
最下一行、最右一列会发生越界
使用虚拟节点法:
dp[m][n-1]和dp[m-1][n]初始化为1,其他虚拟节点初始化为+∞
4. 填表顺序:
从下到上、从右到左
5. 返回值:dp[0][0]
2.6.2:代码实现
//地下城游戏
public int calculateMinimumHP(int[][] dungeon) {
// 创建dp表
//初始化
//填表
//返回值
int m = dungeon.length;
int n = dungeon[0].length;
int[][] dp = new int[m+1][n+1];
//初始化
for(int i=0;i<=m;i++){
dp[i][n]=Integer.MAX_VALUE;
}
for(int j=0;j<=n;j++){
dp[m][j]=Integer.MAX_VALUE;
}
dp[m][n-1]=dp[m-1][n]=1;
//填表
for (int i = m-1; i >= 0; i--) {
for (int j = n-1; j >= 0; j--) {
dp[i][j] = Math.min(dp[i][j+1], dp[i+1][j]) - dungeon[i][j];
//保证最低健康值不会小于1
dp[i][j] = Math.max(1,dp[i][j]);
}
}
return dp[0][0];
}