算法导论(动态规划)——路径问题
算法思路(62)
-
状态表示:
在解决“路径类”问题时,常见的状态表示形式有两种:- 形式一:从位置 [i,j] 出发的路径计数。
- 形式二:从起始位置到达位置 [i,j] 的路径计数。
本文选择第二种形式来定义状态:设 dp[i][j] 表示到达位置 [i,j] 的路径总数。
-
状态转移方程:
根据状态的定义,若 dp[i][j] 表示到达位置 [i,j] 的路径数,考虑到达该位置的方式,可以得到以下两种可能情况:- 从上方位置 [i−1,j] 向下移动一格;
- 从左方位置 [i,j−1] 向右移动一格。
因此,状态转移方程可表示为:
dp[i][j]=dp[i−1][j]+dp[i][j−1] -
初始化:
为了便于后续的状态填表,建议在状态表的前面添加一行和一列作为辅助节点。这里有两个关键点需注意:- 值的设置:确保辅助节点的值正确,以保证后续的填表结果正确。
- 下标映射:添加行列后,实际填表时需注意对应的下标映射。
在本题中,我们可以在状态表中插入一行和一列,并将 dp[0][1] 初始化为 1,以确保后续填表过程顺利进行。
-
填表顺序:
根据状态转移方程的推导,填表顺序应为:- 从上到下逐行填充;
- 在填充每一行时,从左到右逐列填充。
-
返回值:
最终结果即为到达位置 [m,n] 的路径数量,返回 dp[m][n] 的值即可。
C++:
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];
}
};
Java:
class Solution
{
public int uniquePaths(int m, int n)
{
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回值
int[][] dp = new int[m + 1][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];
}
}
算法思路(63)
本题为不同路径问题的变种,需考虑障碍物的影响,稍加修改即可求解。以下是详细的算法思路:
-
状态表示:
在解决“路径类”问题时,常见的状态表示形式有两种:- 形式一:从位置 [i,j] 出发的路径数量。
- 形式二:从起始位置到达位置 [i,j] 的路径数量。
本文采用第二种形式来定义状态:令 dp[i][j] 表示到达位置 [i,j] 的路径总数。
-
状态转移:
通过分析可得,若dp[i][j]表示达到位置[i,j]的路径数量,则可通过以下两种方式到达该位置:- 从上方位置 [i−1,j] 向下移动;
- 从左侧位置 [i,j−1] 向右移动。
然而,需注意如果上方或左侧位置存在障碍物,则无法到达 [i,j][i,j] 位置,此时对应的路径数应设为0。因此我们可以得出结论:
- 如果位置 [i,j] 有障碍物,那么 dp[i][j]=0;否则,状态转移方程为:
-
初始化:
为了便于后续状态表的填充,建议在表的前面添加一行和一列以作为辅助节点。以下是需注意的要点:- 辅助节点值的设置:确保辅助节点值的设置能够保证后续计算的正确性。
- 下标映射:添加的行列后,需特别关注下标的正确映射。
在本题中,添加一行和一列后,将 dp[1][0] 初始化为 1,表示从起始位置出发的路径数。
-
填表顺序:
根据状态转移公式的推导,填表应按照以下顺序进行:- 从上到下逐行填充;
- 在填充每一行时,从左到右逐列填充。
-
返回值:
最后,返回 dp[m][n] 的值,以获取结束位置的路径数量。
C++:
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& ob) {
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回值
int m = ob.size(), n = ob[0].size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
dp[1][0] = 1;
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
if(ob[i - 1][j - 1] == 0)
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
return dp[m][n];
}
};
Java:
class Solution
{
public int uniquePathsWithObstacles(int[][] ob)
{
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回值
int m = ob.length, n = ob[0].length;
int[][] dp = new int[m + 1][n + 1];
dp[1][0] = 1;
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
if(ob[i - 1][j - 1] == 0)
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
return dp[m][n];
}
}
算法思路(166)
-
状态表示:
在处理这类“路径类”问题时,状态表示通常有两种形式:
- 形式一:从位置 [i,j] 出发的路径数。
- 形式二:从起始位置出发到达位置 [i,j] 的路径数。
本题中,我们采用第二种形式进行状态定义,即:
dp[i][j] 表示到达位置 [i,j] 时所能获得的最大价值。 -
状态转移方程:
当我们考虑 dp[i][j] 的值时,可以得出以下两种到达 [i,j] 位置的方式:
- 从上方:通过位置 [i−1,j] 向下移动,能获得的礼物价值为:dp[i−1][j]+grid[i][j]
- 从左侧:通过位置 [i,j−1] 向右移动,能获得的礼物价值为:dp[i][j−1]+grid[i][j]
因此,为了获得最大价值,状态转移方程可以表示为:
dp[i][j]=max(dp[i−1][j],dp[i][j−1])+grid[i][j] -
初始化:
为了初始化状态表,可以在状态表的前方添加一行及一列作为辅助节点。在进行此操作时,应注意以下两个关键点:
- 辅助节点值的设置:确保辅助节点内的值能够保证后续填表时的正确性。
- 下标映射关系:添加行列后,务必关注下标映射的变化。
在本题中,添加一行和一列后,可以将所有值初始化为 0。
-
填表顺序:
根据状态转移方程,填表的顺序为:
- 逐行从上到下进行填充;
- 逐列从左到右进行填充。
-
返回值:
根据状态表的定义,最终返回的结果为:
dp[m][n]该值表示从起始位置到达位置 [m,n] 时能够获得的最大价值。
C++:
class Solution
{
public:
int maxValue(vector<vector<int>>& grid)
{
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回结果
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];
}
};
Java:
class Solution
{
public int maxValue(int[][] grid)
{
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回值
int m = grid.length, n = grid[0].length;
int[][] dp = new int[m + 1][n + 1];
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]) + grid[i - 1]
[j - 1];
return dp[m][n];
}
}
算法思路(931)
对于这类路径问题,由于我们先前已处理过类似题型,因此“状态表示”和“状态转移”相对容易分析。相对而言,比较复杂的地方在于“边界条件”的处理。以下是详细的算法思路:
-
状态表示:
在解决此类“路径类”问题时,通常采用两种状态表示形式:
- 形式一:从位置 [i,j] 出发,到达目标位置的路径数量。
- 形式二:从起始位置出发,到达位置 [i,j] 的路径数量。
本题选择第二种形式进行状态定义:
dp[i][j] 表示到达位置 [i,j] 时,所有下降路径中的最小和。 -
状态转移方程:
对于一般位置 [i,j],根据题意,达到该位置可能有三种情况:
- 从正上方位置 [i−1,j] 转移到 [i,j];
- 从左上方位置 [i−1,j−1] 转移到 [i,j];
- 从右上方位置 [i−1,j+1] 转移到 [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] -
初始化:
在状态表的前面添加辅助节点以便初始化。在进行此操作时,需要注意以下两个要点:
- 辅助节点值的设置:确保辅助节点中的值能保证后续填表时的正确性。
- 下标映射关系:在本题中,需要添加一行和两列作为辅助节点。所有新位置的值初始设为无穷大(∞),而第一行的值则初始化为0。
-
填表顺序:
根据状态表示,我们需按顺序以“从上到下”的方式填充状态表。
-
返回值:
请注意,题目并不要求返回 dp[m][n] 的值。题意是要求到达最后一行即可,因此我们需要返回“状态表中最后一行的最小值”,即:
min(dp[最后一行][j])
C++:
class Solution
{
public:
int minFallingPathSum(vector<vector<int>>& matrix)
{
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回结果
int n = matrix.size();
vector<vector<int>> dp(n + 1, vector<int>(n + 2, INT_MAX));
// 初始化第⼀⾏
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(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[n][j]);
return ret;
}
};
Java:
class Solution
{
public int minFallingPathSum(int[][] matrix)
{
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回结果
int n = matrix.length;
int[][] dp = new int[n + 1][n + 2];
for(int i = 1; i <= n; i++) dp[i][0] = dp[i][n + 1] =
Integer.MAX_VALUE;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
dp[i][j] = Math.min(dp[i - 1][j], Math.min(dp[i - 1][j - 1],
dp[i - 1][j + 1])) + matrix[i - 1][j - 1];
int ret = Integer.MAX_VALUE;
for(int j = 1; j <= n; j++)
ret = Math.min(ret, dp[n][j]);
return ret;
}