赞
踩
本文将从基础的不同路径问题开始,逐步深入到更复杂的最小路径和等问题,最终探讨DP在其他方面的应用。每个例题都将详细介绍状态表示、状态转移方程、初始化过程、填表顺序和返回值,以确保读者能够清晰地理解DP的核心原理。
一个机器人位于一个
m x n
网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
算法流程:
由于题中所问要从左上角到右下角一共有多少种不同路径,所以二维dp数组的第 i 行第 j 列就直接认定为从左上角到达了该位置一共多少种方式。
dp[i] [j] 表示:走到 [i, j] 位置,一共有多少种方式
由于题目中给定每一步都只能往下走或往右走,所以到达 [i, j] 位置的前一步所在的位置也只能是 [i - 1, j] 和 [i, j - 1],那么我们就可以得到状态转移方程:
dp[i] [j] = dp [i - 1] [j] + dp [i] [j - 1]
当我们考虑从左上角开始对dp数组进行赋值时,由于 [i, j] 位置的值需要用到 [i - 1, j] 和 [i, j - 1] 位置,所以在对第一行和第一列直接使用循环赋值会造成访问越界。于是,给原dp数组上面加一行、左面加一列,这样就能避开对首行首列赋值需要考虑的特殊情况。
由于我们需要先给出起始位置的值和首行首列的值,所以将首行首列所有值都初始化为0,接着将dp[0] [1]或dp[1] [0] 位置的值设为 1 即可。
从左往右、从上往下
根据新dp数组,返回原数组右下角位置值变为 dp[m] [n] 的值。
示例代码:
int uniquePaths(int m, int n) { // 1.构建dp表 dp[i][j]表示到该位置一共多少种不同路径 vector<vector<int>> vv(m + 1, vector<int>(n + 1, 0)); // 2.初始化 为递推赋值准备 vv[0][1] = 1; // 3.填表 for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { vv[i][j] = vv[i - 1][j] + vv[i][j - 1]; } } // 4.返回结果 return vv[m][n]; }
题目链接:63. 不同路径 II - 力扣(LeetCode)
一个机器人位于一个
m x n
网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用
1
和0
来表示。
算法流程:
对于此题要求到达右下角一共有多少种路径,我们不妨采用与地图对应的 dp 数组,那么数组中元素就有了相应的意义
dp[i] [j] 表示:走到 [i, j] 位置处,一共有多少种方式
由于题目中给定每一步都只能往下走或往右走,所以到达 [i, j] 位置的前一步所在的位置也只能是 [i - 1, j] 和 [i, j - 1],但是 [i - 1, j] 和 [i, j - 1] 位置都可能是有障碍物的,从上往下或者从左往右相应都有可能是到不了 [i, j] 位置的,所以此时 [i, j] 的方法数应当为 0,从实际代码中表示时就需要体现判断是否有障碍物这个逻辑,接着才能进行叠加。
同样的在首行和首列添加辅助行列,相应的 i 位置变为 i + 1, j 同理。将新添加的首行和首列直接初始化为 0 ,接着将dp[0] [1]或dp[1] [0] 位置的值设为 1 即可。
从左往右、从上往下
根据新dp数组,返回原数组右下角位置值变为 dp[m] [n] 的值。
示例代码:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) { size_t m = obstacleGrid.size(); size_t n = obstacleGrid.at(0).size(); vector<vector<int>> dp(m + 1, vector<int>(n + 1)); // 初始化 dp[0][1] = 1; for (int i = 1; i < m + 1; i++) { for (int j = 1; j < n + 1; j++) { if (obstacleGrid[i - 1][j - 1] != 1) { dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; } else { dp[i][j] = 0; } } } return dp[m][n]; }
现有一个记作二维矩阵
frame
的珠宝架,其中frame[i][j]
为该位置珠宝的价值。拿取珠宝的规则为:
- 只能从架子的左上角开始拿珠宝
- 每次可以移动到右侧或下侧的相邻位置
- 到达珠宝架子的右下角时,停止拿取
注意:珠宝的价值都是大于 0 的。除非这个架子上没有任何珠宝,比如
frame = [[0]]
。
算法流程:
dp[i] [j]表示:走到 [i, j] 位置处,此时的最大价值。
对于dp[i] [j] 而言,能到达 [i, j] 位置的只有 [i - 1] [j] 和 [i] [j - 1] 两个位置,由于需要到达 [i, j] 位置只能存在一条最大价值路径(金额相同算一条),所以
那么状态转移方程为:
dp[i] [j] = max(dp[i - 1] [j], dp[i] [j - 1]) + grid[i] [j]
由于每个当前位置都与上面、左面位置的值有关系,所以在上面和左面各自新加辅助行和辅助列用于帮助我们赋值。将新添加的首行和首列直接初始化为 0 即可
从左往右、从上往下
根据新dp数组,返回原数组右下角位置值变为 dp[m] [n] 的值。
示例代码:
int jewelleryValue(vector<vector<int>>& frame) { size_t m = frame.size(); size_t n = frame.at(0).size(); vector<vector<int>> dp(m + 1, vector<int>(n + 1)); // 初始化 dp[1][1] = frame[0][0]; // 填表 for (size_t i = 1; i < m + 1; i++) { for (size_t j = 1; j < n + 1; j++) { dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + frame[i - 1][j - 1]; } } return dp[m][n]; }
题目链接:931. 下降路径最小和 - 力扣(LeetCode)
给你一个
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] ,根据题意得,到达 [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[i] [j] 表示最小值,那么我们就应当把新加行列中的值都初始化为无穷大,然后将第一行初始化为 0 即可
从上往下
题目要求只要到达最后一行没有指定具体位置,因此这里应该返回 dp 表中最后一行的最小值
示例代码:
int minFallingPathSum(vector<vector<int>>& matrix) { int m = matrix.size(); int n = matrix.at(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 + 1; i++) { for (int j = 1; j < n + 1; j++) { dp[i][j] = min(min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i - 1][j + 1]) + matrix[i - 1][j - 1]; } } // 取dp表最后一行的最小值 int min = INT_MAX; for (int j = 1; j < n + 1; j++) { min = dp[m][j] < min ? dp[m][j] : min; } return min; }
给定一个包含非负整数的
*m* x *n*
网格grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。**说明:**每次只能向下或者向右移动一步。
算法流程:
到达 [i, j] 位置时,所有从左上角到该位置的路径中的最小和。
对于普遍位置 [i, j] ,由于前一个位置到当前 [i, j] 位置只有从上向下和从左往右两种可能,所以到达该位置可能有两种情况:
我们所要的是上面两种情况下的最小值,再加上自身 [i, j] 位置矩阵的对应值,所以状态转移方程:
dp[i] [j] = min(dp[i] [j - 1], dp[i - 1] [j]) + grid[i] [j]
在首行和首列添加辅助行列,相应的 i 位置变为 i + 1, j 同理。由于求最小和,为了避免辅助行列中的值对后续填表赋值产生负面影响,将新添加的首行和首列直接初始化为 INT_MAX,接着将 dp[0] [1] 和 dp[1] [0] 位置的值都设为 0 即可。
从左上角到右下角
返回新 dp 表中最后一个元素的值,即 dp[m] [n],它代表了从左上角到右下角的最小路径和。
示例代码:
int minPathSum(vector<vector<int>>& grid) { int m = grid.size(); int n = grid.at(0).size(); // 初始化 vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX)); // dp数组内的值由min()决定,不能受辅助方格的影响,所以辅助方格初始化为极大值 dp[1][0] = dp[0][1] = 0; // 为了正确初始化dp[1][1]的值 for (int i = 1; i < m + 1; i++) { for (int j = 1; j < n + 1; j++) { dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1]; } } return dp[m][n]; }
题目链接:地下城游戏
恶魔们抓住了公主并将她关在了地下城
dungeon
的 右下角 。地下城是由m x n
个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快解救公主,骑士决定每次只 向右 或 向下 移动一步。
返回确保骑士能够拯救到公主所需的最低初始健康点数。
注意: 任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
算法流程:
到达 [i, j] 位置时,骑士为了能够继续前进至少需要的健康点数
对于普遍位置 [i, j],骑士到达该位置可能有两种情况:
我们所要的是上面两种情况下所需健康点数的较小值,再减去当前房间的将要扣除的血量值(如果是负数则设为1),所以状态转移方程:
dp[i] [j] = min( dp[i] [j - 1], dp[i - 1] [j] ) - dungeon[i] [j]
由于骑士每次只向右或者向下行进且其自身健康点数不能小于等于0,所以我们将辅助行和辅助列初始化为无穷大,以确保骑士在任何情况下都不会因为这些辅助值而死亡。同时,将右下角的初始值设为1,表示骑士至少需要1点健康点数才能拯救公主。
从下往上,从右往左
由于骑士是从左上角出发,所以我们需要返回 dp[0] [0],即骑士在出发点至少需要的健康点数
示例代码:
int calculateMinimumHP(vector<vector<int>>& dungeon) { int m = dungeon.size(); int n = dungeon.at(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]; }
通过本文的学习,我们了解了DP的基本组成部分,包括状态的定义、状态转移的逻辑、初始化的重要性、填表顺序的确定以及最终结果的获取。我们还探讨了DP在不同路径、最小/最优路径选择以及其他相关问题中的应用。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。