目录
题目一:不同路径
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7 输出:28
示例 2:
输入:m = 3, n = 2 输出:3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 3. 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3 输出:28
示例 4:
输入:m = 3, n = 3 输出:6
依旧是这五步:
①状态表示
经验 + 题目要求
以 [i, j] 为结尾 ......
dp[i][j]表示:走到 [i, j] 位置时一共有多少种方式
②状态转移方程
根据最近的一步,划分问题
因为只能向右或是向下移动,那么移动到 [i, j] 位置前的位置,一定是上边的或是左边的位置,也就是坐标为:[ i - 1, j] 或 [ i, j - 1]
所以到 [i, j]位置时的方法数,相当于从起点走到上边或左边位置的方法数,因为走到上边或左边后,走一步走到了[i, j]位置
所以状态转移方程:
dp[i, j] = dp[i -1 , j] + dp[i, j - 1]
③初始化
因为当前位置需要上面或左边位置的方法数,但是上面或是左边可能会越界,那么此时可以给网格多加一行和一列,这样就可以保证每个格子都存在上面和左边的位置了
所以下面考虑两个细节:
1、虚拟节点的值需要保证后面填表的结果是正确的
这里可不是将所有值全部初始化为0,而是将第一个位置的上面的虚拟节点(或左边的)初始化为1,剩下的各自初始化为0,这样就可以保证第一个位置的结果为1,继而每个位置的结果是正确的
上述的绿色方框就是虚拟出来的结点,红数字就是虚拟节点的值,可以保证填表的结果是正确的
2、下标的映射
映射每个位置都+1即可
④填表顺序
填表顺序从上到下填写,从左到右填写
⑤返回值
因为多开辟了一行一列,所以返回值就是dp[m, n]
代码如下:
class Solution
{
public:
int uniquePaths(int m, int n)
{
// 初始化为0
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++) // 从左向右填表
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
return dp[m][n];
}
};
题目二:不同路径II
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1
和 0
来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2
条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]] 输出:1
这道题是上一题的升级版,在表格中加上了障碍物,同样是求有多少种方式到右下角
①状态表示
和上一题的状态表示一样:
dp[i][j]表示:走到 [i, j] 位置时一共有多少种方式
②状态转移方程
如果有障碍物,dp[i][j] = 0
如果没有障碍物,dp[i][j] = dp[i - 1][ j ] + dp[ i ][j - 1]
③初始化
与上一题一样,多加一行多加一列
也是将dp[0][1] = 1,剩下的虚拟位置为0
④填表顺序
同样是从上到下填表,从左到右填表
⑤返回值
同样返回dp[m][n]
代码如下:
class Solution
{
public:
int uniquePathsWithObstacles(vector<vector<int>>& ob)
{
int m = ob.size(), n = ob[0].size();
// 创建dp表
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
// 初始化
dp[0][1] = 1;
// 从上向下,从左向右填表
for(int i = 1; i <= m; i++)
{
for(int j = 1; j <= n; j++)
{
// 如果为0,就说明没有障碍物,执行状态转移方程
// 如果为1,有障碍物不需要处理,该位置的值即方式数默认为0
if(ob[i - 1][j - 1] == 0)
{
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m][n];
}
};
题目三:珠宝的最高价值
现有一个记作二维矩阵 frame
的珠宝架,其中 frame[i][j]
为该位置珠宝的价值。拿取珠宝的规则为:
- 只能从架子的左上角开始拿珠宝
- 每次可以移动到右侧或下侧的相邻位置
- 到达珠宝架子的右下角时,停止拿取
注意:珠宝的价值都是大于 0 的。除非这个架子上没有任何珠宝,比如 frame = [[0]]
。
示例 1:
输入: frame = [[1,3,1],[1,5,1],[4,2,1]]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最高价值的珠宝
这道题同样是从左上角移动到右下角,并且只能向右或想下移动
①状态表示
以这个位置为结尾,......
所以dp[i][j]表示从起点到当前位置时,珠宝的最高价值
②状态转移方程
根据最近的一步,划分问题
所以同样,到达某一个位置,只会从上边或是左边的位置移动到当前位置
所以当前位置的最大价值,就是上边或是左边的位置的最大价值,加上当前位置的最大价值
因为要的是最大价值,所以需要找到这两个位置的最大值,再加该位置的价值
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + g[i][j]
③初始化
同样可能会出现越界的情况,所以多加一行一列
此时为了保证填表的值是正确的,因为每个位置的价值不可能出现小于0的情况,所以根据上面的状态转移方程,可以知道多加的一行和一列位置为0就可以保证结果正确
下标的映射就需要注意:当想要访问原表时,横纵坐标都需要减1
④填表顺序
填表顺序从上往下填写,从左往右填写
⑤返回值
同样返回值是dp[m][n]
代码如下:
class Solution
{
public:
int jewelleryValue(vector<vector<int>>& frame)
{
int m = frame.size(), n = frame[0].size();
// 创建dp表 + 初始化
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]) + frame[i-1][j-1];
return dp[m][n];
}
};
题目四:下降路径最小和
给你一个 n x n
的 方形 整数数组 matrix
,请你找出并返回通过 matrix
的下降路径 的 最小和 。
下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col)
的下一个元素应当是 (row + 1, col - 1)
、(row + 1, col)
或者 (row + 1, col + 1)
。
示例 1:
输入:matrix = [[2,1,3],[6,5,4],[7,8,9]] 输出:13 解释:如图所示,为和最小的两条下降路径
示例 2:
输入:matrix = [[-19,57],[-40,-5]] 输出:-59 解释:如图所示,为和最小的下降路径
分为五步解决:
①状态表示
dp[i][j]表示:到达[i, j]位置时最小的下降路径
②状态转移方程
根据最近的一步
因为可能从三个位置移动到当前位置,分别是:左上角、正上方、右上角
所以分为三种情况:
1、从[i - 1][j - 1]到[i][j]
2、从[i - 1][ j ] 到[i][j]
3、从[i - 1][j + 1]到[i][j]
因为要求的是最小的下降路径,所以就是上述三种情况的最小值,再 + m[i][j] 的值
dp[i][j] = min(情况1,情况2,情况3) + m[i][j]
③初始化
因为每一个位置都可能需要上面的三个位置,所以左边上面右边的一列,都有可能越界,因此还是采用扩容的方式,来保证不越界访问
其中虚线就表示扩容的位置:
因为求的是路径的最小值,所以第一行的虚拟值全部给0,不会影响结果,而从第二行开始,为了不影响到最终的结果,需要将值都填为正无穷大,这样就可以保证肯定不会选到这个位置,从而影响最终的结果,所以最终初始化结果为:
还需要注意下标映射的位置
④填表顺序
因为填每一个位置只需要上面的三个位置,而不需要用到左右位置的数
所以填表顺序是从上往下填
⑤返回值
返回值到最后一行就是最终的路径和了,所以返回值是dp表最后一行的最小值
代码如下:
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& m) {
int n = m.size();
// 创建dp表,默认初始化为+∞
vector<vector<int>> dp(n + 1, vector<int>(n + 2, INT_MAX));
// 将第一行虚拟的位置初始化为0
for(int j = 0; j < n + 2; j++) dp[0][j] = 0;
// 填表
for(int i = 1; i <= n; 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]) + m[i-1][j-1];
int ret = INT_MAX;
// 取最后一行dp表中最小的值
for(int j = 1; j <= n; j++) ret = min(ret, dp[n][j]);
return ret;
}
};
题目五:最小路径和
给定一个包含非负整数的 m x n
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]] 输出:7 解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]] 输出:12
这道题可以说是非常熟悉了,与珠宝的最高价值非常像,那道题是最高价值,这道题是最小路径和,并且也是只能向下或是向右移动一步
①状态表示
以这个位置为结尾,......
所以dp[i][j]表示从起点到当前位置时,最小路径和
②状态转移方程
根据最近的一步,划分问题
所以同样,到达某一个位置,只会从上边或是左边的位置移动到当前位置
所以当前位置的最小路径和,就是上边或是左边的位置的最小路径和,再加上当前位置的路径
因为要的是最小路径和,所以需要找到这两个位置的最小值,再加该位置的路径即可
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + g[i][j]
③初始化
同样可能会出现越界的情况,所以多加一行一列
此时为了保证填表的值是正确的,因为每个位置的价值不可能出现小于0的情况,所以根据上面的状态转移方程,可以知道多加的一行和一列位置全部为INT_MAX,并且将第一个位置的上面或是左边置为0就可以保证结果正确
因为此时第一个位置的最小路径和就是它本身的值,而接下来该第二位置时就不会选择上面的值,只会选择第一个位置,因为第二个位置上面是INT_MAX,要求的是最小值,所以只会选择0
下标的映射就需要注意:当想要访问原表时,横纵坐标都需要减1
④填表顺序
填表顺序从上往下填写,从左往右填写
⑤返回值
同样返回值是dp[m][n]
代码如下:
class Solution
{
public:
int minPathSum(vector<vector<int>>& g)
{
int m = g.size(), n = g[0].size();
// 创建dp表,先全部初始化为INT_MAX,将第一个位置上面位置初始化为0即可
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]) + g[i-1][j-1];
return dp[m][n];
}
};
题目六:地下城游戏
恶魔们抓住了公主并将她关在了地下城 dungeon
的 右下角 。地下城是由 m x n
个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快解救公主,骑士决定每次只 向右 或 向下 移动一步。
返回确保骑士能够拯救到公主所需的最低初始健康点数。
注意:任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
示例 1:
输入:dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]] 输出:7 解释:如果骑士遵循最佳路径:右 -> 右 -> 下 -> 下 ,则骑士的初始健康点数至少为 7 。
示例 2:
输入:dungeon = [[0]] 输出:1
下面依旧分为五步:
①状态表示
经验 + 题目要求
1、以某位置为结尾 .....
此时dp[i][j]表示:从起点出发, 到达[i, j]位置时,所需的最低的初始健康点数
但是这道题中,这种表示方式是不可取的,因为该位置的健康点数不仅受到上面或是左边的点数影响,还会受到下一个位置健康点数的影响,因为这种方式只能根据上面或左边的点数决定,不会考虑到后面位置的影响, 所以排除这种方式
2、以某位置为起点 .....
此时dp[i][j]表示:从[i, j]位置出发, 到达终点时,所需的最低的初始健康点数
这时就能够满足,此时下一步的位置是往下或是往右边移动
②状态转移方程
因为此时的状态表示是以该位置为起点,所以分析如下:
有两种情况,往右边走和往下边走,假设当前的最低健康点数是x,d[i][j]表示当前位置的值,所以需要满足:
x + d[i][j] >= dp[i][j + 1] / x + d[i][j] >= dp[i + 1][j]
x + d[i][j] 就表示能从 [i, j] 位置走出来
>= dp[i][j + 1] 表示走出来后的值还需要能够满足移动到下一个位置的健康点数
所以不等式交换一下得到:x >= dp[i][j + 1] - d[i][j] / x >= dp[i + 1][j] - d[i][j]
所以 dp[i][j] = min(dp[i][j + 1], dp[i + 1][j]) - d[i][j]
但是这里有一种特殊情况,d[i, j] 的值如果非常大,可能走到dp[i, j]位置时是负数,但是加上这个位置的血包,就会变为正数,这种情况也能上面的不等式,但是这是明显不符合题意的,如果走到这个位置时健康值已经为负数了,那么哪怕这个位置血包再大,也会立刻死亡,所以需要考虑到这种情况
上述的情况,在 x >= dp[i][j + 1] - d[i][j] 不等式中,d[i][j] 非常大, x就会变为负数,所以在中途判断时,还需要将dp[i][j]取正数,如果为负数,需要将其置为1,也就是扩大这里的初始健康点数,让骑士能够满足最低血量条件
即:dp[i][j] = max(1, dp[i][j])
③初始化
这道题因为采取的是以某个位置作为起点,所以就不会在上面或左边越界了, 而是会在下面或右边越界,所以增加的位置是:下面和右边加上一行一列,此时因为在访问原有位置时下标并没有发生改变,所以不需要考虑下标的映射关系了
所以只需要考虑填表的结果是正确的即可
因为原本位置的右下角是公主所在的地方,所以此时走到该位置处的健康值至少得是1,所以在该位置的下边或右边置为1,
而新增的一行一列的其他位置,为了保证移动过程中不会选择到这些位置上的数据, 因为每一次都是找下边或是右边的最小值,只需要保证这些位置的数据都是最大的即可,因为值是INT_MAX时,比较时就不会选择这个位置的数,也就不会影响正常结果,所以初始化时将其余位置全部置为INT_MAX,只有右下角的右边或是下边置为1
④填表顺序
因为每个位置需要考虑到右边或是下面的位置的数据
所以填表顺序就是:从下往上填每一行,每一行从右向左填写
⑤返回值
因为要求的是初始位置的最低健康点数,所以返回值是:dp[0][0]
代码如下:
class Solution
{
public:
int calculateMinimumHP(vector<vector<int>>& d)
{
int m = d.size(), n = d[0].size();
// 创建dp表并初始化
vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
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][j + 1], dp[i + 1][j]) - d[i][j];
// 如果dp[i][j]为负数就置为1
dp[i][j] = max(1, dp[i][j]);
}
}
return dp[0][0];
}
};
关于[动态规划]路径问题的题目到此结束啦