动态规划2:不同路径模型
1、不同路径
状态表示

分析题目,我们很容易能想到使用二维数组。
我们以到达某一位置有多少条路径为状态表示方法,即:
dp[i][j]表示到达第i行,第j列位置有多少条路径。
状态转移方程
题目要求机器人只能向下,或者向右走一步,所以走到(i, j)位置,可以是从(i-1, j)向下走一步得来,也可以是(i, j-1)向右走一步得来。
从(i-1, j)得来这个角度,就是将从起点到(i-1, j)的每一个方法,与(i-1, j)到(i, j)的一步搭配,方法数为dp[i-1][j]。
同理,从(i, j-1)得来这个角度,就是将从起点到(i, j-1)的每一个方法,与(i, j-1)到(i, j)的一步搭配,方法数为dp[i][j-1]。
所以状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i][j-1];
初始化
考虑到越界的问题,我们要初始化的有第一行和第一列:

由于从起点到起点,只有一种方法:不动;从起点到第一行或第一列的任何一个位置,只有一种方法:一直向右或一直向下。
所以,第一行和第一列所有值全部初始化成1。
我们不妨多加一行一列,只在一个地方给1,其余都给0:

这样,我们通过状态转移方程,就可以将实际的第一行和第一列所有值全部初始化成1。
填表顺序
从上到下,从左往右。
返回值
返回dp[m][n]。
代码演示:
class Solution {
public:
int uniquePaths(int m, int n) {
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)
{
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m][n];
}
};
2、不同路径Ⅱ
这道题的解法与“不同路径”非常相似,只是在状态转移方程步骤中加一步判断:
走到(i, j)位置,可以是从(i-1, j)向下走一步得来,也可以是(i, j-1)向右走一步得来:
- 如果(i, j)位置上有障碍,那么到不了(i, j)位置,方法数为0(不做处理)
- 如果(i-1, j)或(i, j-1)有障碍,我们不需要另外讨论,因为计算dp[i-1][j]或dp[i][j-1]时,由于障碍,dp[i-1][j]或dp[i][j-1]的值为0


代码演示:
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));
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、珠宝的最大价值

这道题,与不同路径也是非常相似。
状态表示
dp[i][j]表示到达(i, j)位置时,拿到所有珠宝的价值总和的最大值。
状态转移方程
(i, j)位置,可以从(i-1, j)位置向下走一步,也可以从(i, j-1)向右走一步:
- 从(i-1, j)向下走一步,价值总和为(i-1, j)位置处能拿到的最大价值,加上(i, j)位置上的价值
- 从(i, j-1)向下走一步,价值总和为(i, j-1)位置处能拿到的最大价值,加上(i, j)位置上的价值
然后对求得的两个价值取最大:
dp[i][j] = max(dp[i-1][j] + frame[i-1][j-1], dp[i][j-1] + frame[i-1][j-1]);
// 1.由于我们创建dp数组时,多加了一行和一列,以防止越界情况发生,所以frame要与dp正确对应
// 2.frame[i-1][j-1]是一样的,所以我们可以提取出来
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + frame[i-1][j-1];
初始化
创建dp数组时,多加一行和一列,以防止越界情况发生。
第一行和第一列,可以理解为其左、上方没有珠宝,全部初始化成0。
填表顺序
从上到下,从左到右。
返回值
返回dp[m][n]。
代码演示:
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数组对应!!!
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + frame[i-1][j-1];
return dp[m][n];
}
};
4、下降路径最小和

状态表示:
dp[i][j]:到达(i, j)位置时,下降路径的最小和。
状态转移方程
每一个位置都是从这三个位置得来的:

所以(i, j)位置的最小和,是由(i-1, j-1), (i-1, j), (i-1, j+1)三个位置的最小和的最小值,加上(i, j)位置对应matrix的位置值(matrix[i-1][j-1])的和:
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
−
1
]
[
j
−
1
]
,
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
+
1
]
)
+
m
a
t
r
i
x
[
i
−
1
]
[
j
−
1
]
dp[i][j]=min(dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1])+matrix[i-1][j-1]
dp[i][j]=min(dp[i−1][j−1],dp[i−1][j],dp[i−1][j+1])+matrix[i−1][j−1]
初始化
观察状态转移方程,我们可以发现,如果开辟一个空间排布与matrix数组一样的二维数组,那么第一行、第一列、最后一列会越界:

所以我们加上一列,再加上两行:

对于新增空间,第一行全初始化成0是可以的,因为matrix第一行的每个位置,下降路径的最小和都是这个位置上的数:

对于左右两列,则应该设置成整型的最大值。
因为我们不期望新增的最左、最右两列的数参与和的计算算。
所以我们设置左右两列的值为INT_MAX,而不是0。这样比小时,左右两列的INT_MAX就不会被选上:

填表顺序
从上到下,从左到右。
返回值
这里我们的最小和并不一定是在右下角,而可能是在最后一行的某一位置,所以我们要遍历查找。
代码演示:
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& matrix) {
int m = matrix.size();
// 创建dp表
// 1.多加一行两列
// 2.全部初始化成INT_MAX
// 3.第一行初始化成0
vector<vector<int>> dp(m+1, vector<int>(m+2, INT_MAX));
for (int i = 0; i <= m+1; ++i)
{
dp[0][i] = 0;
}
for (int i = 1; i <= m; ++i)
{
for (int j = 1; j <= m; ++j)
{// 比最小,既可以传两个数,也可以传initializer_list
// 但是前者效率可能更高
//dp[i][j] = matrix[i-1][j-1] + min({dp[i-1][j], dp[i-1][j-1], dp[i-1][j+1]});
dp[i][j] = matrix[i-1][j-1] + min(dp[i-1][j], min(dp[i-1][j-1], dp[i-1][j+1]));
}
}
int min_n = INT_MAX;
for (int i = 1; i <= m; ++i)
{// 比大小的新思路
// 这种不用if的方法,效率可能更高
//if (min_n > dp[m][i])
// min_n = dp[m][i];
min_n = min(min_n, dp[m][i]);
}
return min_n;
}
};
5、最小路径和

我们还是先建立一个二维数组,与grid一样(之后会加行、列)。
状态表示
经验 + 题目要求:
dp[i][j]:从左上角开始,到达(i, j)位置时,路径的最小和。
状态转移方程
题目要求每次只能向下或向右移动一步。所以(i, j)位置的路径的最小和,是由这两个位置得来的:

那么(i, j)位置的dp值,就可以分成两种情况:

那么我们能很快地想出一个递推式:
d
p
[
i
]
[
j
]
=
g
r
i
d
[
i
−
1
]
[
j
−
1
]
+
m
i
n
(
d
p
[
i
]
[
j
−
1
]
,
d
p
[
i
−
1
]
[
j
]
)
dp[i][j]=grid[i-1][j-1]+min(dp[i][j-1], dp[i-1][j])
dp[i][j]=grid[i−1][j−1]+min(dp[i][j−1],dp[i−1][j])
初始化
初始化要注意两点:
- 当前位置的dp值,必须由前一步的dp值正确得出。
- 避免越界。
对于当前的grid网格,我们的做法是多加一行、多加一列。

那么,我们向多加的网格放入什么初始值呢?0?
不对。我们对于当前位置的上面、左边的dp值,需要比小。而grid网格的值都是非负值。意味着如果向多加的网格放入0,那么0就有可能参与计算,但这是我们不希望的。
所以我们放入整型的最大值:INT_MAX。

然后将新网格对应网格的(0, 0)位置的上面,或者左边,初始化成0,以确保左上角的dp值正确:

代码演示:
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[1][0] = 0;
for (int i = 1; i <= m; ++i)
{
for (int j = 1; j <= n; ++j)
{
dp[i][j] = grid[i-1][j-1] + min(dp[i-1][j], dp[i][j-1]);
}
}
return dp[m][n];
}
};
6、地下城游戏

骑士需要从左上角的房间开始,一直到右下角的房间拯救公主。
骑士只能向右或向下。骑士会遇到三种房间:
- 数值为负的房间:有恶魔守卫,骑士会减少对应数值的血量
- 数值为0的房间:什么也没有,相当于通道
- 数值为正的房间:有魔法球,骑士会回复对应数值的的血量
本题要返回骑士能到达最下角所需的最小血量。
状态表示
如果我们定义状态表示为:
dp[i][j]:骑士从左上角到达(i, j)位置,所需的最小血量。
我们很容易就会想成,dp[i][j]会受其上方和左方的影响:

但是这样想,有问题:
对于题目给出的示例1,假设骑士的最低血量为3,对于第一个房间(0, 0),能通过;而对于其右边的房间(0, 1),与下面的房间(1, 0),骑士都不能通过:

这时,骑士的最低血量就必须修改为6,也就意味着,dp[i][j]不仅会受其上方和左方的影响,还会受其下方和左方的影响:

这样就无法进行动态规划了,因为当前位置的右边位置,和下边位置,还没有初始化。
我们不妨换一换思路。
状态表示(新)
dp[i][j]:骑士从(i, j)位置开始直到右下角,所需的最小血量。
状态转移方程
此时骑士的血量,就只受右边位置,和下边位置的影响:

我们很可能就会直接想出这样一个状态转移方程:
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
]
[
j
+
1
]
,
d
p
[
i
+
1
]
[
j
]
)
−
d
u
n
g
e
o
n
[
i
]
[
j
]
dp[i][j]=min(dp[i][j+1], dp[i+1][j])-dungeon[i][j]
dp[i][j]=min(dp[i][j+1],dp[i+1][j])−dungeon[i][j]
但是,如果dungeon[i][j]是一个可以回复很多血量的魔法球,即dungeon[i][j]是一个很大的正值,意味着骑士在(i, j)位置的最低血量就变为了负值。
骑士的血量最小必须是1,所以我们要拿1,与分情况算出的最小血量比谁大:
d
p
[
i
]
[
j
]
=
m
a
x
(
1
,
m
i
n
(
d
p
[
i
]
[
j
+
1
]
,
d
p
[
i
+
1
]
[
j
]
)
−
d
u
n
g
e
o
n
[
i
]
[
j
]
)
dp[i][j]=max(1, min(dp[i][j+1], dp[i+1][j])-dungeon[i][j])
dp[i][j]=max(1,min(dp[i][j+1],dp[i+1][j])−dungeon[i][j])
初始化
由于我们分不同路径讨论时是比谁小,所以我们将dp数组用INT_MAX初始化。
我们在末尾多加一行、多加一列,以防止越界:

骑士来到右下角,血量最小必须是1,所以(m-1, n)和(m, n-1)初始化成1,只要一个地方初始化就行。

填表顺序
从下往上,从右往左
返回值
返回dp[0][0]。
代码演示:
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] = 1;
for (int i = m-1; i >= 0; --i)
for (int j = n-1; j >= 0; --j)
dp[i][j] = max(1, min(dp[i+1][j], dp[i][j+1])-dungeon[i][j]);
return dp[0][0];
}
};
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)