算法:[动态规划]路径问题

目录

题目一:不同路径

题目二:不同路径II

题目三:珠宝的最高价值

题目四:下降路径最小和

题目五:最小路径和

题目六:地下城游戏


题目一:不同路径

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

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

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

示例 1:

输入:m = 3, n = 7
输出:28

示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

输入:m = 7, n = 3
输出:28

示例 4:

输入:m = 3, n = 3
输出:6

依旧是这五步:

①状态表示

经验 + 题目要求

以 [i, j] 为结尾 ......

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

②状态转移方程

根据最近的一步,划分问题

因为只能向右或是向下移动,那么移动到 [i, j] 位置前的位置,一定是上边的或是左边的位置,也就是坐标为:[ i - 1, j] 或 [ i, j - 1]

所以到 [i, j]位置时的方法数,相当于从起点走到上边或左边位置的方法数,因为走到上边或左边后,走一步走到了[i, j]位置

所以状态转移方程:

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

③初始化

因为当前位置需要上面或左边位置的方法数,但是上面或是左边可能会越界,那么此时可以给网格多加一行和一列,这样就可以保证每个格子都存在上面和左边的位置了

所以下面考虑两个细节:

1、虚拟节点的值需要保证后面填表的结果是正确的

这里可不是将所有值全部初始化为0,而是将第一个位置的上面的虚拟节点(或左边的)初始化为1,剩下的各自初始化为0,这样就可以保证第一个位置的结果为1,继而每个位置的结果是正确的

上述的绿色方框就是虚拟出来的结点,红数字就是虚拟节点的值,可以保证填表的结果是正确的

2、下标的映射

映射每个位置都+1即可

④填表顺序

填表顺序从上到下填写,从左到右填写

⑤返回值

因为多开辟了一行一列,所以返回值就是dp[m, n]

代码如下:

class Solution 
{
public:
    int uniquePaths(int m, int n) 
    {
        // 初始化为0
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        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];
    }       
};

题目二:不同路径II

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

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

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

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

示例 1:

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

示例 2:

输入:obstacleGrid = [[0,1],[0,0]]
输出:1

这道题是上一题的升级版,在表格中加上了障碍物,同样是求有多少种方式到右下角

①状态表示

和上一题的状态表示一样:

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

②状态转移方程

如果有障碍物,dp[i][j] = 0

如果没有障碍物,dp[i][j] = dp[i - 1][ j ] + dp[ i ][j - 1]

③初始化

与上一题一样,多加一行多加一列

也是将dp[0][1] = 1,剩下的虚拟位置为0

④填表顺序

同样是从上到下填表,从左到右填表

⑤返回值

同样返回dp[m][n]

代码如下:

class Solution 
{
public:
    int uniquePathsWithObstacles(vector<vector<int>>& ob) 
    {
        int m = ob.size(), n = ob[0].size();
        // 创建dp表
        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++)
            {
                // 如果为0,就说明没有障碍物,执行状态转移方程
                // 如果为1,有障碍物不需要处理,该位置的值即方式数默认为0
                if(ob[i - 1][j - 1] == 0)
                {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }
            }
        }
        return dp[m][n];
    }
};

题目三:珠宝的最高价值

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

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

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

示例 1:

输入: frame = [[1,3,1],[1,5,1],[4,2,1]]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最高价值的珠宝

这道题同样是从左上角移动到右下角,并且只能向右或想下移动

①状态表示

以这个位置为结尾,......

所以dp[i][j]表示从起点到当前位置时,珠宝的最高价值

②状态转移方程

根据最近的一步,划分问题

所以同样,到达某一个位置,只会从上边或是左边的位置移动到当前位置

所以当前位置的最大价值,就是上边或是左边的位置的最大价值,加上当前位置的最大价值

因为要的是最大价值,所以需要找到这两个位置的最大值,再加该位置的价值

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

③初始化

同样可能会出现越界的情况,所以多加一行一列

此时为了保证填表的值是正确的,因为每个位置的价值不可能出现小于0的情况,所以根据上面的状态转移方程,可以知道多加的一行和一列位置为0就可以保证结果正确

下标的映射就需要注意:当想要访问原表时,横纵坐标都需要减1

④填表顺序

填表顺序从上往下填写,从左往右填写

⑤返回值

同样返回值是dp[m][n]

代码如下:

class Solution 
{
public:
    int jewelleryValue(vector<vector<int>>& frame) 
    {
        int m = frame.size(), n = frame[0].size();
        // 创建dp表 + 初始化
        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]) + frame[i-1][j-1];
        return dp[m][n];
    }
};

题目四:下降路径最小和

给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径  最小和 。

下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col) 的下一个元素应当是 (row + 1, col - 1)(row + 1, col) 或者 (row + 1, col + 1) 。

示例 1:

输入:matrix = [[2,1,3],[6,5,4],[7,8,9]]
输出:13
解释:如图所示,为和最小的两条下降路径

示例 2:

输入:matrix = [[-19,57],[-40,-5]]
输出:-59
解释:如图所示,为和最小的下降路径

分为五步解决:

①状态表示

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

②状态转移方程

根据最近的一步

因为可能从三个位置移动到当前位置,分别是:左上角、正上方、右上角

所以分为三种情况:

1、从[i - 1][j - 1]到[i][j]
2、从[i - 1][ j ] 到[i][j]
3、从[i - 1][j + 1]到[i][j]

因为要求的是最小的下降路径,所以就是上述三种情况的最小值,再 + m[i][j] 的值

dp[i][j] = min(情况1,情况2,情况3) + m[i][j]

③初始化

因为每一个位置都可能需要上面的三个位置,所以左边上面右边的一列,都有可能越界,因此还是采用扩容的方式,来保证不越界访问

其中虚线就表示扩容的位置:

因为求的是路径的最小值,所以第一行的虚拟值全部给0,不会影响结果,而从第二行开始,为了不影响到最终的结果,需要将值都填为正无穷大,这样就可以保证肯定不会选到这个位置,从而影响最终的结果,所以最终初始化结果为:

还需要注意下标映射的位置

④填表顺序

因为填每一个位置只需要上面的三个位置,而不需要用到左右位置的数

所以填表顺序是从上往下填

⑤返回值

返回值到最后一行就是最终的路径和了,所以返回值是dp表最后一行的最小值

代码如下:

class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& m) {
        int n = m.size();
        // 创建dp表,默认初始化为+∞
        vector<vector<int>> dp(n + 1, vector<int>(n + 2, INT_MAX));
        // 将第一行虚拟的位置初始化为0
        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(min(dp[i-1][j-1], dp[i-1][j]), dp[i-1][j+1]) + m[i-1][j-1];
        int ret = INT_MAX;
        // 取最后一行dp表中最小的值
        for(int j = 1; j <= n; j++) ret = min(ret, dp[n][j]);
        return ret;
    }
};

题目五:最小路径和

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

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

示例 1:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

示例 2:

输入:grid = [[1,2,3],[4,5,6]]
输出:12

这道题可以说是非常熟悉了,与珠宝的最高价值非常像,那道题是最高价值,这道题是最小路径和,并且也是只能向下或是向右移动一步

①状态表示

以这个位置为结尾,......

所以dp[i][j]表示从起点到当前位置时,最小路径和

②状态转移方程

根据最近的一步,划分问题

所以同样,到达某一个位置,只会从上边或是左边的位置移动到当前位置

所以当前位置的最小路径和,就是上边或是左边的位置的最小路径和,再加上当前位置的路径

因为要的是最小路径和,所以需要找到这两个位置的最小值,再加该位置的路径即可

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

③初始化

同样可能会出现越界的情况,所以多加一行一列

此时为了保证填表的值是正确的,因为每个位置的价值不可能出现小于0的情况,所以根据上面的状态转移方程,可以知道多加的一行和一列位置全部为INT_MAX,并且将第一个位置的上面或是左边置为0就可以保证结果正确

因为此时第一个位置的最小路径和就是它本身的值,而接下来该第二位置时就不会选择上面的值,只会选择第一个位置,因为第二个位置上面是INT_MAX,要求的是最小值,所以只会选择0

下标的映射就需要注意:当想要访问原表时,横纵坐标都需要减1

④填表顺序

填表顺序从上往下填写,从左往右填写

⑤返回值

同样返回值是dp[m][n]

代码如下:

class Solution 
{
public:
    int minPathSum(vector<vector<int>>& g) 
    {
        int m = g.size(), n = g[0].size();
        // 创建dp表,先全部初始化为INT_MAX,将第一个位置上面位置初始化为0即可
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
        dp[0][1] = 0;
        // 填表,需要注意下标的映射问题
        for(int i = 1; i <= m; i++)
            for(int j = 1; j <= n; j++)
                dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + g[i-1][j-1];
        return dp[m][n];    
    }   
};

题目六:地下城游戏

恶魔们抓住了公主并将她关在了地下城 dungeon 的 右下角 。地下城是由 m x n 个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。

骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。

有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。

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

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

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

示例 1:

输入:dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]
输出:7
解释:如果骑士遵循最佳路径:右 -> 右 -> 下 -> 下 ,则骑士的初始健康点数至少为 7 。

示例 2:

输入:dungeon = [[0]]
输出:1

下面依旧分为五步:

①状态表示

经验 + 题目要求

1、以某位置为结尾 .....

此时dp[i][j]表示:从起点出发, 到达[i, j]位置时,所需的最低的初始健康点数

但是这道题中,这种表示方式是不可取的,因为该位置的健康点数不仅受到上面或是左边的点数影响,还会受到下一个位置健康点数的影响,因为这种方式只能根据上面或左边的点数决定,不会考虑到后面位置的影响, 所以排除这种方式

2、以某位置为起点 .....

此时dp[i][j]表示:从[i, j]位置出发, 到达终点时,所需的最低的初始健康点数

这时就能够满足,此时下一步的位置是往下或是往右边移动

②状态转移方程

因为此时的状态表示是以该位置为起点,所以分析如下:

有两种情况,往右边走和往下边走,假设当前的最低健康点数是x,d[i][j]表示当前位置的值,所以需要满足:
x + d[i][j] >= dp[i][j + 1] / x + d[i][j] >= dp[i + 1][j]

x + d[i][j] 就表示能从 [i, j] 位置走出来
>= dp[i][j + 1] 表示走出来后的值还需要能够满足移动到下一个位置的健康点数

所以不等式交换一下得到:x >= dp[i][j + 1] - d[i][j] / x >= dp[i + 1][j] - d[i][j]

所以 dp[i][j] = min(dp[i][j + 1], dp[i + 1][j]) - d[i][j]

但是这里有一种特殊情况,d[i, j] 的值如果非常大,可能走到dp[i, j]位置时是负数,但是加上这个位置的血包,就会变为正数,这种情况也能上面的不等式,但是这是明显不符合题意的,如果走到这个位置时健康值已经为负数了,那么哪怕这个位置血包再大,也会立刻死亡,所以需要考虑到这种情况

上述的情况,在 x >= dp[i][j + 1] - d[i][j] 不等式中,d[i][j] 非常大, x就会变为负数,所以在中途判断时,还需要将dp[i][j]取正数,如果为负数,需要将其置为1,也就是扩大这里的初始健康点数,让骑士能够满足最低血量条件
即:
dp[i][j] = max(1, dp[i][j])

③初始化

这道题因为采取的是以某个位置作为起点,所以就不会在上面或左边越界了, 而是会在下面或右边越界,所以增加的位置是:下面和右边加上一行一列,此时因为在访问原有位置时下标并没有发生改变,所以不需要考虑下标的映射关系了

所以只需要考虑填表的结果是正确的即可

因为原本位置的右下角是公主所在的地方,所以此时走到该位置处的健康值至少得是1,所以在该位置的下边或右边置为1,

而新增的一行一列的其他位置,为了保证移动过程中不会选择到这些位置上的数据, 因为每一次都是找下边或是右边的最小值,只需要保证这些位置的数据都是最大的即可,因为值是INT_MAX时,比较时就不会选择这个位置的数,也就不会影响正常结果,所以初始化时将其余位置全部置为INT_MAX,只有右下角的右边或是下边置为1

④填表顺序

因为每个位置需要考虑到右边或是下面的位置的数据

所以填表顺序就是:从下往上填每一行,每一行从右向左填写

⑤返回值

因为要求的是初始位置的最低健康点数,所以返回值是:dp[0][0]

代码如下:

class Solution 
{
public:
    int calculateMinimumHP(vector<vector<int>>& d) 
    {
        int m = d.size(), n = d[0].size();
        // 创建dp表并初始化
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
        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][j + 1], dp[i + 1][j]) - d[i][j];
                // 如果dp[i][j]为负数就置为1
                dp[i][j] = max(1, dp[i][j]);
            }
        }
        return dp[0][0];
    }
};

关于[动态规划]路径问题的题目到此结束啦

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值