算法沉淀 —— 动态规划篇(路径问题)
- 前言
- 一、不同路径1
- 二、珠宝的最高价值
- 三、下降路径最小和
- 四、地下城游戏
前言
几乎所有的动态规划问题大致可分为以下5个步骤,后续所有问题分析都将基于此
-
1.、状态表示:通常状态表示分为以下两种,其中更是第一种为主。
以i为结尾
,dp[i] 表示什么,通常为代求问题(具体依题目而定)以i为开始
,dp[i]表示什么,通常为代求问题(具体依题目而定)
-
2、状态转移方程
*以上述的dp[i]意义为根据, 通过最近一步来分析和划分问题
,由此来得到一个有关dp[i]的状态转移方程。 -
3、dp表创建,初始化
- 动态规划问题中,如果直接使用状态转移方程通常会伴随着
越界访问
等风险,所以一般需要初始化。而初始化最重要的两个注意事项便是:保证后续结果正确,不受初始值影响;下标的映射关系
。 - 而
初始化一般分为以下两种:
直接初始化开头的几个值。
一维空间大小+1,下标从1开始;二维增加一行/一列
。
- 动态规划问题中,如果直接使用状态转移方程通常会伴随着
-
4、填dp表、填表顺序:根据状态转移方程来确定填表顺序。
-
5、确定返回值
一、不同路径1
【题目链接】:LCR 098. 不同路径
【题目】:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
【分析】:
这是个二维数组问题。我们定义dp[i][j]表示机器人走到下标为[i][j]位置时的总路径数。显然机器人要走到[i][j]位置,只能从[i][j-1]向右走、[i-1][j]向下走。所以状态转移方程为dp[i][j] = dp[i-1][j] + dp[i][j-1]
。 但当i = 0或j =0时,显然状态转移方程不适应,需要特殊处理。这里我们采用的办法时,横纵都新增一行。然后我们还需将dp[0][1]或dp[1][0]初始化为1。
接下我仅需从左往右、从上到下依次填表。最后返回结果即可!!
【代码实现】:
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m + 1, vector<int>(n + 1));//创建dp表
//初始化
dp[1][0] = 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];
}
};
二、珠宝的最高价值
【题目链接】:LCR 166. 珠宝的最高价值
【题目】:
现有一个记作二维矩阵 frame 的珠宝架,其中 frame[i][j] 为该位置珠宝的价值。拿取珠宝的规则为:
只能从架子的左上角开始拿珠宝每次可以移动到右侧或下侧的相邻位置到达珠宝架子的右下角时,停止拿取。
注意:珠宝的价值都是大于 0 的。除非这个架子上没有任何珠宝,比如 frame = [[0]]。
【分析】:
我们可以定义dp[i][j]表示从开始到[i][j]位置所能拿到的珠宝最大价值。所以要得到dp[i][j]的值,我们只需将dp[i][j-1]和dp[i-1][j]的较大值假设当前下标[i][j]的珠宝价值即可。即动态转移方程为dp[i][j] = max(dp[i-1][j] + dp[i][j-1]) + frame[i][j]
。但显然当i=0或j=0时,需要特殊处理。这里还是采用横竖都各加一行。需要注意的是此时下标的映射关系(具体参考代码)。
;接下我仅需从左往右、从上到下依次填表。最后返回结果!!
【代码实现】:
class Solution {
public:
int jewelleryValue(vector<vector<int>>& frame) {
int m = frame.size(), n = frame[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][j-1], dp[i-1][j]) + frame[i - 1][j - 1];
return dp[m][n];
}
};
三、下降路径最小和
【题目链接】:931. 下降路径最小和
【题目】:
给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和 。
下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col) 的下一个元素应当是 (row + 1, col - 1)、(row + 1, col) 或者 (row + 1, col + 1) 。
【分析】:
我们定义dp[i][j]表示下降到[i][j]位置时,下降路径的最小和。并且题目中已经明确表示下降到[i][j]位置有如下三种方式:
显然我们容易得到状态转移方程为dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i-1][j+1])) + matrix[i-1][j-1]
。但又有一个问题,当i=0、j=0、j=n时,状态转移方程会越界访问。所以我们给出的办法时加1行、加2列。同时为了不影响后续填表结果,我们将第一行初始为0,第1列和第n+1列初始化为INT_MAX(dp[0][1]、dp[0][n+1]除外)。
接下来从左往右、从上到下依次填表。dp表填好后,最后一行的每个数都有可能是结果。我们需要依次比较,将最后一行的最小值返回!
【代码实现】:
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
vector<vector<int>> dp(m+1, vector<int>(n + 2, INT_MAX));
//初始化
for(int j = 0; j < n + 2; j++)
dp[0][j] = 0;
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
{
dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i-1][j+1])) + matrix[i-1][j-1];
}
int ret = INT_MAX;
for(int j = 1; j <= n; j++)
ret = min(ret, dp[m][j]);
return ret;
}
};
四、地下城游戏
【题目链接】:174. 地下城游戏
【题目】:
恶魔们抓住了公主并将她关在了地下城 dungeon 的 右下角 。地下城是由 m x n 个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快解救公主,骑士决定每次只 向右 或 向下 移动一步。
返回确保骑士能够拯救到公主所需的最低初始健康点数。
注意:任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
【实例】:
输入:dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]
输出:7
解释:如果骑士遵循最佳路径:右 -> 右 -> 下 -> 下 ,则骑士的初始健康点数至少为 7
【分析】:
dp问题中我们一般定义dp[i][j]表示从开始到[i][j]位置的待解结果,即骑士从[0][0]走到[i][j]所需的最低初始健康点数。但我们发现[i][j]位置后面的数据对结果存在影响。例如:dungeon = [[1, 1],[1, -100]],假设我们走到了[0][1]位置,此时dp[0][1]=1。但此时走到dungeon[1][1]时,骑士死亡。后面结果会对当前数据有影响!!因此该思路错误。
我们可以定义dp[i][j]表示从[i][j]位置走到结尾(假设结尾下标为[m][n])骑士所需的最低健康点数。此时示意图如下:(各位懂意思就行,手残画不了图)
所以我们可以得到状态转移方程为dp[i][j] = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]
。但还有两个问题:
- 如果dungeon[i][j]非常大,此时dp[i][j]可能为负数。此时骑士死亡,不符合要求。所以我们要进一步处理,
dp[i][j] = max(1, dp[i][j])
(即如果dp[i][j]为负,此时表示dungeon[i][j]j较大,我们仅需保证骑士到[i][j]位置时没有死亡即可) - 如果[i][j]表示结尾呢?此时状态转移方程不适应。我们给出的办法是,最后一行、和最后一列各增一行。同时为了保存新增行列对后续填dp表不产生影响,我们其中的元素初始化为INT_MAX。同时为了保证dp[m][n]在是由状态转移方程时填表正确。我们要保证的时骑士处于[m][n]位置时还剩1个健康点数即可。所以我们将dp[m+1][n]或dp[m][n+1]初始化为1!
【代码实现】:
class Solution {
public:
int calculateMinimumHP(vector<vector<int>>& dungeon) {
int m = dungeon.size(), n = dungeon[0].size();
//创建dp表
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];
}
};