目录
- 一、63. 不同路径 II
- 1.1 题目解析
- 1.2 状态转移方程
- 1.3 解题代码
- 二、931. 下降路径最小和
- 2.1 题目解析
- 2.2 状态转移方程
- 2.3 解题代码
- 三、174. 地下城游戏
- 3.1 题目解析
- 3.2 状态转移方程
- 3.3 解题代码
一、63. 不同路径 II
题目地址: 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
- 向右 -> 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右 -> 向右
1.1 题目解析
状态表示:
对于这种「路径类」的问题,我们的状态表示⼀般有两种形式:
- 从
[i, j]
位置出发,到达⽬标位置有多少种方式; - 从起始位置出发,到达
[i, j]
位置,,⼀共有多少种方式。
这⾥选择第⼆种定义状态表示的方式:dp[i][j]
表示:走到 [i, j]
位置处,⼀共有多少种方式。
状态转移:
简单分析⼀下。如果 dp[i][j]
表示到达 [i, j]
位置的方法数,那么到达 [i, j]
位置之前的⼀小步,有两种情况:
- 从
[i, j]
位置的上方([i - 1, j]
的位置)向下走一步,转移到[i, j]
位置; - 从
[i, j]
位置的左方([i, j - 1]
的位置)向右⾛⼀步,转移到[i, j]
位置。
但是, [i - 1, j]
与 [i, j - 1]
位置都是可能有障碍的,此时从上⾯或者左边是不可能到达 [i, j]
位置的,也就是说,此时的方法数应该是 0。由此我们可以得出⼀个结论,只要这个位置上「有障碍物」,那么我们就不需要计算这个位置上的值,直接让它等于 0 即可。
初始化:
可以在最前⾯加上⼀个「辅助结点」,帮助我们初始化。使⽤这种技巧要注意两个点:
- 辅助结点⾥⾯的值要「保证后续填表是正确的」;
- 「下标的映射关系」。
在本题中,添加一行,并且添加⼀列后,只需将 dp[1][0]
的位置初始化为 1
即可。 添加一行和一列是因为[i, j]
位置需要[i - 1, j]
和[i, j - 1]
两个方位的值,以此确保状态表不越界,将dp[1][0]
初始化为1,为了保证在起点时有方法数 1 。
填表顺序:
根据「状态转移」的推导,填表的顺序就是 「从上往下」填每一行,每一行「从左往右」。
返回值:
根据「状态表⽰」,我们要返回的结果是 dp[m][n]
。
1.2 状态转移方程
从当前位置的上方和左方,到达当前位置的方法数:dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
。还需要注意的是若当前位置为障碍物,直接将当前状态置为0
(即dp[i][j] = 0;
)
1.3 解题代码
class Solution
{
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid)
{
int row = obstacleGrid.size(), col = obstacleGrid[0].size();
//1. 创建dp表
vector<vector<int>> dp(row + 1, vector<int>(col + 1));
//2. 初始化
dp[0][1] = 1;
//3. 填表
//从上到下填表 -> 从左到右填表
for (int i = 1; i <= row; ++i)
for (int j = 1; j <= col; ++j)
if(obstacleGrid[i - 1][j - 1] == 0) dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
//4. 返回值
return dp[row][col];
}
};
二、931. 下降路径最小和
题目地址: 931. 下降路径最小和
给你一个
n x n
的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和。
下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置(row, col)
的下一个元素应当是(row + 1, col - 1)
、(row + 1, col)
或者(row + 1, col + 1)
。
输入:matrix = [[2,1,3],[6,5,4],[7,8,9]]
输出:13
解释:如图所示,为和最小的两条下降路径
2.1 题目解析
状态表示:
对于这种「路径类」的问题,我们的状态表示一般有两种形式:
- 从 [i, j] 位置出发,到达⽬标位置有多少种方式;
- 从起始位置出发,到达 [i, j] 位置,⼀共有多少种方式
这里选择第⼆种定义状态表示的方式:
dp[i][j]
表示:到达 [i, j]
位置时,所有下降路径中的最小和。
状态转移:
对于普遍位置 [i, j]
,根据题意得,到达 [i, j]
位置可能有三种情况:
- 从正上方
[i - 1, j]
位置转移到[i, j]
位置; - 从左上方
[i - 1, j - 1]
位置转移到[i, j]
位置; - 从右上方
[i - 1, j + 1]
位置转移到[i, j]
位置;
我们要的是三种情况下的「最小值」,然后再加上矩阵在 [i, j]
位置的值。于是 dp[i][j] = min(dp[i - 1][j], min(dp[i - 1][j - 1], dp[i - 1][j + 1])) + matrix[i][j]
。
初始化:
可以在最前⾯加上⼀个「辅助结点」,帮助我们初始化。使用这种技巧要注意两个点:
- 辅助结点里面的值要「保证后续填表是正确的」;
- 「下标的映射关系」。
事实上这题的状态转移方程是不难想到的,而关键问题在于初始化。[i, j]
位置的值可以从三个方向来 - 上、左、右,所以为了不越界,在本题中,需要「加上一行」,并且「加上两列」(最左边和最右边)。 所有的位置都初始化为无穷大,然后将第一行初始化为 0 即可。将初始值设为无穷大是因为,这些是虚拟节点,不应该对选数造成影响(即大于其他两路的值)。
填表顺序:
根据「状态表示」,填表的顺序是「从上往下」。
返回值:
注意这里不是返回 dp[m][n]
的值!
题⽬要求「只要到达最后一行」就行了,因此这⾥应该返回「 dp 表中最后一行的最小值」。
2.2 状态转移方程
dp[i][j] = min(dp[i - 1][j], min(dp[i - 1][j - 1], dp[i - 1][j + 1])) + matrix[i][j];
。
2.3 解题代码
class Solution
{
public:
int minFallingPathSum(vector<vector<int>>& matrix)
{
//1. 创建dp表
int row = matrix.size(), col = matrix.size();
vector<vector<int>> dp(row + 1, vector<int>(col + 2, INT_MAX));
//2. 初始化列表
//第一行变为0
for(int i = 0; i < col + 2; ++i) dp[0][i] = 0;
//3. 填表
for(int i = 1; i <= row; ++i)
for(int j = 1; j <= col; ++j)
dp[i][j] = matrix[i - 1][j - 1]
+ min(min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i - 1][j + 1]);
//4. 返回结果
int ans = INT_MAX;
for(int i = 1; i <= col; ++i)
ans = min(ans, dp[row][i]);
return ans;
}
};
三、174. 地下城游戏
题目地址: 174. 地下城游戏
恶魔们抓住了公主并将她关在了地下城 dungeon 的 右下角 。地下城是由 m x n 个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快解救公主,骑士决定每次只 向右 或 向下 移动一步。
返回确保骑士能够拯救到公主所需的最低初始健康点数。
注意: 任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
输入:dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]
输出:7
解释:如果骑士遵循最佳路径:右 -> 右 -> 下 -> 下 ,则骑士的初始健康点数至少为 7 。
3.1 题目解析
状态表示:
这道题如果我们定义成:从起点开始,到达 [i, j]
位置的时候,所需的最低初始健康点数。那么我们分析状态转移的时候会有⼀个问题:那就是我们当前的健康点数还会受到后面的路径的影响。也就是从上往下的状态转移不能很好地解决问题。
这个时候我们要换⼀种状态表示:从 [i, j]
位置出发,到达终点时所需要的最低初始健康点数。 这样我们在分析状态转移的时候,后续的最佳状态就已经知晓。
综上所述,定义状态表示为:
dp[i][j]
表示:从 [i, j]
位置出发,到达终点时所需的最低初始健康点数。
状态转移方程:
对于 dp[i][j]
,从 [i, j]
位置出发,下⼀步会有两种选择(为了方便理解,设 dp[i][j]
的最终答案是 x ):
- 走到右边,然后走向终点:
那么我们在[i, j]
位置的最低健康点数加上这⼀个位置的消耗,应该要⼤于等于右边位置的最低健康点数,也就是:x + dungeon[i][j] >= dp[i][j + 1]
。通过移项可得:x >= dp[i][j + 1] - dungeon[i][j]
。因为我们要的是最小值,因此这种情况下的x = dp[i][j + 1] - dungeon[i][j]
; - 走到下边,然后走向终点:
那么我们在[i, j]
位置的最低健康点数加上这⼀个位置的消耗,应该要大于等于下边位置的最低健康点数,也就是:x + dungeon[i][j] >= dp[i + 1][j]
。通过移项可得:x >= dp[i + 1][j] - dungeon[i][j]
。因为我们要的是最小值,因此这种情况下的x = dp[i + 1][j] - dungeon[i][j]
;
综上所述,我们需要的是两种情况下的最小值,因此可得状态转移方程为:dp[i][j] = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]
但是,如果当前位置的 dungeon[i][j]
是⼀个比较大的正数的话, dp[i][j]
的值可能变成 0 或者负数。也就是最低点数会小于 1 ,那么骑士就会死亡。因此我们求出来的 dp[i][j]
如果小于等于 0 的话,说明此时的最低初始值应该为 1 。处理这种情况仅需让 dp[i][j]
与 1 取⼀个最大值即可:dp[i][j] = max(1, dp[i][j])
初始化:
可以在最前⾯加上⼀个「辅助结点」,帮助我们初始化。使用这种技巧要注意两个点:
- 辅助结点⾥⾯的值要「保证后续填表是正确的」;
- 「下标的映射关系」。
在本题中,在 dp 表最后⾯添加一行,并且添加⼀列后,所有的值都先初始化为⽆穷大,然后让dp[m][n - 1] = dp[m - 1][n] = 1
即可。
填表顺序:
根据「状态转移方程」,我们需要「从下往上填每一行」,「每一行从右往左」。
返回值:
根据「状态表示」,我们需要返回 dp[0][0]
的值。
3.2 状态转移方程
骑士到达当前位置最低健康点数:dp[i][j] = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]
确保健康值不低于1:dp[i][j] = max(1, dp[i][j]);
3.3 解题代码
class Solution
{
public:
int calculateMinimumHP(vector<vector<int>>& dungeon)
{
int m = dungeon.size(), n = dungeon[0].size();
// 建表 + 初始化
vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
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] = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j];
dp[i][j] = max(1, dp[i][j]);
}
}
// 返回结果
return dp[0][0];
}
};