【动态规划】路径问题模型

【动态规划】路径问题模型

前言

​ 本文将从基础的不同路径问题开始,逐步深入到更复杂的最小路径和等问题,最终探讨DP在其他方面的应用。每个例题都将详细介绍状态表示、状态转移方程、初始化过程、填表顺序和返回值,以确保读者能够清晰地理解DP的核心原理。


一、不同路径

题目链接:62. 不同路径 - 力扣(LeetCode)

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

算法流程:

  1. 状态表示

由于题中所问要从左上角到右下角一共有多少种不同路径,所以二维dp数组的第 i 行第 j 列就直接认定为从左上角到达了该位置一共多少种方式。

dp[i] [j] 表示:走到 [i, j] 位置,一共有多少种方式

  1. 状态转移方程

由于题目中给定每一步都只能往下走或往右走,所以到达 [i, j] 位置的前一步所在的位置也只能是 [i - 1, j] 和 [i, j - 1],那么我们就可以得到状态转移方程:

dp[i] [j] = dp [i - 1] [j] + dp [i] [j - 1]

  1. 初始化

当我们考虑从左上角开始对dp数组进行赋值时,由于 [i, j] 位置的值需要用到 [i - 1, j] 和 [i, j - 1] 位置,所以在对第一行和第一列直接使用循环赋值会造成访问越界。于是,给原dp数组上面加一行、左面加一列,这样就能避开对首行首列赋值需要考虑的特殊情况。

在这里插入图片描述

由于我们需要先给出起始位置的值和首行首列的值,所以将首行首列所有值都初始化为0,接着将dp[0] [1]或dp[1] [0] 位置的值设为 1 即可。

  1. 填表顺序

从左往右、从上往下

  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];
}

二、不同路径-2

题目链接:63. 不同路径 II - 力扣(LeetCode)

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 10 来表示。

算法流程:

  1. 状态表示

对于此题要求到达右下角一共有多少种路径,我们不妨采用与地图对应的 dp 数组,那么数组中元素就有了相应的意义

dp[i] [j] 表示:走到 [i, j] 位置处,一共有多少种方式

  1. 状态转移方程

由于题目中给定每一步都只能往下走或往右走,所以到达 [i, j] 位置的前一步所在的位置也只能是 [i - 1, j] 和 [i, j - 1],但是 [i - 1, j] 和 [i, j - 1] 位置都可能是有障碍物的,从上往下或者从左往右相应都有可能是到不了 [i, j] 位置的,所以此时 [i, j] 的方法数应当为 0,从实际代码中表示时就需要体现判断是否有障碍物这个逻辑,接着才能进行叠加。

  1. 初始化

同样的在首行和首列添加辅助行列,相应的 i 位置变为 i + 1, j 同理。将新添加的首行和首列直接初始化为 0 ,接着将dp[0] [1]或dp[1] [0] 位置的值设为 1 即可。

  1. 填表顺序

从左往右、从上往下

  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];
}

三、珠宝的最高价值

题目链接:LCR 166. 珠宝的最高价值 - 力扣

现有一个记作二维矩阵 frame 的珠宝架,其中 frame[i][j] 为该位置珠宝的价值。拿取珠宝的规则为:

  • 只能从架子的左上角开始拿珠宝
  • 每次可以移动到右侧或下侧的相邻位置
  • 到达珠宝架子的右下角时,停止拿取

注意:珠宝的价值都是大于 0 的。除非这个架子上没有任何珠宝,比如 frame = [[0]]

算法流程:

  1. 状态表示

dp[i] [j]表示:走到 [i, j] 位置处,此时的最大价值。

  1. 状态转移方程

对于dp[i] [j] 而言,能到达 [i, j] 位置的只有 [i - 1] [j] 和 [i] [j - 1] 两个位置,由于需要到达 [i, j] 位置只能存在一条最大价值路径(金额相同算一条),所以

  • 从 [i - 1, j] 到 [i, j] 位置的价值:dp[i - 1] [j] + grid[i] [j]
  • 从 [i, j - 1] 到 [i, j] 位置的价值:dp[i] [j - 1] + grid[i] [j]

那么状态转移方程为:

dp[i] [j] = max(dp[i - 1] [j], dp[i] [j - 1]) + grid[i] [j]

  1. 初始化

由于每个当前位置都与上面、左面位置的值有关系,所以在上面和左面各自新加辅助行和辅助列用于帮助我们赋值。将新添加的首行和首列直接初始化为 0 即可

  1. 填表顺序

从左往右、从上往下

  1. 返回值

根据新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)

算法流程:

  1. 状态表示

dp[i] [j] 表示:到达 [i, j] 位置时,所有下降路径中的最小和。

  1. 状态转移方程

对于普遍位置 [i, j] ,根据题意得,到达 [i, j] 位置可能有三种情况:

  • 从正上方 [i - 1, j] 位置转移到 [i, j] 位置;
  • 从左上方 [i - 1, j - 1] 位置转移到 [i, j] 位置;
  • 从右上方 [i - 1, j + 1] 位置转移到 [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]

  1. 初始化

由于每个当前位置都与上面三个位置(正上方、左上方、右上方)的值有关系,所以在上面和两侧各自新加辅助行和辅助列用于帮助我们赋值,即增加一行两列。因为我们既定的dp[i] [j] 表示最小值,那么我们就应当把新加行列中的值都初始化为无穷大,然后将第一行初始化为 0 即可

在这里插入图片描述

  1. 填表顺序

从上往下

  1. 返回值

题目要求只要到达最后一行没有指定具体位置,因此这里应该返回 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;
}

五、最小路径和

题目链接:64. 最小路径和 - 力扣(LeetCode)

给定一个包含非负整数的 *m* x *n* 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

**说明:**每次只能向下或者向右移动一步。

算法流程:

  1. 状态表示

到达 [i, j] 位置时,所有从左上角到该位置的路径中的最小和。

  1. 状态转移方程

对于普遍位置 [i, j] ,由于前一个位置到当前 [i, j] 位置只有从上向下和从左往右两种可能,所以到达该位置可能有两种情况:

  • 从左边 [i, j - 1] 位置转移到 [i, j] 位置;
  • 从上方 [i - 1, j] 位置转移到 [i, j] 位置;

我们所要的是上面两种情况下的最小值,再加上自身 [i, j] 位置矩阵的对应值,所以状态转移方程:

dp[i] [j] = min(dp[i] [j - 1], dp[i - 1] [j]) + grid[i] [j]

  1. 初始化

在首行和首列添加辅助行列,相应的 i 位置变为 i + 1, j 同理。由于求最小和,为了避免辅助行列中的值对后续填表赋值产生负面影响,将新添加的首行和首列直接初始化为 INT_MAX,接着将 dp[0] [1] 和 dp[1] [0] 位置的值都设为 0 即可。

  1. 填表顺序

从左上角到右下角

  1. 返回值

返回新 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),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。

为了尽快解救公主,骑士决定每次只 向右向下 移动一步。

返回确保骑士能够拯救到公主所需的最低初始健康点数。

注意: 任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。

算法流程:

  1. 状态表示

到达 [i, j] 位置时,骑士为了能够继续前进至少需要的健康点数

  1. 状态转移方程

对于普遍位置 [i, j],骑士到达该位置可能有两种情况:

  • 从左边 [i, j - 1] 位置转移到 [i, j] 位置;
  • 从上方 [i - 1, j] 位置转移到 [i, j] 位置;

我们所要的是上面两种情况下所需健康点数的较小值,再减去当前房间的将要扣除的血量值(如果是负数则设为1),所以状态转移方程:

dp[i] [j] = min( dp[i] [j - 1], dp[i - 1] [j] ) - dungeon[i] [j]

  1. 初始化

由于骑士每次只向右或者向下行进且其自身健康点数不能小于等于0,所以我们将辅助行和辅助列初始化为无穷大,以确保骑士在任何情况下都不会因为这些辅助值而死亡。同时,将右下角的初始值设为1,表示骑士至少需要1点健康点数才能拯救公主。

在这里插入图片描述

  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在不同路径、最小/最优路径选择以及其他相关问题中的应用。

在这里插入图片描述

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

螺蛳粉只吃炸蛋的走风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值