动态规划一

目录

1.斐波那契数列模型

[题目一------1137. 第 N 个泰波那契数 - 力扣(LeetCode)](#题目一——1137. 第 N 个泰波那契数 - 力扣(LeetCode))

[题目二------面试题 08.01. 三步问题 - 力扣(LeetCode)](#题目二——面试题 08.01. 三步问题 - 力扣(LeetCode))

[题目三------746. 使用最小花费爬楼梯 - 力扣(LeetCode)](#题目三——746. 使用最小花费爬楼梯 - 力扣(LeetCode))

1.3.1.解法一

[1.3.2. 解法二](#1.3.2. 解法二)

[题目四------91. 解码方法 - 力扣(LeetCode)](#题目四——91. 解码方法 - 力扣(LeetCode))

2.路径问题

[题目一------62. 不同路径 - 力扣(LeetCode)](#题目一——62. 不同路径 - 力扣(LeetCode))

[题目二------63. 不同路径 II - 力扣(LeetCode)](#题目二——63. 不同路径 II - 力扣(LeetCode))

[题目三------LCR 166. 珠宝的最高价值 - 力扣(LeetCode)](#题目三——LCR 166. 珠宝的最高价值 - 力扣(LeetCode))

[题目四------931. 下降路径最小和 - 力扣(LeetCode)](#题目四——931. 下降路径最小和 - 力扣(LeetCode))

[题目五------64. 最小路径和 - 力扣(LeetCode)](#题目五——64. 最小路径和 - 力扣(LeetCode))

[题目六------174. 地下城游戏 - 力扣(LeetCode)](#题目六——174. 地下城游戏 - 力扣(LeetCode))


1.斐波那契数列模型

题目一------1137. 第 N 个泰波那契数 - 力扣(LeetCode)

我们很容易就得到下面这个公式

这样子有没有发现我们可以一直通过这个公式递推后面的数啊

使用动态规划来处理的话,一般是5步来进行的

  1. 确定一个状态表示
  2. 根据状态表示来推导状态表示方程
  3. 初始化
  4. 填表顺序
  5. 返回值

确定一个状态表示

一般而言,动态规划会创建一个一维数组或者二维数组,并且取名为dp,通常这个数组也被叫做dp表

接下来我们就是想办法把里面填满,然后里面的某一个值可能就是我们的最终结果。

好,接下来来解释一下上面的5步。

确定一个状态表示的意思就是确定dp表里面每个位置存放的值的含义(这个含义就是状态表示)

**那这个状态表示怎么来的?**这里只讲下面3个

  1. 题目要求
  2. 经验+题目要求(非常常用)
  3. 分析问题的过程中,发现重复子问题

这个状态表示怎么来的是非常重要的!!!我们需要不断的练习才会明白

在我们这个题目里面,我们可以让dp[n]存储的值即可。


状态转移方程

说白了,状态转移方程就是dp[i]=?,我们得想办法通过前面(或者后面)的状态来推导dp[i]等于什么。

就像现在这道题目里的,这就是一个现成的方程,的计算依赖于前面3个状态。

我们就能得到dp[i]=dp[i-1]+dp[i-2]+dp[i-3],这就是状态转移方程,dp[i]的计算依赖于前面3个状态。

状态转移方程的推导是动态规划的难点!!!我们得好好学习


初始化

其实就是保证填dp表的时候不越界。

我们是根据状态转移方程来填表的,但是状态转移方程不一定适用于所有元素。

就像这题的dp[i]=dp[i-1]+dp[i-2]+dp[i-3],有没有考虑一下下标为0,1,2这3个位置的元素呢?这3个位置的元素是不能使用动态转移方程来计算的,因为那会越界访问。所有我们得先处理这3个位置的元素。

这道题很贴心啊,直接告诉我们了

这样子我们的dp[0],dp[1],dp[2]就填好了,后面我们填dp[3],dp[4]的时候就不会越界了。


填表顺序

其实就是为了填写当前状态的时候,所需要的状态已经计算过了。

就像是 dp[i]=dp[i-1]+dp[i-2]+dp[i-3],我们首先得知道dp[i-1],dp[i-2],dp[i-3]吧!!!

所以我们需要从左往右填


返回值

我们可以根据题目要求和状态表示来确定

  • 题目要求是给你整数 n,请返回第 n 个泰波那契数 Tn 的值。
  • 状态表示是Tn的值

那我直接返回dp[n]不就好了吗!!


我们很快就能写出代码

cpp 复制代码
class Solution {
public:
    int tribonacci(int n) {
        if (n == 0 )
            return 0;
        if(n==1||n==2)
            return 1;
        
        //创建dp表,注意需要n+1大小
        vector<int> dp(n + 1); // dp[i] 表⽰:第 i 个泰波那契数的值。

        dp[0] = 0, dp[1] = 1, dp[2] = 1; // 初始化

        // 从左往右填表
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];//状态转移方程
        }

        //返回结果
        return dp[n];
    }
};

内存优化版本

接下来我将讲解这个内存优化版本

动态规划的内存优化,我只会在这题和后面的背包问题里面讲解。

我们这题可以使用滚动数组来看看

有没有想过dp[i]=dp[i-1]+dp[i-2]+dp[i-3],求dp[i]只需要dp[i-1],dp[i-2],dp[i-3]这3个,那我可不可以搞3个变量,不搞数组了!!!后面一个进来,前面一个出去就好。

cpp 复制代码
class Solution {
public:
    int tribonacci(int n) {
        if (n == 0)
            return 0;
        if (n == 1 || n == 2)
            return 1;
        int a = 0, b = 1, c = 1, d = 0;
        for (int i = 3; i <= n; i++) {
            d = a + b + c;
            a = b;
            b = c;
            c = d;
        }
        return d;
    }
};

题目二------面试题 08.01. 三步问题 - 力扣(LeetCode)

这个题目数据量特别大啊。

我们先模拟一下这个题目怎么做?

嗯?一顿分析下来,怎么感觉和上题差不多


1.确定状态表⽰

这道题可以根据「经验+题⽬要求」

直接定义出状态表⽰:

  • dp[i] 表⽰:到达 i 位置时,⼀共有多少种⽅法。

2.根据状态表示推导状态转移⽅程

我们一般是使用之前或者之后的状态来推导这个dp[i]的!!!

以i位置状态的最近的⼀步,来分情况讨论:

如果 d p[i] 表⽰⼩孩上第 i 阶楼梯的所有⽅式,那么它应该等于所有上⼀步的⽅式之和:

  • i. 上⼀步上⼀级台阶, dp[i] += dp[i - 1] ;
  • ii. 上⼀步上两级台阶, dp[i] += dp[i - 2] ;
  • iii. 上⼀步上三级台阶, dp[i] += dp[i - 3] ;

综上所述, dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3] 。

需要注意的是,这道题⽬说,由于结果可能很⼤,需要对结果取模。

在计算的时候,三个值全部加起来再取模,即(dp[i - 1] + dp[i - 2] + dp[i - 3]) % MOD 是不可取的,同学们可以试验⼀下, integer overflow 。

n 取题⽬范围内最⼤值时,⽹站会报错 signed 对于这类需要取模的问题,我们每计算⼀次(两个数相加/乘等),都需要取⼀次模。否则,万⼀ 发⽣了溢出,我们的答案就错了。

3. 初始化

从我们的递推公式可以看出, 推导的,因为 dp[-3] dp[i] 在 i = 0, i = 1 以及 dp[-2] 或 i = 2 的时候是没有办法进⾏ dp[-1] 不是⼀个有效的数据。

因此我们需要在填表之前,根据题意将 1, 2 , 3 位置的值初始化。

dp[1] = 1, dp[2] = 2, dp[3] = 4 。

4. 填表顺序

毫⽆疑问是「从左往右」。

5. 返回值

直接返回dp[n] 的值。


cpp 复制代码
class Solution {
public:
    const int MOD = 1e9 + 7;
    int waysToStep(int n) {     
        // 处理边界情况
        if (n == 1 || n == 2)
            return n;
        if (n == 3)
            return 4;
        // 1. 创建 dp 表
        vector<int> dp(n + 1);
        // 2. 初始化
        dp[1] = 1, dp[2] = 2, dp[3] = 4;
        // 3. 填表
        for (int i = 4; i <= n; i++)
            dp[i] = ((dp[i - 1] + dp[i - 2]) % MOD + dp[i - 3]) % MOD;
        // 4. 返回
        return dp[n];
    }
};

题目三------746. 使用最小花费爬楼梯 - 力扣(LeetCode)

这个题意是特别简单的吧!这题我们将使用2种dp思想来解决这个问题。

1.3.1.解法一

这是以i位置为结尾的思路来思考问题。

我们还是按照五步走走

1.确定状态表⽰

这道题可以根据「经验+题⽬要求」

直接定义出状态表⽰:

  • **dp[i] 表⽰:**到达i位置时候的最小花费

2.根据状态表示推导状态转移⽅程

我们一般是使用之前或者之后的状态来推导这个dp[i]的!!!

一般而言,根据经验而言,是通过最近的那个状态来推导的。也就是下面这个

  1. 从i-1位置来推导:先到达i-1位置,然后支付cost[i-1],走一步
  2. 从i-2位置来推导:先到达i-2位置,然后支付cost[i-2],走两步

为了使总花费最小,dp[i] 应取上述两项的最小值,因此状态转移方程如下:

  • dp[i]=min(dp[i−1]+cost[i−1],dp[i−2]+cost[i−2])

依次计算 dp 中的每一项的值,最终得到的 dp[n] 即为达到楼层顶部的最小花费。

3. 初始化

上面那个状态转移方程其实是有使用范围的,即 2≤i≤n 时。

从我们的递推公式可以看出,我们需要先初始化 i = 0 ,以及 i = 1 位置的值。容易得到 dp[0] = dp[1] = 0 ,因为不需要任何花费,就可以直接站在第 0 层和第 1 层上

4. 填表顺序

很简单,直接从左往右

5. 返回值

根据「状态表⽰以及题⽬要求」,需要返回 dp[n] 位置的值。


现在我们很容易就写出下面这个代码

cpp 复制代码
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int>dp(cost.size()+1);
        dp[0]=dp[1]=0;
        for(int i=2;i<cost.size()+1;i++)
        {
            dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[cost.size()];
    }
};

1.3.2. 解法二

我们可以以i位置为起点来思考这个问题

我们还是按照五步走走

1.确定状态表⽰

这道题可以根据「经验+题⽬要求」

直接定义出状态表⽰:

  • **dp[i] 表⽰:**从 i 位置出发,到达楼顶,此时的最⼩花费。

2.根据状态表示推导状态转移⽅程

我们一般是使用之前或者之后的状态来推导这个dp[i]的!!!

一般而言,根据经验而言,是通过最近的那个状态来推导的。

也就是下面这个

  1. ⽀付 cost[i] ,往后⾛⼀步,接下来从 i + 1 的位置出发到终点 ,这个时候dp[i]=dp[i+1] + cost[i]
  2. ⽀付cost[i] ,往后⾛两步,接下来从 i + 2 的位置出发到终点,这个时候dp[i]]=dp[i+2] + cost[i] ;

为了使总花费最小,因此状态转移方程如下:

  • dp[i] = min( dp[i + 1], dp[i + 2] ) + cost[i] 。

依次计算 dp 中的每一项的值,最终得到的 dp[n] 即为达到楼层顶部的最小花费。

3. 初始化

为了保证填表的时候不越界,我们需要初始化最后两个位置的值,结合状态表⽰易得:

  • dp[n - 1] = cost[n - 1]
  • dp[n - 2] = cost[n - 2]

4. 填表顺序

根据「状态转移⽅程」可得,遍历的顺序是「从右往左」。

5. 返回值

根据「状态表⽰以及题⽬要求」,需要返回 min(dp[0], dp[1]);的值


我们很快就得出

cpp 复制代码
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int n=cost.size();
        vector<int>dp(n);
       dp[n-1]=cost[n-1];
       dp[n-2]=cost[n-2];
        for(int i=n-3;i>=0;i--)
        {
            dp[i]=min(dp[i+1],dp[i+2])+cost[i];
        }
        return  min(dp[0], dp[1]);;
    }
};

其实这种题目的状态表示怎么来的完全是根据经验得出的

  1. 以i位置为起点
  2. 以i位置为终点

这两种思想是特别重要的,特别是在线性dp的

题目四------91. 解码方法 - 力扣(LeetCode)

类似于斐波那契数列~

1. 状态表⽰:

根据以往的经验,对于⼤多数线性 dp ,我们经验上都是「以某个位置结束或者开始」做⽂章,这 ⾥我们继续尝试「⽤i位置为结尾」结合「题⽬要求」来定义状态表⽰。

dp[i] 表⽰:字符串中 [0 , i] 区间上,⼀共有多少种编码⽅法。

2.根据状态表示推导状态转移方程

定义好状态表⽰,我们就可以分析 i 位置的 dp 值,如何由「前⾯」或者「后⾯」的信息推导出 来。

关于 i 位置的编码状况,我们可以分为下⾯两种情况:

  • 让 i 位置上的数单独解码成⼀个字⺟;
  • 让 i 位置上的数与 i - 1 位置上的数结合,解码成⼀个字⺟。

下⾯我们就上⾯的两种解码情况,继续分析:

  • 让i位置上的数单独解码成⼀个字⺟,就存在「解码成功」和「解码失败」两种情况:

i. 解码成功 :当 i 位置上的数在[1, 9] 之间的时候,说明 i 位置上的数是可以单独解码的,那么此时 [0, i] 区间上的解码⽅法应该等于[0, i - 1] 区间上的所有解码结果。因为此时只需要在[0, i - 1] 区间上的解码⽅法后⾯填上⼀个 i 位置解码后的字⺟就可以了,但是这个不影响这个解码方式的总数。此时dp[i] = dp[i - 1];。

ii. 解码失败 :**当 i 位置上的数是 0 的时候,说明 i 位置上的数是不能单独解码的,那么 此时 [ 0, i] 区间上不存在解码⽅法。**因为 i 位置如果单独参与解码,但是解码失败 了,那么前⾯做的努⼒就全部⽩费了。此时dp[i] = 0 。

  • 让 i 位置上的数与 i - 1 位置上的数结合在⼀起,解码成⼀个字⺟,也存在「解码成功」 和「解码失败」两种情况:

i.解码成功 :当结合的数(i-1位置上的数x10+i位置上的数)在 [10, 26] 之间的时候,说明 [i - 1, i] 两个位置是可以 解码成功的,那么此时 [0, i] 区间上的解码⽅法应该等于 [0, i - 2 ]区间上的解码 ⽅法,原因同上。此时dp[i] = dp[i - 2]

ii.解码失败 :当结合的数(i-1位置上的数x10+i位置上的数)在 [0, 9] 和 [27 , 99] 之间的时候,说明两个位置结合后解 码失败(这⾥⼀定要注意 00 01 02 03 04 ......这⼏种情况),那么此时 [0, i] 区 间上的解码⽅法就不存在了,原因依旧同上。此时 dp[i] = 0 。

综上所述: dp[i] 最终的结果应该是上⾯四种情况下,解码成功的两种的累加和(因为我们关⼼ 的是解码⽅法,既然解码失败,就不⽤加⼊到最终结果中去),因此可以得到状态转移⽅程 ( dp[i] 默认初始化为 0 ):

  1. 当 s[i] 上的数在 [1, 9] 区间上时: dp[i] += dp[i - 1] ;
  2. 当 s[i - 1] 与 s[i] 上的数结合后,在 [10, 26] 之间的时候: dp[i] += dp[i - 2] ;
  3. 如果上述两个判断都不成⽴,说明没有解码⽅法, dp[i] 就是默认值 0 。

3.初始化:

⽅法⼀(直接初始化): 由于可能要⽤到 i - 1 以及 i - 2 位置上的 dp 值,因此要先初始化「前两个位置」。

初始化 dp[0] :

  • 当 s[0] == '0' 时,没有编码⽅法,结果 dp[0] = 0 ;
  • 当 s[0] != '0' 时,能编码成功, dp[0] = 1

初始化 dp[1] :

  • 当 s[1] 在 [1 , 9] 之间时,能单独编码,此时 dp[1] += dp[0] (原因同上,dp[1] 默认为 0 )
  • 当 s[0] 与 s[1] 结合后的数在 [10, 26] 之间时,说明在前两个字符中,⼜有⼀种 编码⽅式,此时 dp[1] += 1

4.填表顺序:

毫⽆疑问是「从左往右」

5.返回值:

应该返回 dp[n - 1] 的值,表⽰在 [0, n - 1] 区间上的编码⽅法。


使⽤直接初始化

cpp 复制代码
class Solution {
public:
    int numDecodings(string s) {
        int n = s.size();
        vector<int> dp(n); //创建⼀个dp 表

            //初始化前两个位置

        dp[0] = s[0]=='0'?0:1;
        if (n == 1)
            return dp[0]; //处理边界情况

        if (s[1] <= '9' && s[1] >= '1')
            dp[1] += dp[0];
        int t = (s[0] - '0') * 10 + s[1] - '0';
        if (t >= 10 && t <= 26)
            dp[1] += 1;
        
        //填表
        for (int i = 2; i < n; i++) {
            //如果单独编码
            if (s[i] <= '9' && s[i] >= '1')
                dp[i] += dp[i - 1];
            
            //如果和前⾯的⼀个数联合起来编码
            int t = (s[i - 1] - '0') * 10 + s[i] - '0';
            if (t >= 10 && t <= 26)
                dp[i] += dp[i - 2];
        }
        //返回结果
        return dp[n - 1];
    }
};

使⽤添加辅助结点

有没有发现dp[1]的计算过程跟那个for循环里面的过程完全一样啊,

我们完全可以通过设置一个虚拟结点dp[0],并设置为1,然后在dp[1]开始存储有效的数据即可,就把这个过程放到那个for循环里面

cpp 复制代码
class Solution {
public:
    int numDecodings(string s) {
        //优化
        int n = s.size();
        vector<int> dp(n + 1);
        dp[0] = 1; //保证后续填表是正确的
        dp[1] = s[0] != '0';
        //填表
        for (int i = 2; i <= n; i++) {
            //处理单独编码
            if (s[i - 1] != '0')
                dp[i] += dp[i - 1];
            //如果和前⾯的⼀个数联合起来编码
            int t = (s[i - 2] - '0') * 10 + s[i - 1] - '0';
            if (t >= 10 && t <= 26)
                dp[i] += dp[i - 2];
        }
        return dp[n];
    }
};

2.路径问题

题目一------62. 不同路径 - 力扣(LeetCode)

大家特别注意这个只能往下走和只能往右走

1. 状态表⽰:

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

  • 从 [i, j] 位置出发,巴拉巴拉;
  • 从起始位置出发,到达 [i, j] 位置,巴拉巴拉。

这⾥选择第⼆种定义状态表⽰的⽅式:

  • dp[i][j] 表⽰:从起始位置出发, ⾛到 [i, j] 位置处,⼀共有多少种⽅式。

2. 状态转移⽅程:

大家特别注意这个只能往下走和只能往右走

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

  • 从 [i, j] 位置的上⽅( [i - 1, j] 的位置)向下⾛⼀步,转移到 [i, j] 位置;
  • 从 [i, j] 位置的左⽅( [i, j - 1] 的位置)向右⾛⼀步,转移到 [i, j] 位置

由于我们要求的是有多少种⽅法,因此状态转移⽅程就呼之欲出了: dp[i][j] =dp[i-1][j] + dp[i][j - 1] 。

  1. 初始化: [i, j] 位置。

dp[i][j] =dp[i-1][j] + dp[i][j - 1] 这个方程是有越界风险的,第一行和第一列都会越界。所以我们需要处理这个越界情况。

那我们可以直接多开一列,多开一行,这样子就不会有越界的情况发生了。同时我们还要将dp[0][1] 设置为1.

现在灰色格子里面的都可以使用状态转移方程来解决啦!!

当然,下面这种情况也是可以的

  1. 填表顺序:

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

  1. 返回值:

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


我们很快就能写出下面的代码

cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>>dp(m+1,vector(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];
    }
};

题目二------63. 不同路径 II - 力扣(LeetCode)

机器人每次只能向下或者向右移动一步。

1.状态表示

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

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

  • 从 [i, j] 位置出发,巴拉巴拉;
  • 从起始位置出发,到达 [i, j] 位置,巴拉巴拉。

这⾥选择第⼆种定义状态表⽰的⽅式:

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

2. 状态转移:

简单分析⼀下。

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

  • i. 从[i, j] 位置的上⽅( [i - 1, j] 的位置)向下⾛⼀步,转移到 [i, j] 位置;
  • ii. 从 [i, j] 位置的左⽅( [i, j - 1] 的位置)向右⾛⼀步,转移到 [i, j] 位置。

但是, [i - 1, j] 与 [i, j - 1] 位置都是可能有障碍的,此时从上⾯或者左边是不可能到达[ i, j] 位置的,也就是说,此时的⽅法数应该是0。

由此我们可以得出⼀个结论,只要这个位置上「有障碍物」,那么我们就不需要计算这个位置上的 值,直接让它等于 0 即可。

我们的状态转移方程就是

  • grid[i][j]!=1: dp[i][j] =dp[i-1][j] + dp[i][j - 1]
  • grid[i][j]==1:dp[i][j]=0;

3. 初始化:

dp[i][j] =dp[i-1][j] + dp[i][j - 1] 这个方程是有越界风险的,第一行和第一列都会越界。所以我们需要处理这个越界情况。

那我们可以直接多开一列,多开一行,这样子就不会有越界的情况发生了。同时我们还要将dp[0][1] 设置为1.

现在灰色格子里面的都可以使用状态转移方程来解决啦!!

当然,下面这种情况也是可以的

4. 填表顺序:

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

5. 返回值:

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


我们很快就能写出代码

cpp 复制代码
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m=obstacleGrid.size(),n=obstacleGrid[0].size();
        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++)
            {
                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. 珠宝的最高价值 - 力扣(LeetCode)

这题和上面两题都很类似啊

  • 每次可以移动到右侧或下侧的相邻位置

1. 状态表⽰:

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

  • i. 从 [i, j] 位置出发,巴拉巴拉;
  • ii. 从起始位置出发,到达 [i, j] 位置,巴拉巴拉。

这⾥选择第⼆种定义状态表⽰的⽅式:

  • dp[i][j] 表⽰:从起始位置出发,⾛到 [i, j] 位置处,此时的最⼤价值。

2. 状态转移⽅程:

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

  • 从[i, j] 位置的上⽅ [i - 1, j] 位置,向下⾛⼀步,此时到达[i, j]位置能 拿到的礼物价值为 dp[i - 1][j] + grid[i][j] ;
  • 从 [i, j] 位置的左边 [i, j] 位置,向右⾛⼀步,此时到达[i, j]位置能 拿到的礼物价值为 dp[i][j - 1] + grid[i][j]

我们要的是最⼤值,因此状态转移⽅程为: dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]

3. 初始化:

dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j] 有越界的风险啊啊

第一行和第一列都会越界。所以我们需要处理这个越界情况。

那我们可以直接多开一列,多开一行,这样子就不会有越界的情况发生了。同时我们还要将所有元素设置为0.

现在灰色格子里面的都可以使用状态转移方程来解决啦!!

不过注意一下:现在状态转移方程变成了dp[i][j]=max(dp[i-1][j],dp[i][j-1])+frame[i-1][j-1]

4. 填表顺序:

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

5. 返回值:

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


cpp 复制代码
class Solution {
public:
    int jewelleryValue(vector<vector<int>>& frame) {
        int m=frame.size(),n=frame[0].size();
        vector<vector<int>>dp(m+1,vector<int>(n+1,0));
        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];
    }
};

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

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

1. 状态表⽰:

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

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

这⾥选择第⼆种定义状态表⽰的⽅式:

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

2. 状态转移⽅程:

对于普遍位置 [i, j] ,根据题意得,到达 i. 从正上⽅ [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.初始化

dp[i][j] = min(dp[i - 1][j], min(dp[i - 1][j - 1], dp[i - 1][j + 1])) + matrix[i][j] 这个有越界情况啊。

上面,左边和右边都会越界。所以我们需要处理这个越界情况。

那我们可以直接多开两列,多开一行,这样子就不会有越界的情况发生了。同时我们还要将所有元素设置为0.

现在灰色格子里面的都可以使用状态转移方程来解决啦!!

4. 填表顺序:

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

5. 返回值:

注意这⾥不是返回 dp[m][n] 的值! 题⽬要求「只要到达最后⼀⾏」就⾏了,因此这⾥应该返回「dp表中最后⼀⾏的最⼩值」。


我们很快就能写出答案

cpp 复制代码
class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {
        int m=matrix.size(),n=matrix[0].size();
        vector<vector<int>>dp(m+1,vector<int>(n+2,INT_MAX));
        //第一行
        for(int i=0;i<n+2;i++)
            dp[0][i]=0;
        
        for(int i=1;i<=m;i++)
        {
            for(int j=1;j<=n;j++)
            {
                dp[i][j]=min(dp[i-1][j],min(dp[i-1][j-1],dp[i-1][j+1]))+matrix[i-1][j-1];
            }
        }

        //寻找最后一行的最小值
        int ret=INT_MAX;
        for(int i=1;i<=n;i++)
        {
            ret=min(ret,dp[m][i]);
        }
        return ret;
    }
};

题目五------64. 最小路径和 - 力扣(LeetCode)

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

1. 状态表⽰:

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

  • i. 从 [i, j] 位置出发,巴拉巴拉;
  • ii. 从起始位置出发,到达 [i, j] 位置,巴拉巴拉。

这⾥选择第⼆种定义状态表⽰的⽅式:

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

2. 状态转移:

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

  • i. 从 [i - 1, j] 向下⾛⼀步,转移到 [i, j] 位置;
  • ii. 从 [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.初始化

**dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]**有越界的风险,所以我们需要做一些处理工作

那我们可以直接多开一列,多开一行,这样子就不会有越界的情况发生了。同时我们还要将所有元素设置为0.

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

4.填表顺序:

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

5.返回值:

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


很快就能写出代码

cpp 复制代码
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];
    }
};

题目六------174. 地下城游戏 - 力扣(LeetCode)

我们可以是自己来演示一下示例一

我们分别使用初始血量为3,6,7来模拟,发现7刚刚好。

1.状态表⽰:

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

  • i. 从 [i, j] 位置出发,巴拉巴拉;
  • ii. 从起始位置出发,到达 [i, j] 位置,巴拉巴拉。

但是我们发现第二种方法不行了!!

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

那么我们分析状态转移的时候会有⼀个问题:那就是我们当前的健康点数不仅受到前面路径的影响,还会受到后⾯的路径的影 响。也就是从上往下的状态转移不能很好地解决问题。

这个时候我们要换第一种状态表⽰:

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

这样我们在分析状态转移的时候,后续的最佳状态就已经知晓。 综上所述,定义状态表⽰为:

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

这题可是我们第一次使用第一种方式来处理这种问题的

2.状态转移方程

对于 dp[i][j] ,从 [i, j] 位置出发,下⼀步会有两种选择:

  1. ⾛到右边,然后⾛向终点:
    • 很容易知道我们从 [i, j] 位置出发,到达终点时所需的最低初始健康点数(即dp[i][j]) 加上这⼀个位置的消耗(即dungeon[i][j]),应该大于等于从 [i, j+1] 位置出发,到达终点时所需的最低初始健康点数(即dp[i][j+1])
    • 即:dp[i][j] + dungeon[i][j] >= dp[i][j + 1]
    • 通过移项可得:dp[i][j] >= dp[i][j + 1] - dungeon[i][j]
    • 因为我们需要最小值,所以 dp[i][j] = dp[i][j + 1] - dungeon[i][j]
  2. 走到下边
    • 很容易知道我们从 [i, j] 位置出发,到达终点时所需的最低初始健康点数(即dp[i][j]) 加上这⼀个位置的消耗(即dungeon[i][j]),应该大于等于从 [i+, j] 位置出发,到达终点时所需的最低初始健康点数(即dp[i+1][j])
    • 即:dp[i][j] + dungeon[i][j] >= dp[i + 1][j]
    • 通过移项可得:dp[i][j] >= dp[i + 1][j] - dungeon[i][j]
    • 因为我们需要最小值,所以 dp[i][j] = 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 。

处理这种情况仅需让 1 取⼀个最⼤值即可:dp[i][j] = max(1, dp[i][j])

3.初始化

  • dp 表最后添加一行和一列,并初始化为无穷大。
  • dp[m][n-1] = dp[m-1][n] = 1,因为从右下角出发到终点只需要1点健康值。

4.填表顺序

  • 从下往上填每一行,每一行从右往左。

5.返回值

  • 返回 dp[0][0] 的值,即到达起点所需的最低初始健康点数。

我们很快就能写出下面这个代码

cpp 复制代码
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];
    }
};
相关推荐
Espresso Macchiato20 分钟前
Leetcode 3389. Minimum Operations to Make Character Frequencies Equal
动态规划·leetcode hard·分类讨论·leetcode 3389·leetcode周赛428
呆呆的猫25 分钟前
【LeetCode】3356、零数组变换 II
算法·leetcode·职场和发展
xxb24942 分钟前
一个初始化bitmap的小算法
算法
两水先木示44 分钟前
【Unity3D】实现UGUI高亮引导点击
算法
懒洋洋大魔王1 小时前
数据结构——排序
数据结构·算法·排序算法
clear sky .1 小时前
[数据结构]环形队列
java·数据结构·算法
SchrodingerSDOG2 小时前
(补)算法刷题Day19:BM55 没有重复项数字的全排列
数据结构·python·算法
一条好痴粉肠2 小时前
算法设计与分析第五章作业
数据结构·算法
tinker在coding2 小时前
Coding Caprice - dynamic programming13
算法·leetcode·动态规划
CP-DD2 小时前
KMP 算法
算法