专题二:不同路径

> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:了解什么是记忆化搜索,并且掌握记忆化搜索算法。

> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!

> 专栏选自:动态规划算法_დ旧言~的博客-CSDN博客

> 望小伙伴们点赞👍收藏✨加关注哟💕💕

一、算法讲解

动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法:

  • 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
  • 与分治法不同的是,适合于用动态规划求解的问题,经分解得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上)。

【Tips】动态规划算法解决问题的分类

  • 计数:有多少种方式走到右下角 / 有多少种方法选出k个数使得和是 sum。
  • 求最大值/最小值:从左上角走到右下角路径的最大数字和最长上升子序列长度。
  • 求存在性:取石子游戏,先手是否必胜 / 能不能取出 k 个数字使得和是 sum。

【Tips】动态规划dp算法一般步骤

  • 确定状态表示(dp[ i ] 的含义是什么,来源:1、题目要求;2、经验+题目要求;3、分析问题时发现重复子问题)
  • 状态转移方程(可求得 dp[ i ] 的数学公式,来源:题目要求+状态表示)
  • 初始化(dp 表中特别的初始值,保证填 dp 表时不会越界,来源:题目要求+状态表示)
  • 填表顺序(根据状态转移方程修改 dp[ i ] 的方式,来源:题目要求+状态表示)
  • 返回值(题目求解的结果,来源:题目要求+状态表示)

 二、算法习题

2.1 第一题

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

题目描述:

算法思路:

1. 状态表⽰:

dp[i][j] 表⽰:⾛到 [i, j] 位置处,⼀共有多少种⽅式。

2. 状态转移⽅程:

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

  1. 从 [i, j] 位置的上⽅( [i - 1, j] 的位置)向下⾛⼀步,转移到 [i, j] 位置;
  2. 从 [i, j] 位置的左⽅( [i, j - 1] 的位置)向右⾛⼀步,转移到 [i, j] 位置。
  3. 由于我们要求的是有多少种⽅法,因此状态转移⽅程就呼之欲出了: dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 。

3. 初始化:

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

  1. 辅助结点⾥⾯的值要「保证后续填表是正确的」;
  2. 「下标的映射关系」。
  3. 在本题中,「添加⼀⾏」,并且「添加⼀列」后,只需将 dp[0][1] 的位置初始化为 1 即可。

4. 填表顺序:

根据「状态转移⽅程」的推导来看,填表的顺序就是「从上往下」填每⼀⾏,在填写每⼀⾏的时候
「从左往右」。

5. 返回值:

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

代码呈现:

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

2.2 第二题

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

题目描述:

算法思路:

本题为不同路径的变型,只不过有些地⽅有「障碍物」,只要在「状态转移」上稍加修改就可解决。
1. 状态表⽰:

dp[i][j] 表⽰:⾛到 [i, j] 位置处,⼀共有多少种⽅式。

2. 状态转移:

简单分析⼀下。如果 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 即可。

3. 初始化:

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

  1. 辅助结点⾥⾯的值要「保证后续填表是正确的」;
  2. 「下标的映射关系」。
  3. 在本题中,添加⼀⾏,并且添加⼀列后,只需将 dp[1][0] 的位置初始化为 1 即可。

4. 填表顺序:

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

5. 返回值:

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

代码呈现:

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

2.3 第三题

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

题目描述:

算法思路:

1. 状态表⽰:

dp[i][j] 表⽰:⾛到 [i, j] 位置处,此时的最⼤价值。

2. 状态转移⽅程:

对于 dp[i][j] ,我们发现想要到达 [i, j] 位置,有两种⽅式:

  1. 从 [i, j] 位置的上⽅ [i - 1, j] 位置,向下⾛⼀步,此时到达 [i, j] 位置能拿到的礼物价值为 dp[i - 1][j] + grid[i][j] ;
  2. 从 [i, j] 位置的左边 [i, j - 1] 位置,向右⾛⼀步,此时到达 [i, j] 位置能拿到的礼物价值为 dp[i][j - 1] + grid[i][j]
  3. 我们要的是最⼤值,因此状态转移⽅程为:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j] 。

3. 初始化:

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

  1. 辅助结点⾥⾯的值要「保证后续填表是正确的」;
  2. 「下标的映射关系」。
  3. 在本题中,「添加⼀⾏」,并且「添加⼀列」后,所有的值都为 0 即可。

4. 填表顺序:

根据「状态转移⽅程」,填表的顺序是「从上往下填写每⼀⾏」,「每⼀⾏从左往右」。

5. 返回值:

根据「状态表⽰」,我们应该返回 dp[m][n] 的值。

代码呈现:

class Solution {
public:
    int jewelleryValue(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];
    }
};

2.4 第四题

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

题目描述:

算法思路:

关于这⼀类题,由于我们做过类似的,因此「状态表⽰」以及「状态转移」是⽐较容易分析出来的。⽐较难的地⽅可能就是对于「边界条件」的处理。

1. 状态表⽰:

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

2. 状态转移⽅程:

对于普遍位置 [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] 。

3. 初始化:

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

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

在本题中,需要「加上⼀⾏」,并且「加上两列」。所有的位置都初始化为⽆穷⼤,然后将第⼀⾏
初始化为 0 即可。

4. 填表顺序:

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

代码呈现:

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

2.5 第五题

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

题目描述:

算法思路:

像这种表格形式的动态规划,是⾮常容易得到「状态表⽰」以及「状态转移⽅程」的,可以归结到
「不同路径」⼀类的题⾥⾯。

1. 状态表⽰:

dp[i][j] 表⽰:到达 [i, j] 位置处,最⼩路径和是多少。

2. 状态转移:

简单分析⼀下。如果 dp[i][j] 表⽰到达 到达 [i, j] 位置处的最⼩路径和,那么到达[i, j] 位置之前的⼀⼩步,有两种情况:

  1. 从 [i - 1, j] 向下⾛⼀步,转移到 [i, j] 位置;
  2. 从 [i, j - 1] 向右⾛⼀步,转移到 [i, j] 位置。

由于到 [i, j] 位置两种情况,并且我们要找的是最⼩路径,因此只需要这两种情况下的最⼩值,再加上 [i, j] 位置上本⾝的值即可。也就是: dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]

3. 初始化:

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

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

在本题中,「添加⼀⾏」,并且「添加⼀列」后,所有位置的值可以初始化为⽆穷⼤,然后让dp[0][1] = dp[1][0] = 1 即可。

4. 填表顺序:

根据「状态转移⽅程」的推导来看,填表的顺序就是「从上往下」填每⼀⾏,每⼀⾏「从左往后」。

5. 返回值:

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

代码呈现:

class Solution {
public:
    int minPathSum(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, INT_MAX));
        dp[0][1] = dp[1][0] = 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]) + grid[i - 1][j - 1];
        return dp[m][n];
    }
};

2.6 第六题

题目链接:174. 地下城游戏 - 力扣(LeetCode)

题目描述:

算法思想:

1. 状态表⽰:

这道题如果我们定义成:从起点开始,到达 [i, j] 位置的时候,所需的最低初始健康点数。

  1. 那么我们分析状态转移的时候会有⼀个问题:那就是我们当前的健康点数还会受到后⾯的路径的影响。也就是从上往下的状态转移不能很好地解决问题。
  2. 这个时候我们要换⼀种状态表⽰:从 [i, j] 位置出发,到达终点时所需要的最低初始健康点数。这样我们在分析状态转移的时候,后续的最佳状态就已经知晓。

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

2. 状态转移⽅程:

对于 dp[i][j] ,从 [i, j] 位置出发,下⼀步会有两种选择(为了⽅便理解,设 dp[i][j] 的最终答案是 x ):
i. ⾛到右边,然后⾛向终点

  • 那么我们在 [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] ;

ii. ⾛到下边,然后⾛向终点

  • 那么我们在 [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])

3. 初始化:

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

  1. 辅助结点⾥⾯的值要「保证后续填表是正确的」;
  2. 「下标的映射关系」。
  3. 在本题中,在 dp 表最后⾯添加⼀⾏,并且添加⼀列后,所有的值都先初始化为⽆穷⼤,然后让dp[m][n - 1] = dp[m - 1][n] = 1 即可。

4. 填表顺序:

根据「状态转移⽅程」,我们需要「从下往上填每⼀⾏」,「每⼀⾏从右往左」。

5. 返回值:

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

代码呈现:

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

三、结束语 

今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

​​ 

评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值