路径问题
- 1. 不同路径(medium)
- 2. 不同路径II(medium)
- 3. 礼物最大值(medium)
- 4. 下降路径最小和(medium)
- 5. 最⼩路径和(medium)
- 6. 地下城游戏(hard)
1. 不同路径(medium)
1.题目链接:不同路径
2.题目描述:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
3.问题分析:
对于动态规划关于路径问题的分析,首先需要选取某个位置,然后以该位置为起点或者结尾进行分析。如这题dp数组中i位置的结果是需要i-1、i-2等位置算出来的,这种情况就以i位置为结尾进行分析。
- 状态表示:对于m*n的网格来说,每一个格子可以对应到一个二维数组中的元素,对于从起点到某点(i,j)由多少种不同路径是从起始位置进行计算,所以以(i,j)位置为结尾进行分析。用dp[i][j]表示:从起点位置到[i,j]位置处,一共有多少种方式。有时候是不知道状态表示是正确还是错误的,为此只能先往下分析
- 状态转移方程:对(i,j)位置进行分析,dp[i][j]表示从起点位置到[i , j]位置处,一共有多少种方式,那么怎样才能到达 [i , j] 这个位置?因为只能往下或者往右走,所以到达 [i , j] 位置可以是 [i - 1, j] 或者是 [i , j - 1]这两个位置,两个位置的值进行相加 ,就是到达[i , j]位置处总共的方式。因此状态转移方程为:dp[i , j] = dp[i - 1, j] + [i , j - 1]。
- 初始化:对于第一行第一列来说,进行-1操作会放生越界情况。可以用if语句判断一下,也可以添加一行一列,多出来的一行一列成为辅助节点,使⽤这种技巧要注意两个点:辅助结点⾥⾯的值要保证后续填表是正确的;下标的映射关系。初始化只将dp[0][1]位置初始化为1即可。
- 填表顺序:从上往下填每⼀⾏,在填写每⼀⾏的时候从左往右。
- 返回值:返回dp[m][n]。
4.代码如下:
class Solution
{
public:
int uniquePaths(int m, int n)
{
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)); // 创建⼀个 dp表
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];
}
};
2. 不同路径II(medium)
1.题目链接:不同路径II
2.题目描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
3.问题分析
这道题和不同路径I区别就是这道题中的网格有障碍物,所以大致分析思路是相同的,就比如这道题是以[i, j]位置为结尾进行分析,状态表示也相同。
- 状态表示:dp[i][j] 表示:⾛到 [i, j] 位置处,⼀共有多少种⽅式。
以[i, j]位置为结尾进行分析,同上到题,到达 [i , j] 位置可以是 [i - 1, j] 或者是 [i , j - 1]这两个位置,两个位置的值进行相加 ,就是到达[i , j]位置处总共的方式;但是如果有障碍物呢?比如[i - 1, j] 或者是 [i , j - 1]其中有一个是障碍物,那么到达[i , j] 位置的所有方法就是其中一个某个不是障碍物的路径数。所以另一个有障碍的路径数为零,即遇到障碍物就不需要计算这个位置上的值,直接让它等于0。 - 状态转移方程:同上一题dp[i , j] = dp[i - 1, j] + [i , j - 1]。不过如果这个位置上有障碍物,那么就不需要计算这个位置上的值,直接让它等于 0 即可。
- 初始化:同上一题,可以添加⼀行,并且添加⼀列后,只需将dp[1][0]的位置初始化为1 即可。
- 填表顺序:从上往下填每⼀⾏,在填写每⼀⾏的时候从左往右。
- 返回值:返回dp[m][n]。
4.代码如下
class Solution
{
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid)
{
int m = obstacleGrid.size(), n = obstacleGrid[0].size();
vector<vector<int>> dp(m + 1, vector<int>(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] == 0)
{
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m][n];
}
};
3. 礼物最大值(medium)
1.题目链接:礼物最大值
2.题目描述
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
3.问题分析
这道题和上述两道又有所不同,但都属于同种类型,所以方法大致一样。先进行状态表示:用dp[i][j] 表示:⾛到 [i, j] 位置处,此时的最⼤价值。上述两题是要算出路径数,所以将[i - 1, j] 的路径数与[i , j - 1]的路径数相加。而这道题是要求路径上的最大价值,可以将[i - 1, j] 路径上的价值数与[i , j - 1]路径上的价值数进行比较,保留两个中较大的值然后再加上grid[i,j]位置上的值即可。
- dp[i][j] 表⽰:⾛到 [i, j] 位置处,此时的最⼤价值
- 状态转移方程:从[i - 1, j] 位置,向下⾛⼀步,此时到达 [i, j] 位置能拿到的礼物价值为 dp[i - 1][j] + grid[i][j] ; 从 [i, j - 1] 位置,向右⾛⼀步,此时到达 [i, j] 位置能拿到的礼物价值为dp[i][j - 1] + grid[i][j]。
- 初始化:同上一题,可以添加⼀行,并且添加⼀列后,所有的值都为 0 即可。
- 填表顺序:从上往下填每⼀⾏,在填写每⼀⾏的时候从左往右。
- 返回值:返回dp[m][n]。
4.代码如下
class Solution
{
public:
int maxValue(vector<vector<int>>& grid)
{
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
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]) + grid[i - 1][j - 1];
return dp[m][n];
}
};
4. 下降路径最小和(medium)
1.题目链接:下降路径最小和
2.题目描述
给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和 。
下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col) 的下一个元素应当是 (row + 1, col - 1)、(row + 1, col) 或者 (row + 1, col + 1) 。
3.问题分析
大致思路不变,先表示状态,用二维数组dp[i][j] 表⽰:到达 [i, j] 位置时,所有下降路径中的最⼩和。然后进行分析,上述所示的问题,因只能向左或向下移动,所以[i,j]的路径数是将[i - 1, j] 的路径数与[i , j - 1]的路径数相加;而这道题位置 (row, col) 的下一个元素应当是 (row + 1, col - 1)、(row + 1, col) 或者 (row + 1, col + 1) ;以[i,j]为结尾进行分析,到达[i,j]位置的上一层应当是[i - 1, j - 1],[i - 1, j],[i - 1, j + 1]中三者最小的一个,然后加上matrix[i,j]的值,然后遍历依次填写dp表即可。其中有越界问题,如[0, j]或[i, n - 1]位置,可以用辅助结点来解决,增加两列,一列在起始,另一列在末尾;最后寻找最后一行最小的值即为下降路径最小和。
- dp[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] 。
- 初始化:需要加上两列,所有的位置都初始化为⽆穷⼤,将dp表第一行初始为matrix第一行(左右两列的辅助结点是无穷大)
- 从上往下填每⼀⾏,在填写每⼀⾏的时候从左往右。
4.代码如下
class Solution
{
public:
int minFallingPathSum(vector<vector<int>>& matrix)
{
int n = matrix.size();
//初始化dp表,全部初始化为无穷大
vector<vector<int>> dp(n, vector<int>(n + 2, INT_MAX));
//=将dp表第一行初始为matrix第一行
for (int j = 0; j < n; ++j)
dp[0][j + 1] = matrix[0][j];
//从第二行第二列开始遍历
for (int i = 1; i < n; ++i)
{
//在dp表中不是辅助结点的位置范围是1到n
for (int j = 1; j <= n; ++j)
{
int x = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i - 1][j + 1]));
dp[i][j] = x + matrix[i][j - 1]; //没有添加行,matrix对应的映射为i,j-1
}
}
//寻找最后一行最小值
int ret = dp[n - 1][0];
for (int j = 1; j <= n; ++j)
ret = min(ret, dp[n - 1][j]);
return ret;
}
};
5. 最⼩路径和(medium)
1.题目链接:最⼩路径和
2.题目描述
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
3.问题分析
这道题与上述题基本一致了,分析就略过。
- 状态表⽰:dp[i][j] 表⽰:到达 [i, j] 位置处,最⼩路径和是多少。
- 这道题是要求路径上的最小和,可以将[i - 1, j] 路径上的最小和与[i , j - 1]路径上的最小和进行比较,保留两个中较小的值然后再加上grid[i,j]位置上的值即可。
- 添加⼀⾏,并且添加⼀列后,所有位置的值可以初始化为⽆穷⼤,然后让dp[0][1] = dp[1][0] = 0 即可。
- 要返回的结果是dp[m][n]。因为添加了一行一列。
4.代码如下
class Solution
{
public:
int minPathSum(vector<vector<int>>& grid)
{
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
dp[0][1] = dp[1][0] = 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];
}
};
6. 地下城游戏(hard)
1.题目链接:地下城游戏
2.题目描述
恶魔们抓住了公主并将她关在了地下城 dungeon 的 右下角 。地下城是由 m x n 个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快解救公主,骑士决定每次只 向右 或 向下 移动一步。
返回确保骑士能够拯救到公主所需的最低初始健康点数。
注意:任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
3.问题分析
这道题已知骑士到达公主所在地,也就是右下角,骑士此时所剩健康点数为1 ,求到达终点时,起始位置生命健康值的最小点数。所以这道题应该从右下角开始遍历,至于怎么遍历,看如下分析:状态表示:dp[i][j] 表⽰:从 [i, j] 位置出发,到达终点时所需的最低初始健康点数; 以[i,j]位置为起点进行分析,有很多条路径可以到达终点,路径上有加血的,有减血的,我们要求什么?求的是血最多的时候?不是,求的是应该血量刚够过该路径的值 也就是dungeon数组中路径求和过程中各个路径负数的最大值的绝对值,所以减去 dungeon[i][j]就是所需的生命值,如果为dp结果负数,就说明能量值很大,而dp[i][j] 表⽰:从 [i, j] 位置出发,到达终点时所需的最低初始健康点数,dp[i][j]大于0,出现负数就要赋值为1。负数就代表起始位置生命值最低值为负数,这很显然不肯能为负数。
位置[i + 1, j]或者 [i, j + 1]可以到达[i, j] 位置,dp的位置表示最低点数,所以选取 [i + 1, j], [i, j + 1]两者中的较小值减去dungeon[i][j]就是dp[i][j]的最小值。
- dp[i][j] 表⽰:从 [i, j] 位置出发,到达终点时所需的最低初始健康点数。
- 状态转移方程:dp[i][j] = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j];如果dp[i][j] <= 0,则dp[i][j] = 1。
- 初始化:在本题中,在 dp 表最后⾯添加⼀⾏,并且添加⼀列后,所有的值都先初始化为⽆穷⼤,然后让dp[m][n - 1] = dp[m - 1][n] = 1 即可。
- 填表顺序:需要从下往上填每⼀⾏,每⼀⾏从右往左。
- 返回值:dp[0][0]。
4.代码如下
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];
if (dp[i][j] <= 0)
{
dp[i][j] = 1;
}
}
}
// 返回结果
return dp[0][0];
}
};