【ONE·基础算法 || 动态规划(一)】

在这里插入图片描述

总言

  主要内容:编程题举例,熟悉理解动态规划类题型(斐波那契数列模型、路径问题、简单多状态 dp 问题)。
  
  
  
  

  
  
  
  
  

1、动态规划基本介绍

  1)、基本介绍
  术语介绍: 动态规划(Dynamic Programming,简称DP)是一种在数学、计算机科学和经济学中使用的,用于找出多阶段决策过程最优解的方法。它通常用于优化递归问题,这些问题可以分解为相同或相似的子问题,或者称为重叠子问题和最优子结构性质。动态规划的基本思想是将问题分解为简单的子问题,并将子问题的解存储起来,以便在需要时直接使用,从而避免重复计算。
  
  核心思想: 在解决一些复杂问题时,我们通常会发现问题可以被分解成一些更小的、相似的子问题。如果我们能够找到这些子问题的解,并将其存储起来,那么当我们再次需要这些解时,就可以直接从存储中取出,而不需要重新计算。这就是动态规划的核心思想。

  动态规划的基本要素——dp表: 一般在使用动态规划解题时,往往需要先定义一个dp表,它是一种用于存储子问题解的数据结构,通常定义为一个一维或二维数组。
  
  
  动态规划的基本路流程: 如下,前两步为核心步骤,后面的三步其实是在做细节处理。

1、确定状态表示(最重要的一步)
2、推导状态转移方程(最难的一步)
3、初始化(细节处理部分)
4、确定填表顺序(细节处理部分)
5、返回值(细节处理部分)

  
  
  2)、状态表示
  状态表示: 根据上述dp表的描述,状态表示指的是dp表中某一位置(或索引)的值所代表的具体含义(或问题的一个特定子问题的解)。这个“状态”是根据问题的特性和需要来定义的,以dp[i]dp[i][j]表示,是一个“虚”的值,但实际落到每一个具体位置上,有具体的值。
在这里插入图片描述

  通常来讲有两种基本套路:①以 i 位置为结尾,巴拉巴拉……;②以 i 位置为起点,巴拉巴拉……
  
  如何确定一个状态表示? (通常有3种确定方法)
  ①从题目要求中获取。例如,下面的第 N 个泰波那契数,题目直接给了泰波那契序列的定义,此时dp[i]表示第 i 个泰波那契数的值。
  ②经验+题目要求。这里经验就需要我们大量的做题。
  ③分析问题过程中,发现重复子问题。这时候我们可以把这个子问题抽象成为状态表示。
  ④除了上述三者,也有其它确定状态表示的方法。
  
  
  
  3)、状态转移方程
  状态转移方程实际就是dp[i]的填表公式(一个数学表达式),其一般是根据题目要求推导获得的,推导思想为利用已知的状态(或子问题的解)来计算新的状态(或问题的解)。
  例如,求第n个斐波那契数列中,状态转移方程就可以写为:dp[i] = dp[n-1] + dp[n-2]。这意味着为了计算斐波那契数列的第 i 个数,我们需要知道第 i-1 个数和第 i-2 个数的值。
  
  
  
  4)、初始化
  初始化: 主要是为了保证填充dp表时不会越界,并且确保起始状态或边界状态有正确的值。
  例如上述斐波那契数列中dp[i] = dp[n-1] + dp[n-2],若i=0或i=1,此时dp表中下标为负数,因此初始化时,要我们手动设置这两处的值。
  
  其它说明(处理边界问题以及初始化的技巧): 为了防止越界,一般在使用动态规划时,可以为dp表引入虚拟节点。
在这里插入图片描述
  引入虚拟节点一定程度上能够简化初始化的复杂度(尤其是在二维dp表中),但我们需要保证在使用它时,能得到正确的状态关系。因此,我们需要注意:
  1、注意如何填充虚拟节点中的值,使得后续填表仍旧正确。(这需要具体题目具体分析)
  2、注意引入虚拟节点后,下标的映射关系。例如,一位dp表中,dp[i]和实际arr[i]之间如何映射?二维呢?

  当然,不引入虚拟节点也是可以使用动态规划的,比如,我们可以加入一个if判断语句(4.7举例),或者直接对存在越界的位置进行初始化,等等。看写法风格。

  
  
  
  5)、填表顺序、返回值
  填表顺序: 比如一维dp表中,填表可以从左向右,也可以从右向左。主要是根据状态转移方程来判断的,为了填写当前状态,其所依赖的状态应该已经计算过(处于已知)。
在这里插入图片描述

  
  返回值: 即我们要的最终结果,dp表中存储着各个状态位置的解,而我们需要的有时只是其中某一状态的解。具体是哪一返回值,需要根据 题目要求 + 状态表示 来确定。
  例如,斐波那契数列,题目要求返回第n个斐波那契数,而状态表示dp[i]表示第i个斐波那契数列,那么我们只用返回dp[n]即可。
  
  
  
  
  
  
  

2、斐波那契数列模型

2.1、第 N 个泰波那契数(easy)

  题源:链接

在这里插入图片描述

  
  

2.1.1、动态规划(基础版)

  1)、思路分析
  状态表示: 本题可以根据题目的要求直接定义出状态表示。dp[i] 表示,以i位置为结尾,第 i 个泰波那契数的值。
  状态转移方程: 题目已经给定,dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]
  初始化: 从我们的递推公式可以看出, dp[i]i = 0、1、2 的时候是没有办法进行推导的,因为 dp[-2]dp[-1] 不是⼀个有效的数据。因此我们需要在填表之前,将 0, 1, 2 位置的值初始化。关于它们的初始值,题目已经告诉了我们 dp[0] = 0, dp[1] = dp[2] = 1
  填表顺序: 毫无疑问,此题需要从左往右填表。
  返回值: 根据题目,返回dp[n],即第n个泰波那契数。
  
  
  2)、题解
  如下,时间复杂度为O(n),空间复杂度为O(n)。

class Solution {
public:
    // 使用动态规划解决:根据题目,设状态表示dp[i]为第i个泰波那契数
    int tribonacci(int n) {
        // 处理边界情况:题目告知 0 <= n <= 37,说明n可以为0,1,2,而我们下述动态规划是从n=3开始
        if(n == 0) return 0;
        if(n == 1 || n == 2) return 1;

        int* dp = new int[n+1];// 1、创建dp表:题目要求返回第n个值,这里dp表需开辟n+1空间
        dp[0] = 0; dp[1] = dp[2] = 1;// 2、初始化dp表:用于保证后续填表过程不越界
        for(int i = 3; i <= n; ++i)// 3、根据状态转移方程,从左到右填表:这里从i=3开始填,直到i==n为止(注意要把n算入)
            dp[i] = dp[i-1] + dp[i-2] + dp[i-3];
        return dp[n];// 4、根据状态表示,获取返回值
    }
};

  
  

2.1.2、动态规划(引入空间优化版)

  1)、说明
  进行空间优化: 观察此题,上述动态规划能为我们提升时间复杂度,但避免不了空间资源的使用。实际上在动态规划中我们也可以不使用dp表,而是使用变量迭代的方式来代替,即滚动数组(当然,如果你愿意也可以使用一个常量级别的数组来实现 int dp[3] )。

在这里插入图片描述
  
  
  2)、题解
  优化的效果: 若空间复杂度是 O ( n 2 ) O(n^2) O(n2),优化后为 O ( n ) O(n) O(n);若空间复杂度是 O ( n ) O(n) O(n),优化后为 O ( 1 ) O(1) O(1)

class Solution {
public:
    // 使用动态规划解决:根据题目,设状态表示dp[i]为第i个泰波那契数
    int tribonacci(int n) {
        // 处理边界情况:题目告纸 0 <= n <= 37,说明n可以为0,1,2,而我们下述动态规划是从n=3开始
        if(n == 0) return 0;
        if(n == 1 || n == 2) return 1;

        int a = 0, b = 1, c = 1;// 用于进行滚动的变量
        int d = 0;// 用于获取每次的泰波那契数
        for(int i = 3; i <= n; ++i)
        {
            // 计算第i个泰波那契数
            d = a + b + c;
            // 滚动操作:迭代,计算后续的泰波那契数
            a = b;
            b = c;
            c = d;
        }

        return d;// 4、for循环结束时,此时d中获取到的是第n个泰波那契数
    }
};

  关于学习重心问题说明: 重点在如何使用动态规划解决问题上,空间优化是学会能够灵活运用后进行的锦上添花的操作。
  
  
  
  
  
  
  
  
  
  

2.2、三步问题(easy)

  题源:链接

在这里插入图片描述

  
  

2.2.1、动态规划

  1)、思路分析
  根据题目,小孩一次可以上1阶、2阶或3阶台阶。也就是说,对第 i 个台阶:

可以从第 i-1 个台阶,迈 1 个台阶到达 i
可以从第 i-2 个台阶,迈 2 个台阶到达 i
可以从第 i-3 个台阶,迈 3 个台阶到达 i

  以此类推,对第 i-1 个台阶,也存在着类似的到达方式。这也就意味着,要想知道到达第 i 个台阶一共有多少种走法,只需要知道到达第 i-1、i-2、i-3个台阶的走法即可。到第 i 个台阶,无非是在这三者基础上走对应的1、2、3步。因此,我们可以得出动态规划的状态转移方程dp[i] = dp[i-1] + dp[i-2] + dp[i-3]

在这里插入图片描述
  注意初始化: 在计算 dp[i] 时,我们需要用到 dp[i−1], dp[i−2], 和 dp[i−3] 的值。当 i 较小时,递推公式中某些项会涉及到负数索引,这在实际情况中是没有意义的,也不是有效的数据。
  为了解决这个问题,我们需要在开始填表(即计算 dp 数组的值)之前,先对123 这三个位置进行初始化。
  

  取模问题: 题目中告知我们结果可能很大,需要对结果取模。对于这类需要取模的问题,一般可每次计算(两个数相加/乘等),都需要取一次模。 (以防两次、三次运算后再取模,仍旧发生数据溢出。)
  

  扩展说明: 1e9 + 7,在科学计数法中, 1 e 9 1e9 1e9 表示 1 × 1 0 9 1×10 ^9 1×109,故此数实际上是 1 e 9 + 7 = 1000000007 1e9 + 7 = 1000000007 1e9+7=1000000007相关扩展链接
  
  

  2)、题解
  状态表示: 本题状态表示是根据经验+题目的要求获得。dp[i] 表示:以i位置为结尾,到达第 i 个台阶时,一共存在多少种上楼方式。
  状态转移方程: 我们之前已经推导出,dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]
  初始化: 在上述已经讲明了。
  填表顺序: 毫无疑问,此题需要从左往右填表。
  返回值: 根据题目,返回dp[n],到达第n个台阶时存在的上楼方法数。

class Solution {
    const int MOD = 1e9 + 7;// 用于大数取模
public:
    // 使用动态规划解决:dp[i]表示到达第i个台阶时,总的上楼方式。(例如:dp[1]表示到第1个台阶,dp[0]无意义)
    int waysToStep(int n) {
        // 0、处理特殊边界情况
        if(n == 1 || n == 2) return n;
        if(n == 3) return 4;

        int* dp = new int[n+1];// 1、创建一个dp表:根据上述状态表示,n个台阶,数组大小为n+1
        dp[0] = 0;
        dp[1] = 1, dp[2] = 2, dp[3] = 4;// 2、初始化dp表:主要用于后续状态转移方程时,防止越界。
        for(int i = 4; i <= n; ++i)
            dp[i] = ((dp[i-1] + dp[i-2]) % MOD + dp[i-3]) % MOD;// 3、根据状态转移方程,从左到右填表:这里从i=4开始,到i=n结束。
        return dp[n];// 4、返回目标值
    }
};

  
  
  
  
  
  
  
  

2.3、使用最小花费爬楼梯(easy)

  题源:链接

在这里插入图片描述

  
  

2.3.1、题解一

  1)、思路分析
  先来思考一个问题,题目中的楼梯顶指哪里?根据题目示例描述,“从下标为 1 的台阶开始。支付 15 ,向上爬两个台阶,到达楼梯顶部。”,由此说明,对于vector<int>& cost,设其下标区间在[0,n)内,则楼梯顶在下标为 n 的位置处。
在这里插入图片描述

  
  
  1、确定状态表示:
  根据章节1处的介绍,通常这类一维的动态规划中,状态表示通常有两种基本套路:

以 i 位置为结尾,巴拉巴拉(具体含义)……
以 i 位置为起点,巴拉巴拉(具体含义)……

  这里,“为起点还是结尾”,通常来源于经验总结,而“具体含义”,通常来源于题目要求。
  

  我们先使用第一种:根据经验+题目要求,对dp[i],其表示以 i 为结尾,到达 i 位置处所需要的最小花费。(注意理解:到达 i 位置的时,i 位置的花费并不需要算上。)

在这里插入图片描述

  2、根据上述状态表示的含义,我们来分析状态转移方程,要到达dp[i]无非两种情况:

1、以最小的花费先到达 i - 1 下标处的台阶,支付cost[i-1]的经费,走 1 步到达i。 
2、以最小的花费先到达 i - 2 下标处的台阶,支付cost[i-2]的经费,走 2 步到达i。

  而这里,“以最小的花费先到达i-1、i-1台阶处”,正是我们对状态表示的描述:

dp[i-1]: 到达 i-1 位置处,所需要的最小花费。
dp[i-2]: 到达 i-2 位置处,所需要的最小花费。

  因此,上述描述就成为了:

1、dp[i] = dp[i-1] + cost[i-1] : 先以最小的花费到达 i - 1 下标处的台阶,支付cost[i-1]的经费,走 1 步到达i。 
2、dp[i] = dp[i-2] + cont[i-2] : 先以最小的花费到达 i - 2 下标处的台阶,支付cost[i-2]的经费,走 2 步到达i。

由于我们计算的是最小花费,因此取两者最小值:
dp[i] = min(dp[i-1] + cost[i-1],dp[i-2] + cont[i-2])

  

  3、初始化: 主要是为了填表时不越界,实则我们在上图中已经分析过了。从这里的递推公式也可以看出,要先初始化 i = 0i = 1 位置的值。由于题目中告知我们可以直接站在第 0 层或第 1 层台阶上,再根据此处对 dp[i] 含义的表述可知,容易得到 dp[0] = dp[1] = 0 ,不需要任何花费。
  
  
  4、填表顺序: 无疑,这里需要从左向右填表。
  

  5、返回值: 题目要求我们返回楼梯顶位置处的最小花费,在上述我们已经分析过了,这里的楼梯顶指的是cost.size()位置处的花费。设 n = cost.size() , 也就意味着这里要返回的是 dp[n]。

dp[n]:到达n位置处时,所需要的最小花费。

  
  
  
  
  2)、题解

class Solution {
public:
    // 使用动态规划解决:dp[i]表示到达第i个位置时,所需的最小花费
    int minCostClimbingStairs(vector<int>& cost) {
        
        int n = cost.size();// 统计一共有多少个台阶:实际下标为[0,n-1],楼梯顶为n
        if(n == 0 || n == 1) return 0;// 0、处理边界特殊情况(根据题目2 <= cost.length <= 1000,也不用特殊处理,但谨慎一点可以写上)

        vector<int> dp(n+1, 0);// 1、创建dp表:最终要获取dp[n],这里dp表需要大小为n+1
        dp[0] = dp[1] = 0;// 2、初始化dp表:根据题目,可以从下标为 0 或下标为 1 的台阶开始爬,故这两台阶的最小花费为0(即无需花费)
        for(int i = 2; i <= n; ++i)
            dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]);// 3、根据状态转移方程,从左到右填表
        return dp[n];// 4、根据题目,返回到达楼顶(下标为n的位置处)的最小花费
    }
};

  
  
  
  

2.3.2、题解二

  1、确定状态表示: 上述的题解一中,状态表示是 “以 i 位置为结尾” 获得的。这里,我们尝试使用第二种,即“以 i 位置为起点”。

1、以 i 位置为结尾,(具体含义)……
2、以 i 位置为起点,(具体含义)……

  若以 i 位置为起点,则dp[i]可以表示为,从 i 位置开始,到达楼梯顶时,所需要的最小花费。
在这里插入图片描述
  那么dp[i] 无非两种情况:

1、支付 i 位置的费用cost[i], 往后走1 步,到达i+1位置处。此时,只需要获取到从i+1到楼梯顶的最小费用,即可知道dp[i]位置的最小费用。
2、支付 i 位置的费用cost[i], 往后走2 步,到达i+2位置处。此时,只需要获取到从i+2到楼梯顶的最小费用,即可知道dp[i]位置的最小费用。

  2、获取状态方程: 由上述状态表示可知,从i+1到楼梯顶的最小费用即dp[i+1]、同理可得dp[i+2]。则有:

1、dp[i] = cost[i] + dp[i+1];
2、dp[i] = cost[i] + dp[i+2];

取最小费用:dp[i] = min(cost[i] + dp[i+1], cost[i] + dp[i+2]);

  
  3、初始化: 为了保证填表的时候不越界,需要初始化最后两个位置的值,结合状态表示可知:dp[n-1] = cost[n - 1]dp[n - 2] = cost[n - 2]
在这里插入图片描述
  
  4、填表顺序: 从右往左,这里我们只需要从n-3下标位置开始填表即可。
  
  5、返回值: 根据题目 ,可以从下标为0,或者下标为1的位置到达楼梯顶。而此处dp[i]表示从 i 位置为起点到楼梯顶的最小花费,因此,最终返回值,应该取 dp[0]dp[1] 二者最小值。
  
  
  2)、题解
  这里,dp表的空间开辟,只需要n个位置即可。

class Solution {
public:
    // 使用动态规划解决:dp[i]表示从第i个位置出发,到达楼顶时的最小花费
    int minCostClimbingStairs(vector<int>& cost) {
        int n = cost.size();// 统计一共有多少个楼层,[0,n-1]表示楼层数,n表示楼顶
        if(n == 0 || n == 1) return 0;// 0、处理特殊情况:若本身就要到达下标为第0、第1个台阶位置,无需花费

        vector<int> dp(n,0);// 1、创建dp表:这里最大从n-1位置出发,到达楼顶(n),因此dp表大小只需n
        dp[n-1] = cost[n-1]; dp[n-2] = cost[n-2];// 2、初始化dp表

        for(int i = n-3; i >= 0; --i)
            dp[i] = min(dp[i+1] + cost[i], dp[i+2] + cost[i]);// 3、根据状态转移方程,从右到左填表
        
        return min(dp[1],dp[0]);// 4、获取返回值:这里要从下标为 0 或下标为 1 的台阶开始,获取到达楼顶的最小花费。
    }
};

  
  
  
  
  
  
  
  
  

2.4、解码方法(medium)

  题源:链接

在这里插入图片描述

  
  

2.4.1、题解

  1)、思路分析
  根据题目,对A~Z进行编码,对应数字1~26。那么,对于给定的数字,解码方式要么是①取1位数字单独进行解码,要么是②取2位数字组合在一起解码。不可能有3位及3位以上的数组解码为1位字符的情况。
在这里插入图片描述

  1、确定状态表示:(经验+题目要求)这里,我们以 i 为结尾来分析。那么对于dp[i],其表示,到达 i 位置时,所存在的解码方法总数。
  
  2、确定状态方程: 在上述我们分析,要对 i 位置解码,有两种解码方式,①选取 i 位置处单独解码,②选取 i-1 、i 位置处的组合解码(因为这里是以 i 位结尾,自然2个数字的组合,选取了 i 之前的数字)。而二者分别有成功、不成功两种解码状态。

在这里插入图片描述
  由上图可知,最终,dp[i] = dp[i-1] + dp[i-2](有条件的!)
  

  3、初始化: 根据题目,需要对i =0 和 i=1初始化。在章节1中我们介绍过,初始化有两种方式,①直接初始化;②添加虚拟节点/虚拟位置/辅助位置。

  这里我们先选择直接初始化的方式:
  对dp[0]:

1、当 s[0] == '0' 时,没有编码方式,结果dp[0] = 02、当 s[0] != '0' 时,能编码成功,dp[0] = 1

  对dp[1]:

1、当s[1][1,9]之间时,能单独编码,此时dp[1] += dp[0]2、当s[0]与s[1]结合后的数在[1026] 之间时,说明在前两个字符中,又有一种编码方式,此时dp[1] += 1

  

  引入虚拟节点的初始化方式:
在这里插入图片描述
  引入虚拟节点后,对dp[1]的初始化,实则只需要看s[0]即可。
  反倒是虚拟节点的dp[0]的初始化,需要仔细考虑。注意:在填写时,并非所有虚拟节点dp[0],一上来都不闻不问,直接dp[0] = 0,需要我们具体题目具体分析
在这里插入图片描述

  
  
  4、填表顺序: 从左往右
  
  5、返回值: 如果选择直接初始化的方式,返回 dp[n - 1] 的值,表示在 [0, n - 1] 区间上的解码方式总数。
  
  
  
  2)、题解
  直接初始化的解题方式:

class Solution {
public:
    // 以动态规划解题:dp[i],以i位置为结尾,表示到达i位置时,解法的方法总数
    int numDecodings(string s) {
        int n = s.size();// 获取字符串长度:[0,n-1],返回s的解法总数,则意味着返回dp[n-1]

        // 1、创建dp表
        vector<int> dp(n,0);// 每个位置记录到达该位置的解码方案总数

        // 2、初始化dp表
        dp[0] = (s[0] != '0') ? 1 : 0;//初始化dp[0]

        if(n == 0) return 0;// 处理特殊情况
        if(n == 1) return dp[0];// 处理特殊情况

        if(s[1] >= '1' && s[1] <= '9') dp[1] += dp[0];// 初始化dp[1]:考虑单数的情况
        int tmp = (s[0] - '0') * 10 + (s[1] - '0');// 初始化dp[1]:考虑两位数的情况
        if(tmp >= 10 && tmp <= 26) ++dp[1];// 这里判断条件必须是[10,26]而非[1,26],若是后者说明有前导零

        // 3、根据状态转移方程,从左到右填表
        for(int i = 2; i < n; ++i)
        {
            // 考虑单位数时
            if(s[i] >= '1' && s[i] <= '9') dp[i] += dp[i-1];
            // 考虑两位数时
            int tmp = (s[i-1] - '0') * 10 + (s[i] -'0');
            if(tmp >= 10 && tmp <= 26) dp[i] += dp[i-2];
        }

        // 4、返回
        return dp[n-1];

    }
};

  
  使用虚拟节点的写法:这种写法中,由于映射关系,需要注意返回值的下标位置。

class Solution {
public:
    int numDecodings(string s) {
        // 1、创建dp表,确定状态转移方程:dp[i]表示,到达i位置时,所存在的解码方式总数
        int n = s.size();
        vector<int> dp(n + 1, 0); // 引入虚拟节点,多开辟一个位置

        // 2、初始化dp表
        dp[0] = 1;
        dp[1] = (s[0] != '0');

        // 3、根据状态方程,从左到右填表
        for (int i = 2; i <= n; ++i) {
            if (s[i - 1] >= '1' && s[i - 1] <= '9')
                dp[i] += dp[i - 1];
            int tmp = (s[i - 1] - '0') + (s[i - 2] - '0') * 10;
            if (tmp >= 10 && tmp <= 26)
                dp[i] += dp[i - 2];
        }

        // 4、返回
        return dp[n]; // 注意这里的返回值
    }
};

  
  
  
  
  
  
  

3、路径问题

3.1、不同路径(medium)

  题源:链接

在这里插入图片描述

  
  

3.1.1、题解

  1)、思路分析
  此题我们曾介绍过递归+记忆搜索版本
  相关思路大差不差,这里我们以动态规划来实现。
  

3.1.1.1、题解一

  1、确定状态表示: 根据经验+题目要求,这是在二维数组中,则以 (i,j) 为结尾,dp[i][j]表示,从起点(0,0)到达(i,j)时,存在的路径总数。
  

  2、确定状态转移方程: 根据题目,到达 (i,j) 的方式有二,①从 (i-1,j) 出发,往下走一步;②从 (i, j-1) 位置出发,往右走一步。

  则有:dp[i][j] = dp[i-1][j] + dp[i][j-1]

在这里插入图片描述
  

  3、初始化:
  ①直接初始化。根据题目和状态转移方程,这里存在特殊情况的是首行和首列。可以在填表前先对其进行初始化。

  ②引入虚拟节点。二维数组中,虚拟节点的引入通常需要看题目需求。这里,dp[i]是以i为结尾,表示到达i位置处的路径总数,因此。我们可以引入虚拟的首行、首列,方便初始化原先的首行、首列
  根据章节1反复强调的,引入虚拟节点,需要注意两点。
  ①此时dp表和存储数据的数组,二者的下标映射关系。
  ②虚拟节点如何填值,以保证动态转移方程能够正确使用。

在这里插入图片描述  
  
   4、填表顺序:
  根据状态转移方程,这里的填表的顺序是 从上往下,从左往右。
  
  5、返回值: 根据状态表示,我们要返回的结果是dp[m][n] (引入了虚拟节点,存在映射关系)。
  
  
  
  2)、题解
  时间复杂度: O ( m × n ) O(m×n) O(m×n)、空间复杂度: O ( m × n ) O(m×n) O(m×n)

class Solution {
public:
    // 以动态规划的方式解题:这里是二维数组,dp表也是二维的,dp[i][j]表示从start起始位置开始到达(i,j)位置有多少种路径
    int uniquePaths(int m, int n) {
        // 1、创建dp表:这里为了方便后续初始化dp表,采用虚拟节点的方式。此处首行、首列为虚拟节点。因此,这里矩阵大小为(m+1)×(n+1)
        vector<vector<int>> dp(m+1,vector<int>(n+1, 0));
        dp[0][1] = 1;// 2、初始化dp表:因为引入虚拟节点,这里初始化时就不用对首行、首列进行整行整列初始化。
        // 3、根据状态转移方程填表:这里按照矩阵顺序填写即可
        for(int i = 1; i < m+1; ++i)
        {
            for(int j = 1; j < n+1; ++j)
            {
                dp[i][j] = dp[i-1][j] + dp[i][j-1];// 左→右、上→下
            }
        }

        // 4、返回目标值:这里是(m+1)×(n+1)矩阵,其finish位置就是(m,n)
        return dp[m][n];
    }
};

  
  
  
  

3.1.1.2、题解二

  1)、思路分析

  实际上,本题也可以倒着思考。

  1、确定状态表示: 根据经验+题目要求,在二维数组中,以(i,j)位置为起点,则 dp[i][j] 表示以 (i,j) 位置为起点,到达终点位置时存在的路径总数。
  
  2、确定状态转移方程: 根据题目,方式有二。

1、从(i,j)位置出发,往右走一步到达(i,j+1)位置处。那么我们只需要知道(i,j+1)位置处存在的路径总数,即可知道(i,j)位置的路径总数(无非是多走一步)。
2、从(i,j)位置出发,往下走一步到达(i+1,j)位置处。那么我们只需要知道(i+1,j)位置处存在的路径总数,即可知道(i,j)位置的路径总数(无非是多走一步)。

  则有dp[i][j] = dp[i+1][j] + dp[i][j+1]

在这里插入图片描述

  
  3、初始化: 这里,我们仍旧选择使用虚拟节点的方式。在这种状态转移方程下,我们需要处理的是尾行和尾列。

在这里插入图片描述

  4、填表顺序: 根据状态转移方程,这里的填表顺序就是从右到左,从下到上。
  5、返回值: 题目要求从左上角开始,即以(0,0)位置为起点。
  
  
  

  2)、题解

class Solution {
public:
    int uniquePaths(int m, int n) {
        // 1、确定状态表示:dp[i][j],以(i,j)为起点,到达终点位置时,存在的路径总数
        vector<vector<int>> dp(m+1,vector<int>(n+1,0));//引入虚拟节点(尾行、尾列)
        // 2、初始化
        dp[m][n-1] = 1;
        // 3、填表:从右到左,从下到上
        for(int i = m-1; i >= 0; --i )
        {
            for(int j = n-1; j >=0; --j)
                dp[i][j] = dp[i][j+1] + dp[i+1][j];
        }
        // 4、返回:题目要求从左上角走,即以(0,0)位置为起点
        return dp[0][0];
    }
};

  
  
  
  
  
  
  
  
  

3.2、不同路径II(medium)

  题源:链接

在这里插入图片描述
  
  

3.2.1、题解

  1)、思路分析
  此题和上题的思路一致,区别只是在于写动态转移方程时,我们需要根据题目进行一定调整。
  
  1、确定状态表示: 仍旧是经验+题目要求。二维数组,这里,我们以(i,j)为结尾,则dp[i][j]表示,从起点位置,到达(i,j)时所存在的方法总数。

  2、确定状态转移方程: 引入了障碍物,这意味着①若当前(i,j)位置处是障碍,则没有路径。②若当前(i,j)位置非障碍,则到达 (i,j) 的方式有二:①从 (i-1,j) 出发,往下走一步;②从 (i, j-1) 位置出发,往右走一步。
  因此,dp[i][j] = dp[i-1][j] + dp[i][j-1] (有条件!若碰到障碍物,当前dp[i][j] = 0)。

  
  3、初始化: 和先前一样,这里我们选择引入虚拟节点方式进行初始化。

  4、填表顺序: 在当前这种状态转移方程下,填表方式为从左到右、从上到下。

  5、返回值: 因虚拟节点的存在,这里需要返回的结果是dp[m][n]

  
  
  2)、题解

class Solution {
public:
    // 这里使用动态规划来解题:二维数组,dp[i][j]表示到达(i,j)位置时,一共有多少种路径
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        if(obstacleGrid[0][0] == 1) return 0;// 起点即障碍物的情况

        int m = obstacleGrid.size();// 原始矩阵行
        int n = obstacleGrid[0].size();// 原始矩阵列

        // 1、创建dp表:这里采用虚拟节点的方式初始化dp表,因此我们多加了一行、一列在首部。
        // 则实际矩阵为:(m+1)×(n+1)
        vector<vector<int>> dp(m+1, vector<int>(n+1,0));// finish位置在(m,n)处
        // 2、初始化dp表:初始化的值保证结果正确即可,这里选择(0,1)、(1,0)初始化均可。
        dp[0][1] = 1;
        // 3、根据状态转移方程填表:这里按照矩阵顺序填写即可。区别于不同路径Ⅰ,多一个判断障碍物的条件
        for(int i = 1; i < m +1; ++i)
        {
            for(int j = 1; j < n+1; ++j)
            {
                // dp[i][j]实则映射的是obstacleGrid[i-1][j-1]处的位置,这里判断障碍物是要判断原表的
                if(obstacleGrid[i-1][j-1] == 1 ) dp[i][j] = 0;
                else
                    dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }

        // 4、返回finish处的路径
        return dp[m][n];
    }
};

  
  
  
  
  
  
  

3.3、珠宝的最高价值(medium)

  题源:链接

在这里插入图片描述

  

3.3.1、题解

  1)、思路分析
  根据题目描述,我们需要从二维数组frame的左上角开始拿格子里的珠宝,并每次 向右 或者 向下 移动一格、直到到达二维数组的右下角位置。选择一条路径,使得最终获取到的珠宝值最大。
在这里插入图片描述
  仍旧是路径问题,可用决策树递归,记忆搜索化等等都方法也可解题。这里我们学习动态规划的解题模式。
  

  1、确定状态表示: 根据经验+题目要求。此处选择一个常见的思考方式。以(i,j)位置为结尾,那么对于本题,dp[i][j]表示,从起点位置,到达(i,j)位置时,所获得的珠宝最大值。
  

  2、确定状态转移方程: 根据题目条件,要想到达(i,j)位置,可以有两种走法:从上往下,从左往右。

1、从上方位置 (i-1, j) 向下走一步,此时到达 (i, j) 拿到的珠宝价值为 dp[i-1][j] + frame[i][j]2、从左边位置 (i, j-1) 向右走一步,此时到达 (i, j) 拿到的珠宝价值为 dp[i][j-1] + frame[i][j]

  我们需要获取的是当前位置珠宝的最大值,因此需要选择的是上述两种路径中较大的那个:

//两种写法都行
dp[i][j] = max(dp[i−1][j] + frame[i][j], dp[i][j−1] + frame[i][j])
dp[i][j] = max(dp[i−1][j],dp[i][j−1]) + frame[i][j]

  

  3、初始化: 这里选择引入虚拟节点的方式初始化。此时需要注意两点:

1、dp表和原先frame表的下标映射关系
2、虚拟节点中的填值,要保证后续填表是正确的。

  本题中,虚拟行列的所有值都可为0。
在这里插入图片描述

  
  
  4、填表顺序: 根据状态转移方程,填表的顺序是从左到右,从上到下。

  5、返回值: 根据状态表示,由于我们引入了虚拟行列,这里应该返回dp[m][n] 的值。
  
  

  2)、题解

class Solution {
public:
    // 使用动态规划解题:二维矩阵,dp[i][j]表示到(i,j)位置时,最高的珠宝价值
    int jewelleryValue(vector<vector<int>>& frame) {
        int m = frame.size();
        int n = frame[0].size();
        if(m == 0 && n == 0) return 0;// 没有任何珠宝的情况

        // 1、创建dp表:这里采用虚拟节点的方式,故需要(m+1)×(n+1)的矩阵
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
        // 2、初始化dp表:只需初始化(0,1)、(1,0)的位置
        dp[0][1] = dp[1][0] = 0;// 题目说明了珠宝的价值都是大于 0 的,因此这里可以二者初始化为0(不含该条件则初始化为最小值)
        // 3、根据状态方程填表:从上到下,从左到右
        for(int i = 1; i < m + 1; ++i)
        {
            for(int j = 1; j < n + 1; ++j)
            {
                // 因为引入了虚拟节点,这里dp[i][j]实际映射的是珠宝架frame[i-1][j-1]
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + frame[i-1][j-1];// 当前珠宝的价值 = 从上往下或从左往右任意路径的最大珠宝价值+当前(i,j)架子上的珠宝价值
            }
        }
        // 4、返回目标值:这里珠宝架的右下角是(m,n)
        return dp[m][n];
    }
};

  
  
  
  
  
  
  

3.4、下降路径最小和(medium)

  题源:链接

在这里插入图片描述

  

3.4.1、题解

  1)、思路分析
  先来理解下降路径:

在这里插入图片描述
  
  1、确定状态表示: 根据经验+题目要求。我们选择以(i,j)位置为结尾,则dp[i][j]表示,下降过程中,到达(i,j)位置时,路径的最小和。

  
  2、确定状态转移方程: 对于坐标(i,j),根据题意得,从上到下到达(i,j)位置可能有三种情况。
在这里插入图片描述

1、从左上方(i-1,j-1)位置处,转移到(i,j)位置;
2、从正上方(i-1,j)位置处,转移到(i,j)位置;
3、从右上方(i-1,j+1)位置处,转移到(i,j)位置;

  要知道(i,j)位置处的最小和,只需要知道左上方(i-1,j-1)、正上方(i-1,j)、右上方(i-1,j+1)这三者位置处的最小和,然后再加上当前(i,j)位置处的值即可。而左上、正上、右上三个位置的最小和,正是我们对状态表示的描述,因此有:

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

  
  
  3、初始化: 根据上述状态方程,在填表时,首行、首列、尾列存在越界。这里为了方便,我们引入虚拟节点进行初始化。仍旧需要面对引入虚拟节点后的两个问题:

1、下标的映射关系
2、虚拟节点中的值,要保证后续填表时是正确的

在这里插入图片描述

  分析如下:
在这里插入图片描述

  4、填表顺序: 根据状态表示,填表的顺序是从上往下。

  5、返回值: 根据题目表示,这里要返回的实则是,dp表中最后一行的最小值。
  
  
  
  2)、题解
  需要注意,引入虚拟节点后,dp表中下标映射关系发生改变。填表时,也需要注意遍历的边界问题。

class Solution {
public:
    // 以动态规划解题:二维矩阵,这里以(i,j)为结尾,dp[i][j]表示到达(i,j)位置时,最小的下降和
    int minFallingPathSum(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        if(m == 0 && n == 0) return 0;// 实则不会发生此情况,因为题目规定1 <= {m,n} <= 100

        // 1、创建dp表:这里采用虚拟节点的方式,引入首行、首列、尾列
        vector<vector<int>> dp(m+1, vector<int>(n+2, INT_MAX));// 因为要找最小和,题中数值可以为负数,这里我们设默认值为无穷大
        // 2、初始化dp表:需要初始化虚拟节点,即首行、首列、尾列。
        // 首列、尾列已经在创建dp表时初始化为合法值,这里我们处理一下首行即可:将其设为0(可根据状态方程理解,设为无穷大有误)
        for(int j = 0; j < n+2; ++j) dp[0][j] = 0;
        // 3、根据状态方程填表:从上到下
        for(int i = 1; i < m + 1; ++i)
        {
            for(int j = 1; j < n + 1; ++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];// 这里注意三数取小的写法
        }

        // 4、返回目标值:最后一行的最小值
        int ret = INT_MAX;
        for(int j = 1; j < n + 1; ++j)
            ret = min(ret, dp[m][j]);
        return ret;
    }

};

  
  
  
  
  
  
  
  

3.5、最小路径和(medium)

  题源:链接

在这里插入图片描述

  
  

3.5.1、题解

  1)、思路分析
  此题和之前的珠宝最大值思路类似。只是那里求的是最大和,这里求的是最小和。
  从左上角的起点,到右下角的终点,有非常多的路径可供选择,我们需要找出其中和最小的一条路径。

  
  1、确定状态表示: 根据经验+题目要求。此处选择一个常见的思考方式。以(i,j)位置为结尾,那么对于本题,dp[i][j]表示,从起点开始,到达(i,j)位置时,最小的路径和。
  
在这里插入图片描述

  2、确定状态转移方程: 根据题目条件,要想到达(i,j)位置,可以有两种走法:从上往下,从左往右。

1、从上方位置 (i-1, j) 往下走一步到达 (i, j) ,此时的路径和为 dp[i-1][j] + grid[i][j]2、从左边位置 (i, j-1) 往右走一步到达 (i, j) ,此时的路径和为 dp[i][j-1] +grid[i][j]3、题目要求最小和,因此, dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]

  
  3、初始化: 引入虚拟节点进行初始化。需要注意两点。

1、dp表和原先grid表的下标映射关系。(尤其要注意状态转移中的映射关系,以及遍历填表时的隐射关系)
   dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i-1][j-1]
   
2、虚拟节点中的填值,要保证后续填表是正确的。

在这里插入图片描述

  

  4、填表顺序: 根据状态转移方程,填表的顺序就是从上往下填每一行,每一行从左往右填写。

  
  5、返回值: 因为这里引入了虚拟节点,我们要返回的结果是dp[m][n]

  
  
  2)、题解

class Solution {
public:
    // 以动态规划解题:二维数组,dp[i][j]表示以(i,j)位置为结尾,到达(i,j)位置时的最小路径和
    int minPathSum(vector<vector<int>>& grid) {
        int m = grid.size();
        int n = grid[0].size();
        if(m == 0 && n == 0) return 0;// 特殊情况(实则不用处理,特意标出来只是想说明做题有时也要注意看给的变量范围)

        // 1、创建dp表:这里采用虚拟节点的方式,为了方便后续初始化,引入虚拟首行、首列
        vector<vector<int>> dp(m+1, vector<int>(n+1, INT_MAX));// 设值为MAX_INT也是在对初始化做铺垫
        // 2、初始化dp表:由于采用了虚拟节点的方式,这里只需特殊处理dp[0][1]和dp[1][0]两处位置。使得dp[1][1] == grid[0][0]
        dp[0][1] = dp[1][0] = 0;// PS:不能设置其余虚拟行、列位置为0(用状态方程推到算一下就明白了)

        // 3、根据状态转移方程,从左到右,从上到下填表
        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];// 注意这里的映射关系
        }

        // 4、返回目标值
        return dp[m][n];
    }
};

  
  
  
  
  
  
  
  
  
  

3.6、地下城游戏(hard)

  题源:链接

在这里插入图片描述

  
  

3.6.1、题解

  1)、思路分析
  先来分析此题,骑士每次只 向右 或 向下 移动一步后,其健康值要么不变,要么减少,要么增加。我们需要计算的是他顺利从左上角走到右下角,所需要的最低初始健康点数。
  
  1、确定状态表示:
  根据章节一,状态表示通常有两种,①以(i,j)为起点,②以(i,j)为结尾。

  在本题中,如果将状态表示定义成以(i,j)为结尾,那么,dp[i][j]表示:从起点开始,到达(i, j)位置的时候,所需的最低初始健康点数。
  仔细分析过程就会发现问题:在推导状态转移的时候,当前(i,j)位置的健康点数不仅仅受到前面路径的影响,还会受到后续的路径的影响。也就是说,以这种状态表示,不能很好地推导出状态方程。

在这里插入图片描述
  
  基于上述因素,我们选择以(i,j)位置为起点。那么此时dp[i][j]表示,从(i,j)位置开始,到达终点时所需要的最低初始健康点数。
  

  2、确定状态转移方程:
  根据此题,要从(i,j)位置走到终点,有往右或者往下两种走法。
  ①如果骑士在当前(i, j)位置时健康点数降为0或以下,他会立即死亡。因此,我们需要确保骑士在(i, j)位置时的健康点数至少为1(不考虑(i, j)位置本身对健康点数的影响)。
  ②此后,骑士可以选择向右走到(i, j+1)位置或向下走到(i+1, j)位置。我们需要确保骑士有足够的健康点数,能安全地走到右侧或下方的房间。
在这里插入图片描述
  考虑到有两条路径可选,但要求最低健康值,有dp[i][j] = min( dp[i][j+1], dp[i+1][j]) - dungeon[i][j]
  但需要注意,打开一个房间,骑士可能面对减少血条或加血条的情况。而当其遇到的是增加健康点数的魔法球时, dungeon[i][j]为正数,就意味着dp[i][j] 做减法后,有可能会得到一个负值,表示即使骑士的生命值为负数,也能成功到达终点
  但这并不符合题目的逻辑,因为骑士的健康值下降到0后就会死亡。因此就需要保证,无论骑士是否获得了超级大的魔法球,在此过程中骑士的健康值始终为正数。因此,有dp[i][j] = max(1, dp[i][j]))

dp[i][j] = max(1, min( dp[i][j+1], dp[i+1][j]) - dungeon[i][j])
//因为获得血球包后,有可能导致dp表中的健康值变为负数

  
  3、初始化: 这里,选择引入虚拟节点的方式进行初始化。在本题中,会发生越界的是尾行和尾列,因此,虚拟节点需要添加在尾行、尾列。
在这里插入图片描述

  
  4、填表顺序: 根据状态转移方程,我们需要从下往上填每一行,每一行从右往左。
  
  5、返回值: 根据状态表示,需要返回dp[0][0] 的值。
  
  
  
  2)、题解

class Solution {
public:
    // 以动态规划解决此题:二维数组,dp[i][j]表示以(i,j)为起点,从(i,j)位置到达终点时,所需要的最低初始健康点数。
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        int m = dungeon.size();
        int n = dungeon[0].size();

        // 1、创建dp表:这里采用了虚拟节点的方式,多引入了虚拟尾行、尾列,因此dp表的规模为 (m+1)×(n+1)
        vector<vector<int>> dp(m+1, vector<int>(n+1, INT_MAX));// 这里初始化为INT_MAX是为了方便虚拟行、列
        // 2、初始化dp表:在上述创建表时解决了大部分,这里只需处理特殊情况
        dp[m][n-1] = dp[m-1][n] = 1;// 到达终点位置,拯救完公主时,骑士至少还剩下一滴残血。(因为题目要求了要算起点和终点位置的值)
        // 3、根据状态转移方程,从下往上、从右往左填表
        for(int i = m-1; i >=0; --i)
        {
            for(int j = n-1; j >= 0; --j)
            {
                dp[i][j] = max(min(dp[i+1][j], dp[i][j+1]) - dungeon[i][j], 1);
                // 内层min:用于计算从(i,j)位置到达终点,是走右侧划算,还是走下侧划算。
                // 外层max:由于存在加血量的魔法球,那么这里的减法有可能为负数,而我们的健康值不可能为负数,因此在该格处,最少要保持1滴残血。
            }
        }
        // 4、返回目标值
        return dp[0][0];// 从(0,0)位置为起点,到达终点时所需的最低初始健康值。
    }
};

  
  
  
  
  
  
  
  
  

4、简单多状态 dp 问题

4.1、按摩师(easy,打家劫舍Ⅰ)

  题源:链接

在这里插入图片描述

  
  

4.1.1、题解

  1)、思路分析
  先理解题目含义,相邻下标不能选,不相邻的下标,可以连续跳过多个
在这里插入图片描述

  

  1、确定状态表示:
  根据经验+题目要求,这里我们以i为结尾。则dp[i]表示,到达i位置时,所获得的最长预约时长。
  继续分析细化,在本题中,对于某一个位置 i,实则有两种状态:选择接受 i 处的预约,或选择不接受 i 处的预约。因此,实则在 i 处的状态表示可细分为两种:
在这里插入图片描述

1、f[i]:到达i位置时,选择接受i位置处的预约(nums[i] 必选),此时的最长预约时长
2、g[i]:到达i位置时,选择不接受i位置处的预约(nums[i] 必不选),此时的最长预约时长

  
  
  2、确定状态转移方程:
在这里插入图片描述
  由此,状态转移方程为:

f[i] = num[i] + g[i-1]
g[i] = max(f[i-1], g[i-1])

  
  
  3、初始化: 这道题的初始化比较简单,可以不使用虚拟节点。根据状态方程,此时只需要初始化i=0处的位置即可。

1、若接受i=0处的预约,则f[0] = num[0]
2、若不接受i=0处的预约,则g[0] = 0

  
  4、填表顺序: 根据状态转移方程,从左往右,两个表一起填。

  
  5、返回值: 我们的状态表示有两种,两种情况都有可能,那么应该返回 max(f[n - 1], g[n - 1])
  
  
  2)、题解

class Solution {
public:
    // 以动态规划解题:一维数组,dp[i]表示以i位置为结尾,到达i位置时最长预约时长
    int massage(vector<int>& nums) {
        int n = nums.size();
        if(n == 0) return 0;// nums无数据时

        // 1、创建dp表:对i,可细分为两种状态表示,选择i或不选择i。因此我们需要建立不同选择下的状态表
        vector<int> select(n, 0);// 到达i位置时,nums[i]必选,此时的最长预约时长
        vector<int> unselect(n, 0);// 到达i位置时,nums[i]必不选,此时的最长预约时长
        // 2、初始化dp表:分别对上述两个dp表进行初始化。这里一维数组相对简单,可直接初始化(不必搞虚拟节点,当然,要搞也行)
        // 在起点位置处,其前侧无序号,故这里最长时长只考虑nums[0]
        select[0] = nums[0]; // 必选nums[0]
        unselect[0] = 0;// 不选nums[0](实则此步不必要,这里写出来是为了完整)

        // 3、根据状态方程,从左到右填表。(这里是一次同时填两个表)
        for(int i = 1; i < n; ++i)
        {
            // 选择i位置的预约号的情况:
            select[i] = unselect[i-1] + nums[i];// 选了i,i-1位置必然不可选,要找出这种状况下[0,i-1]中最长的时长,再加上当前位置的时长
            // 不选择i位置的预约号的情况:
            unselect[i] = max(select[i-1], unselect[i-1]);// 不选i,i-1位置可选可不选,因此两种状态取最大
        }

        // 4、返回:
        return max(select[n-1], unselect[n-1]);
    }
};

  
  
  
  
  
  
  
  
  
  

4.2、打家劫舍II (medium)

  题源:链接

在这里插入图片描述

  
  

4.2.1、题解

  1)、思路分析
  分析此题可以发现,它和之前4.1的思路基本一致。区别点只是在于题目给出了限制,需要解决首位成环,属于相邻点的问题。
  
  这就意味着,①如果我们选择拿取 i=0 处的现金,那么 i = 1 和 i = n-1 处的现金必然不能被拿取(因为属于相邻位置)。②如果我们选择不拿取 i = 0 处的现金,那么 i = 1 和 i= n-1 处的现金可以被选择。
在这里插入图片描述

  由此,我们就将本题分为了两种情况,对这两种情况分别按照4.1中的思路求取即可。
  最后,我们再汇总上述两种情况,求一个最大值即可。
  
  
  2)、题解
  这里,要注意判断边界问题。

class Solution {
public:
    // 使用动态规划来做:这里,以首位元素区分为两种情况,然后使用打家劫舍Ⅰ的解法。
    // 状态表示:dp[i],以i位置为结尾,表示到达i位置时,窃取到的最高金额。
    int rob(vector<int>& nums) {
        int n = nums.size();
        if(n == 0) return 0;

        // 选择窃取首元素时:此时不能窃取尾元素。对[2,n-2]区间,使用打家劫舍Ⅰ的解法
        int ret1 = ROB(nums, 2, n-2) + nums[0];
        // 选择不窃取首元素时:此时能窃取尾元素。对[1,n-1]区间,使用打家劫舍Ⅰ的解法
        int ret2 = ROB(nums, 1, n-1);

        return max(ret1,ret2);
    }


    int ROB(vector<int>& nums, int begin, int end)
    {
        if(begin > end) return 0;

        int n = nums.size();
        // 1、创建dp表:对同一位置i,这里有两种状态,偷or不偷
        vector<int> select(n, 0);// 到i位置并选择偷窃i处时,最高的偷窃金额
        vector<int> unselect(n, 0);// 到i位置但不选择偷窃i处时,最高的偷窃金额

        // 2、初始化dp表:虽然我们开辟了[0,n-1]的区间,但只偷窃[begin,end]区间中的金额
        select[begin] = nums[begin];
        unselect[begin] = 0;
        // 3、根据状态转移方程,初始化dp表(从左到右)
        for(int i = begin + 1; i <= end; ++i)
        {
            // 选择i:此时不能选择i-1
            select[i] = unselect[i-1] + nums[i];
            // 不选择i:此时i-1任意
            unselect[i] = max(select[i-1], unselect[i-1]);
        }

        // 4、返回
        return max(select[end],unselect[end]);
    }
};

  
  
  
  
  
  
  
  

  
  
  

4.3、删除并获得点数(medium)

  题源:链接

在这里插入图片描述

  
  

4.3.1、题解

  1)、思路分析
  先来分析此题,题目给定数组是乱序的,但假如我们选择了nums[i],要删除的固定就只会是该数左右两侧的数nums[i] - 1nums[i] + 1。因此,我们不妨以一个有序数组来分析:

在这里插入图片描述

  再来分析一下题目含义,它实际是在说,选择 x 数字的时候,相邻的数 x - 1x + 1 是不能被选择的。有没有觉得很熟悉?这不就是4.1、4.2中打家劫舍的问题吗。
在这里插入图片描述
  但要注意此题中的细节,题目给定的数字并非是从0n完整地数字,也就是说,选择nums[i]元素时,其左右两侧的数nums[i] - 1nums[i] + 1 在数组中可能不存在。而根据我们之前做打家劫舍时,使用动态规划的经验,我们在遍历填表时,是需要知道相邻位置处的值的。
  题目中1 <= nums[i] <= 10^4,题中元素最大值为10000,因此,我们不妨创建一个大小为10001的hash数组,hash数组中下标为x的位置,即num[i] = x 的元素。这样一来,只需遍历一次原num数组获得hash数组,对hash数组来一次打家劫舍即可

在这里插入图片描述

  
  
  2)、题解
  

class Solution {
public:
    int deleteAndEarn(vector<int>& nums) {
        // 先统计一遍数组nums,建立hash映射
        const int N = 10001;
        int hash[N] = {0};
        for (int i = 0; i < nums.size(); ++i)
            hash[nums[i]] += nums[i];

        // 开始打家劫舍:对数组hash,选择任意x,不能选择其相邻两数x-1、x+1,求最大和。

        // 1、创建dp表
        vector<int> f(N, 0); // f[i]:到达i位置时,必然选择hash[i],此时获得的最大点数
        vector<int> g(N, 0); // g[i]:到达i位置时,必然不选hash[i],此时获得的最大点数

        // 2、初始化
        f[0] = hash[0]; // 选择0处下标
        g[0] = 0;       // 不选择0处下标

        // 3、填表:从左到右
        for (int i = 1; i < N; ++i) 
        {
            f[i] = g[i - 1] + hash[i];
            g[i] = max(g[i - 1], f[i - 1]);
        }
        
        // 4、返回:
        return max(f[N - 1], g[N - 1]);
    }
};

  
  
  
  
  
  
  
  
  
  

4.4、粉刷房子(medium)

  题源:链接

在这里插入图片描述

  
  

4.4.1、题解

  1)、思路分析
  先来分析题目中 n x 3 的正整数矩阵 costs :
在这里插入图片描述

  
  1、确定状态表示: 根据经验+题目要求,这里我们选择以i为结尾,则dp[i]表示,粉刷到 i 位置时,所需要的最小花费。
  根据题目,我们粉刷时可以选择三种颜色,实则这里的状态表示,可以根据粉刷的颜色进行细化。 一种方法是用三个一维数组分别表示三种动态规划的状态。另外一种方式,我们也可以向题目给出的costs数字学习,用一个二维数组表示三种状态:dp[i][j]

  这里我们选择使用二维数组一次同时描述上述三种状态表示:

和题目照应,二维数组的列表示选择的颜色状态:0为红,1为蓝,2为绿。
0、dp[i][0]:粉刷到第i号房间时,将第i个位置粉刷为红色,此时所需要的最小花费
1、dp[i][1]:粉刷到第i号房间时,将第i个位置粉刷为蓝色,此时所需要的最小花费
2、dp[i][2]:粉刷到第i号房间时,将第i个位置粉刷为绿色,此时所需要的最小花费

  
  
  2、确定状态转移方程:

在这里插入图片描述
  根据上图分析可得:

0、把i处粉成红色:dp[i][0] = min(dp[i][1],dp[i][2]) + costs[i][0];
1、把i处粉成蓝色:dp[i][1] = min(dp[i][0],dp[i][2]) + costs[i][1];
2、把i处粉成绿色:dp[i][2] = min(dp[i][0],dp[i][1]) + costs[i][2];

  
  

  3、初始化: 这里,我们引入虚拟节点,添加一列。此时需要注意两点。
  ①下标的映射关系:上述推导的dp表,以及后续填表时遍历的位置,这些涉及下标的地方,都需要注意下标映射关系。
  ②虚拟节点的初始值要保证填表正确。

在这里插入图片描述

  
  
  4、填表顺序: 根据状态转移方程,这三个状态之间是相互依赖的,因此填表时,需要从左往右,三个表一起填。
  
  
  5、返回值: 根据状态表示,应该返回最后一个位置粉刷上三种颜色情况下的最小值。
  
  
  
  2)、题解

class Solution {
public:
    int minCost(vector<vector<int>>& costs) {
        int m = costs.size(); // 有多少个房子

        // 1、创建dp表并初始化
        vector<vector<int>> dp(m + 1, vector<int>(3, 0)); // m+1行,3列
        // 2、填表:注意下标映射
        for (int i = 1; i < m + 1; ++i) {
            dp[i][0] = min(dp[i - 1][1], dp[i - 1][2]) + costs[i - 1][0]; // 选i为红色
            dp[i][1] = min(dp[i - 1][0], dp[i - 1][2]) + costs[i - 1][1]; // 选i为蓝色
            dp[i][2] = min(dp[i - 1][1], dp[i - 1][0]) + costs[i - 1][2]; // 选i为绿色
        }
        // 3、返回
        return min(min(dp[m][0], dp[m][1]), dp[m][2]);
    }
};

  
  
  
  
  
  
  
  
  
  
  
  
  

4.5、买卖股票的最佳时机含冷冻期(medium)

  题源:链接

在这里插入图片描述

  此类系列题扩展学习股票问题系列通解
  

4.5.1、题解

  1)、思路分析
  根据题目含义,对某一天的股票,存在三种状态:已买入、可交易(待买入)、冷冻期。这三者之间有如下关系:

1、处于「买入」状态的时候,我们现在有股票,此时不能买股票,只能继续持有股票,或者卖出股票;
2、处于「卖出」状态的时候:如果在冷冻期,不能买⼊股票;如果不在冷冻期,才能买⼊股票。

  

  1、确定状态表示: 通常,根据经验+题目要求,我们在描述状态时一般有两种情况:①以某个位置为结尾,……;②以某个位置为开始,……。
  这里我们选择前者,则有dp[i]表示,第 i 天结束后,此时的最大利润。但根据上述分析,i 天存在多个状态,因此状态表示dp[i]可以细化为上述三种状态:第 i 天结束后,处于已买入、可交易(待买入)、冷冻期时,此时的最大利润值。
  这里,我们用一个二维数组同时表示这三种状态,则有:

1、dp[i][0]:第 i 天结束后,处于"已买入"状态,此时的最大利润。
2、dp[i][1]:第 i 天结束后,处于"可交易"(即待买入)状态,此时的最大利润。
3、dp[i][2]:第 i 天结束后,处于"冷冻期"状态,此时的最大利润。

  
  
  2、确定状态转移方程: 可以看到,dp表同时存在多个状态,为了方便推导状态方程,这里我们可以画图分析各个状态之间的联系(此类图也称为“状态机”)。
在这里插入图片描述

  dp[i]表示第 i 天结束后的状态。要推导获取 i 处的状态方程,主要看第 i-1 天结束时的状态(dp[i-1]),以及第 i 天当天的操作(prices[i])。
  

  Ⅰ、分析已买入:
  ①“已买入”→“已买入”: 若第 i-1 天结束后,处于“已买入”状态,能否让第 i 天结束后,处于“已买入”状态?
  分析:当然可以,第 i-1 天结束后处于已买入,说明此时我们手里持有股票,那么第 i 天我们什么也不做(既不卖出也不买入),那第 i 天结束后,我们手中仍旧持有股票,即“已买入”状态。

  ②“冷冻期”→“已买入”: 若第i-1天结束后,处于“冷冻期”状态,能否让第 i 天结束后,处于“已买入”状态?
  分析:不可以。因为冷冻期我们不能买入股票,所以无法在第i天结束后,进入“已买入”状态。

  ③“可交易”→“已买入”: 若第i-1天结束后,处于“可交易”状态,能否让第 i 天结束后,处于“已买入”状态?
  分析:可以。第 i-1 天结束后,处于“可交易”状态,说明此时我们手里没有股票,是可以在第 i 天进行买入的,那么当第 i 天结束后,我们当然就处于了“已买入”状态。需要注意,第i天进行买入,此时利润减少,-prices[i]
在这里插入图片描述

  
  
  Ⅱ、分析冷冻期:
  ①“冷冻期”→“冷冻期”: 若第 i-1 天结束后,处于“冷冻期”状态,能否让第 i 天结束后,处于“冷冻期”状态?
  分析:不可以,因为冷冻期只能维持一天,在第 i 天结束后,此时就进入了“可交易”状态。

  ②“可交易”→“冷冻期”: 若第i-1天结束后,处于“可交易”状态,能否让第 i 天结束后,处于“冷冻期”状态?
  分析:不可以。第 i-1天结束我们手里没有股票,不能在第i天卖出,自然无法进入冷冻期。

  ③“已买入”→“冷冻期”: 若第i-1天结束后,处于“已买入”状态,能否让第 i 天结束后,处于“冷冻期”状态?
  分析:可以。这说明第i-1天结束后,我们手里是持有股票的,只需要在第i天卖出,自然就进入了冷冻期。需要注意,卖出股票会获取到利润,+prices[i]
在这里插入图片描述

  
  
  Ⅲ、分析可交易:
  ①“可交易”→“可交易”: 若第 i-1 天结束后,处于“可交易”状态,能否让第 i 天结束后,处于“可交易”状态?
  分析:可以。第 i-1 天结束后处于可交易,说明此时我们手里没有股票,那么第 i 天我们什么也不做(既不卖出也不买入),那第 i 天结束后,我们手中仍旧没有股票,即“可交易”状态。

  ②“冷冻期”→“可交易”: 若第i-1天结束后,处于“冷冻期”状态,能否让第 i 天结束后,处于“可交易”状态?
  分析:当然可以。处于冷冻期的第 i 天本身就不能买入股票,这一天什么都不做,那么在第 i 天结束时,冷冻期结束自然就进入了可交易状态。

  ③“已买入”→“可交易”: 若第 i-1天结束后,处于“已买入”状态,能否让第 i 天结束后,处于“可交易”状态?
  分析:不可以。第 i-1 天结束时处于“已买入”状态,说明此时手头持有股票,在第 i 天时,我们要么只能卖出股票进入冷冻期,要么只能继续持有股票保持在已买入状态。

在这里插入图片描述

  
  综上分析,可得最终结果图如下。我们可根据此推导三个状态表示之间的关系:

1、第i天结束后,处于"已买入"状态,到达该情况有两种方式,我们只要其中最大利润,则有:
   dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);
   
2、第i天结束后,处于"可交易"状态,到达该情况有两种方式,我们只要其中最大利润,则有:
   dp[i][1] = max(dp[i-1][1], dp[i-1][2]);
   
3、第i天结束后,处于"冷冻期"状态,到达该情况有一种方式,则最大利润为:
   dp[i][2] = dp[i-1][0] + prices[i];

在这里插入图片描述

  
  
  
  
  
  3、初始化: 根据上述三个状态方程,为了防止越界,需要对 dp[0][j]位置进行初始化。根据状态表示,有

1、dp[0][0] = -prices[0];// 必须把这天的股票买了,才能在这天结束后进入已买入状态
2、dp[0][1] =  0; // 啥也不⽤⼲即可
3、dp[0][2] =  0; // ⼿上没有股票,当天买入当天卖出,就能进入冷冻期,此时收益为0

  
  
  4、填表顺序: 根据上述分析,三个表之间存在相互依赖关系,因此需要一起填写。对每个表,按照天数累加填写。
  
  
  5、返回值: 返回最后一天结束时的最大利益值。实则只需要看最后一天的“可交易”状态和“冷冻期”状态。因为最后一天结束后处于“买入”状态,此时手里有票,势必会比其它两种状态亏损。

max(dp[n-1][1],dp[n-1][2]);
max(max(dp[n-1][1],dp[n-1][2]),dp[n-1][0]);// 不放心也可以三者进行比较取最大值

  
  
  
  2)、题解
  一次遍历,时间复杂度为O(n)。常数列可以忽略,空间复杂度为O(n)。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();//一共存在多少天
        
        // 1、创建状态表
        vector<vector<int>> dp(n,vector<int>(3,0));// 这里用一个二维数组同时表示三种状态
        // dp[i][0]:第i天结束后,处于“买入”状态,此时的最大利润
        // dp[i][1]:第i天结束后,处于“可交易”状态,此时的最大利润
        // dp[i][2]:第i天结束后,处于“冷冻期”状态,此时的最大利润

        // 2、初始化:直接初始化
        dp[0][0] = -prices[0];
        dp[0][1] = dp[0][2] = 0;

        // 3、填表:从左到右,三表一起填(因为三者动态转移方程相互依赖)
        for(int i = 1; i < n; ++i)
        {
            dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);
            dp[i][1] = max(dp[i-1][1],dp[i-1][2]);
            dp[i][2] = dp[i-1][0] + prices[i];
        }

        // 4、返回最大值
        return max(dp[n-1][1],dp[n-1][2]);//dp[n-1][0]可加可不加

    }
};

  
  
  
  
  
  
  
  
  

4.6、买卖股票的最佳时期含手续费(medium)

  题源:链接

在这里插入图片描述

  
  

4.6.1、题解

  1)、思路分析
  有了上一题的经验,此题学习理解起来会相对顺畅许多。这里仍旧以动态规划来解题。
  先来分析题目,对于某一天的股票,我们可以做两种操作:买入、卖出。当我们完成这样一个完整流程时,算作一笔交易,此时需要支付一次手续费。
  
  
  1、确定状态表示: 根据经验+题目要求,这里选择以 i 为结尾。则 dp[i] 表示,当第 i 天结束时,所拥有的最大利润值。继续细分:对第 i 天,可以有买入(持有股票)、卖出(未持有股票)两种状态。

1、f[i]:第 i 天结束之后,处于"买入"状态(持有股票),此时所拥有的最大利润。
2、g[i]:第 i 天结束之后,处于"卖出"状态(未持有股票),此时所拥有的最大利润。
// 此处,可以使用一个n×2的二维数组表示,也可以如上述一样,分别用两个一维数组表示。

  
  2、确定状态转移方程: 对于此类状态彼此相互转换的,可以通过画图分析,这样能做到不重不漏。
在这里插入图片描述

  Ⅰ、分析“买入”:
  ①“买入”→“买入”: 若第 i-1 天结束之后,处于“买入”状态,能否让第 i 天结束之后,仍旧处于“买入”状态?
  分析:可以。在第 i 天时,我们什么都不干(不卖出股票),即可在第 i 天结束之后,处于“买入”状态。

  ②“卖出”→“买入”: 若第 i-1 天结束之后,处于“卖出”状态(此时手里无股票),能否让第 i 天结束之后,处于“买入”状态?
  分析:可以。只需要我们在第 i 天时,买入股票,即可在第 i 天结束之后,处于“买入”状态。需要注意,买入股票,需要减去本金。
  
  Ⅱ、分析“卖出”:
  ①“卖出”→“卖出”: 若第 i-1 天结束之后,处于“卖出”状态,能否让第 i 天结束之后,仍旧处于“卖出”状态?
  分析:可以。在第 i 天时,我们什么都不干(不买入股票),即可在第 i 天结束之后,处于“卖出”状态。此时第 i 天结束后的股票最大利润,就是第i-1天结束后,卖出状态的最大利润。

  ②“买入”→“卖出”: 若第 i-1 天结束之后,处于“买入”状态(此时手里持有股票),能否让第 i 天结束之后,处于“卖出”状态?
  分析:可以。只需要我们在第 i 天时,卖出股票,即可在第 i 天结束之后,处于“买入”状态。需要注意,卖出股票,获得利润,完成一次交易,需要减去手续费。
  
  综上,最终状态方程为:

1、f[i] = max(f[i-1], g[i-1] - prices[i]);// max(第i天啥也不做,第i天买入股票)
2、g[i] = max(g[i-1], f[i-1] + prices[i] - free);// max(第i天啥也不做,第i天卖出股票完成一次交易)

  
  

  3、初始化: 由于此处初始化比较简单,我们直接初始化即可。根据题目,为了防止越界,这里需要对i=0处进行初始化。

1、f[0] = -prices[0]
2、g[0] = 0

  

  4、填表顺序: 两种状态相互依赖,填表一起填,从左往右按照天数增加填写。

  5、返回值: 实际只需要返回f[n-1]即可,因为最后一天买入股票,利润受到亏损。

return f[n-1];
return max(f[n-1],g[n-1]);//这样写也行

  
  
  
  2)、题解

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();// 总天数
        
        // 1、创建dp表并确定状态表示
        vector<int> f(n,0);// 第 i 天结束之后,处于"买入"状态(持有股票),此时所拥有的最大利润。
        vector<int> g(n,0);// 第 i 天结束之后,处于"卖出"状态(未持有股票),此时所拥有的最大利润。
        
        // 2、初始化
        f[0] = -prices[0];
        g[0] = 0;//可省略
        
        // 3、填表
        for(int i = 1; i < n; ++i)
        {
            f[i] = max(f[i-1], g[i-1] - prices[i]);
            g[i] = max(g[i-1], f[i-1] + prices[i] -fee);
        }

        // 4、返回
        return max(f[n-1],g[n-1]);

    }
};

  
  
  
  
  
  
  
  
  
  

4.7、买卖股票的最佳时机III(hard)

  题源:链接

在这里插入图片描述  
  

4.7.1、题解

  1)、思路分析
  分析此题,对某一天 i ,我们能做两种操作:买入股票,卖出股票。对于所给的 n 天,我们需要控制交易次数在 2 次以内。这就说明,对于某一天 i 的状态,同时受到上述买卖状态,以及交易次数的影响。
  
  
  1、确定状态表示: 根据经验+题目要求,这里以 i 为结尾进行分析。则dp[i]表示,第 i 天结束之后,所获得的最大利润。但由于第 i 天的状态同时受到买卖状态和交易次数的影响。若我们单独用f[i]g[i]两个一维数组分别表示买入、卖出两种状态,这远远不够,因为在这两种状态之下,还可以细分出各种交易状态。因此,我们在此基础上再追加一维,表示交易次数,即f[i][j]g[i][j]。(当然,这里也可以直接用一个三维数组表示dp[i][j][k]

f[i][j]: 第 i 天结束时,完成了 j 次交易,处于"买入"状态下的最大的利润值。
g[i][j]: 第 i 天结束时,完成了 j 次交易,处于"卖出"状态下的最大的利润值。

  
  
  2、确定状态转移方程: 这里的状态转换和之前类似,但需要注意
在这里插入图片描述
  对于f[i][j] ,要求第 i 天结束时,完成了 j 次交易,处于"买入"状态下的最大的利润值。有两种方式到这个状态:

1、在 i-1 天结束时,交易了 j 次,处于"买入"状态,第 i 天啥也不干,仍旧处于"买入"状态,则有最大利润为: f[i-1][j] ;
2、在 i-1 天结束时,交易了 j 次,处于"卖出"状态,第 i 天的时候把股票买了,则有最大利润为: g[i-1][j]-prices[i] 。

综上,第 i 天的最大利润为: f[i][j] = max( f[i - 1][j], g[i - 1][j] - prices[i])

  对于g[i][j] ,要求第 i 天结束时,完成了 j 次交易,处于"卖出"状态下的最大的利润值。也有两种方式到达这个状态:

1、在 i-1 天结束时,交易了 j 次,处于"卖出"状态,第 i 天啥也不干,仍旧处于"卖出"状态,则有最大利润为: g[i - 1][j] ;
2、在 i-1 天结束时,交易了 j-1 次,处于"卖出"状态,在第 i 天把股票卖出,增加一次交易次数到 j 次,则有最大利润为: f[i - 1][j -1] + prices[i]。 

综上,第 i 天的最大利润为: g[i][j] = max( g[i - 1][j], f[i - 1][j - 1] + prices[i])
// 要注意理解这里的f[i - 1][j - 1]

  
  
  3、初始化: 根据上述推导的两个状态方程,对f[i][j],需要对i=0行进行初始化。但对g[i][j],需要对i = 0行、j=0列进行初始化。
  按照我们之前的做题经验,可以选择直接初始化,也可以选择引入虚拟节点。
  除了上述引入虚拟节点,其实我们还可以使用 if 条件判断,稍微改变一下状态转移方程,然后就方便我们直接进行初始化:

1、f[i][j] = max( f[i - 1][j], g[i - 1][j] - prices[i])// 不变

2、g[i][j] = g[i - 1][j];// 对于(啥也不干)这种状态,j = 0列是不会发生越界行为的。因此我们可以先将赋值为这种状态
   if(j >= 1) 			// 然后,在(从买入→卖出)交易次数成立时,再进行最大值的取舍(也就是说,j=0时,都没有买入过票,更别谈卖出了。)
   g[i][j] = max( g[i][j], f[i - 1][j - 1] + prices[i])

  将状态转移方程修改为上述值后,来判断初始化,即 i = 0 处于第一天时:

在这里插入图片描述

  关于这里的取值,如果不做运算,一般我们想到的无穷,是对应类型的最大、最小值。比如此处的int类型,则选择INT_MAX、INT_MIN,但这里,在负无穷的基础上进行减法运算,则存在数据溢出的风险,因此,一般情况下,选择正负无穷时,我们需要折办:

+∞ : 选择 0x3f3f3f //16进制,INT_MAX的一半
-∞ : 选择 -0x3f3f3f

  
  
  4、填表顺序: 从上往下填每一行,每行从左往右,两个表一起填。

  
  
  5、返回值: 首先,能确定的是,最大利润在卖出状态的最后一行(若最后一行是买入状态,此时手里还持有股票,达不到最大利润的)。但由于我们并不清楚第 j 次卖出获利最大,因此需要对卖出状态的最后一行进行遍历找寻最大值。
在这里插入图片描述

  
  
  
  2)、题解

class Solution {
    const int minint = -0x3f3f3f; // 定义负无穷,用于初始化
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();

        // 1、创建dp表,确定状态表示(这里总交易次数不超过2)
        vector<vector<int>> f(n, vector<int>(3, minint)); // f[i][j]: 第 i 天结束时,完成了 j 次交易,处于"买入"状态下的最大的利润值。
        vector<vector<int>> g(n, vector<int>(3, minint)); // g[i][j]: 第 i 天结束时,完成了 j 次交易,处于"卖出"状态下的最大的利润值。

        // 2、初始化
        f[0][0] = -prices[0];
        g[0][0] = 0;

        // 3、填表
        for (int i = 1; i < n; ++i) 
        {
            for (int j = 0; j < 3; ++j) 
            {
                f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i]);

                g[i][j] = g[i - 1][j];
                if (j >= 1)
                    g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);
            }
        }

        // 4、返回
        int ret = 0;
        for (int j = 0; j < 3; ++j)
            ret = max(g[n - 1][j], ret);

        return ret;
    }
};

  
  
  
  
  
  
  
  

4.8、买卖股票的最佳时机IV(hard)

  题源:链接

在这里插入图片描述

  
  

4.8.1、题解

  1)、思路分析
  此题思路和上题基本一致,区别在于上一题中,交易次数最多不超过2次。本题中,给定交易次数最多不超过 k 次。
  由于我们的交易次数是不会超过整个天数的⼀半的,因此我们可以先把k处理⼀下,优化⼀下问题的规模:

k = min(k, n / 2)

  
  2)、题解

class Solution {
    const int minint = -0x3f3f3f; // 定义负无穷,拥有初始化
public:
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        k = min(k, n / 2);//进行优化

        // 1、创建dp表,确定状态表示(这里总交易次数不超过k)
        vector<vector<int>> f(n, vector<int>(k+1, minint)); // f[i][j]: 第 i 天结束时,完成了 j 次交易,处于"买入"状态下的最大的利润值。
        vector<vector<int>> g(n, vector<int>(k+1, minint)); // g[i][j]: 第 i 天结束时,完成了 j 次交易,处于"卖出"状态下的最大的利润值。

        // 2、初始化
        f[0][0] = -prices[0];
        g[0][0] = 0;

        // 3、填表
        for (int i = 1; i < n; ++i) 
        {
            for (int j = 0; j <= k; ++j) 
            {
                f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i]);

                g[i][j] = g[i - 1][j];
                if (j >= 1)
                    g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);
            }
        }

        // 4、返回
        int ret = 0;
        for (int j = 0; j <= k; ++j)
            ret = max(g[n - 1][j], ret);

        return ret;
    }
};

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

Fin、共勉。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值