【LeetCode】--- 动态规划 集训(二)

简介: 【LeetCode】--- 动态规划 集训(二)

一、63. 不同路径 II

题目地址: 不同路径 II


一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

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

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

输出:2

解释:3x3 网格的正中间有一个障碍物。

从左上角到右下角一共有 2 条不同的路径:

  1. 向右 -> 向右 -> 向下 -> 向下
  2. 向下 -> 向下 -> 向右 -> 向右

1.1 题目解析

状态表示:

对于这种「路径类」的问题,我们的状态表示⼀般有两种形式

  1. [i, j]位置出发,到达⽬标位置有多少种方式;
  2. 从起始位置出发,到达 [i, j]位置,,⼀共有多少种方式。

这⾥选择第⼆种定义状态表示的方式:dp[i][j]表示:走到 [i, j]位置处,⼀共有多少种方式。

状态转移:

简单分析⼀下。如果 dp[i][j]表示到达 [i, j]位置的方法数,那么到达 [i, j]位置之前的⼀小步,有两种情况:

  1. 从 [i, j]位置的上方( [i - 1, j]的位置)向下走一步,转移到 [i, j]位置;
  2. 从 [i, j]位置的左方( [i, j - 1]的位置)向右⾛⼀步,转移到 [i, j]位置。

但是, [i - 1, j][i, j - 1]位置都是可能有障碍的,此时从上⾯或者左边是不可能到达 [i, j]位置的,也就是说,此时的方法数应该是 0。由此我们可以得出⼀个结论,只要这个位置上「有障碍物」,那么我们就不需要计算这个位置上的值,直接让它等于 0 即可。

初始化:

可以在最前⾯加上⼀个「辅助结点」,帮助我们初始化。使⽤这种技巧要注意两个点:

  1. 辅助结点⾥⾯的值要「保证后续填表是正确的」;
  2. 下标的映射关系」。

在本题中,添加一行,并且添加⼀列后,只需将 dp[1][0]的位置初始化为 1即可。 添加一行和一列是因为[i, j]位置需要[i - 1, j][i, j - 1]两个方位的值,以此确保状态表不越界,将dp[1][0]初始化为1,为了保证在起点时有方法数 1 。

填表顺序:

根据「状态转移」的推导,填表的顺序就是 「从上往下」填每一行,每一行「从左往右」。

返回值:

根据「状态表⽰」,我们要返回的结果是 dp[m][n]

1.2 状态转移方程

从当前位置的上方和左方,到达当前位置的方法数:dp[i][j] = dp[i - 1][j] + dp[i][j - 1];。还需要注意的是若当前位置为障碍物,直接将当前状态置为0(即dp[i][j] = 0;)

1.3 解题代码

class Solution 
{
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) 
    {
        int row = obstacleGrid.size(), col = obstacleGrid[0].size();
        //1. 创建dp表
        vector<vector<int>> dp(row + 1, vector<int>(col + 1));
        //2. 初始化
        dp[0][1] = 1;
        //3. 填表
        //从上到下填表 -> 从左到右填表
        for (int i = 1; i <= row; ++i)
            for (int j = 1; j <= col; ++j)
                if(obstacleGrid[i - 1][j - 1] == 0) dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
        //4. 返回值
        return dp[row][col];
    }
};

二、931. 下降路径最小和

题目地址: 931. 下降路径最小和


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

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

输入:matrix = [[2,1,3],[6,5,4],[7,8,9]]

输出:13

解释:如图所示,为和最小的两条下降路径

2.1 题目解析

状态表示:

对于这种「路径类」的问题,我们的状态表示一般有两种形式:

  1. 从 [i, j] 位置出发,到达⽬标位置有多少种方式;
  2. 从起始位置出发,到达 [i, j] 位置,⼀共有多少种方式

这里选择第⼆种定义状态表示的方式:

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

状态转移:

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

  1. 从正上方 [i - 1, j]位置转移到 [i, j]位置;
  2. 从左上方 [i - 1, j - 1]位置转移到 [i, j]位置;
  3. 从右上方 [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. 辅助结点里面的值要「保证后续填表是正确的」;
  2. 下标的映射关系」。

事实上这题的状态转移方程是不难想到的,而关键问题在于初始化。[i, j]位置的值可以从三个方向来 - 上、左、右,所以为了不越界,在本题中,需要「加上一行」,并且「加上两列」(最左边和最右边)。 所有的位置都初始化为无穷大,然后将第一行初始化为 0 即可。将初始值设为无穷大是因为,这些是虚拟节点,不应该对选数造成影响(即大于其他两路的值)。

填表顺序:

根据「状态表示」,填表的顺序是「从上往下」。

返回值:

注意这里不是返回 dp[m][n]的值!

题⽬要求「只要到达最后一行」就行了,因此这⾥应该返回「 dp 表中最后一行的最小值」。

2.2 状态转移方程

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

2.3 解题代码

class Solution 
{
public:
    int minFallingPathSum(vector<vector<int>>& matrix) 
    {
        //1. 创建dp表
        int row = matrix.size(), col = matrix.size();
        vector<vector<int>> dp(row + 1, vector<int>(col + 2, INT_MAX));
        //2. 初始化列表
        //第一行变为0
        for(int i = 0; i < col + 2; ++i) dp[0][i] = 0;
        //3. 填表
        for(int i = 1; i <= row; ++i)
            for(int j = 1; j <= col; ++j)
                dp[i][j] = matrix[i - 1][j - 1] 
                             + min(min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i - 1][j + 1]);
        //4. 返回结果
        int ans = INT_MAX;
        for(int i = 1; i <= col; ++i)
            ans = min(ans, dp[row][i]);
        return ans;
    }
};

三、174. 地下城游戏

题目地址: 174. 地下城游戏


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


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

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

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

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

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

输入:dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]

输出:7

解释:如果骑士遵循最佳路径:右 -> 右 -> 下 -> 下 ,则骑士的初始健康点数至少为 7 。

3.1 题目解析

状态表示:

这道题如果我们定义成:从起点开始,到达 [i, j]位置的时候,所需的最低初始健康点数。那么我们分析状态转移的时候会有⼀个问题那就是我们当前的健康点数还会受到后面的路径的影响。也就是从上往下的状态转移不能很好地解决问题。

这个时候我们要换⼀种状态表示:[i, j]位置出发,到达终点时所需要的最低初始健康点数。 这样我们在分析状态转移的时候,后续的最佳状态就已经知晓。

综上所述,定义状态表示为:

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

状态转移方程:

对于 dp[i][j],从 [i, j]位置出发,下⼀步会有两种选择(为了方便理解,设 dp[i][j]的最终答案是 x ):

  1. 走到右边,然后走向终点:那么我们在 [i, j]位置的最低健康点数加上这⼀个位置的消耗,应该要⼤于等于右边位置的最低健康点数,也就是: x + dungeon[i][j] >= dp[i][j + 1]。通过移项可得: x >= dp[i][j + 1] - dungeon[i][j]。因为我们要的是最小值,因此这种情况下的 x = dp[i][j + 1] - dungeon[i][j];
  2. 走到下边,然后走向终点:那么我们在 [i, j]位置的最低健康点数加上这⼀个位置的消耗,应该要大于等于下边位置的最低健康点数,也就是: x + dungeon[i][j] >= dp[i + 1][j]。通过移项可得: x >= dp[i + 1][j] - dungeon[i][j]。因为我们要的是最小值,因此这种情况下的 x = dp[i + 1][j] - dungeon[i][j];

综上所述,我们需要的是两种情况下的最小值,因此可得状态转移方程为:dp[i][j] = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]

但是,如果当前位置的 dungeon[i][j]是⼀个比较大的正数的话, dp[i][j]的值可能变成 0 或者负数。也就是最低点数会小于 1 ,那么骑士就会死亡。因此我们求出来的 dp[i][j]如果小于等于 0 的话,说明此时的最低初始值应该为 1 。处理这种情况仅需让 dp[i][j]与 1 取⼀个最大值即可:dp[i][j] = max(1, dp[i][j])

初始化:

可以在最前⾯加上⼀个「辅助结点」,帮助我们初始化。使用这种技巧要注意两个点:

  1. 辅助结点⾥⾯的值要「保证后续填表是正确的」;
  2. 「下标的映射关系」。

在本题中,在 dp 表最后⾯添加一行,并且添加⼀列后,所有的值都先初始化为⽆穷大,然后让dp[m][n - 1] = dp[m - 1][n] = 1即可。

填表顺序:

根据「状态转移方程」,我们需要「从下往上填每一行」,「每一行从右往左」

返回值:

根据「状态表示」,我们需要返回 dp[0][0]的值。

3.2 状态转移方程

骑士到达当前位置最低健康点数:dp[i][j] = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]

确保健康值不低于1:dp[i][j] = max(1, dp[i][j]);

3.3 解题代码

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] = 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];
                dp[i][j] = max(1, dp[i][j]);
            }
        }
        // 返回结果
        return dp[0][0];
    }
};


目录
相关文章
|
6月前
|
缓存
力扣每日一题 6/14 动态规划+数组
力扣每日一题 6/14 动态规划+数组
41 1
|
6月前
|
算法 索引
力扣每日一题 6/28 动态规划/数组
力扣每日一题 6/28 动态规划/数组
55 0
|
6月前
|
存储
力扣每日一题 6/19 排序+动态规划
力扣每日一题 6/19 排序+动态规划
34 0
|
6月前
|
算法
力扣每日一题 6/16 字符串 + 随机一题 动态规划/数学
力扣每日一题 6/16 字符串 + 随机一题 动态规划/数学
48 0
|
6月前
【LeetCode】--- 动态规划 集训(一)
【LeetCode】--- 动态规划 集训(一)
36 0
【42页动态规划学习笔记分享】动态规划核心原理详解及27道LeetCode相关经典题目汇总(4)
【42页动态规划学习笔记分享】动态规划核心原理详解及27道LeetCode相关经典题目汇总
|
6月前
|
存储 算法
【42页动态规划学习笔记分享】动态规划核心原理详解及27道LeetCode相关经典题目汇总(1)
【42页动态规划学习笔记分享】动态规划核心原理详解及27道LeetCode相关经典题目汇总
|
6月前
|
存储 算法 数据挖掘
力扣174题动态规划:地下城游戏(含模拟面试)
力扣174题动态规划:地下城游戏(含模拟面试)
|
3月前
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
|
4月前
|
搜索推荐 索引 Python
【Leetcode刷题Python】牛客. 数组中未出现的最小正整数
本文介绍了牛客网题目"数组中未出现的最小正整数"的解法,提供了一种满足O(n)时间复杂度和O(1)空间复杂度要求的原地排序算法,并给出了Python实现代码。
123 2