专题三:简单多状态 dp 问题

78 阅读23分钟

1 按摩师

打家劫舍问题的变形~

⼩偷变成了按摩师

1.1 题目链接

面试题 17.16. 按摩师

1.2 题目描述

一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。

注意: 本题相对原题稍作改动

 

示例 1:

输入: [1,2,3,1]
输出: 4
解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4

示例 2:

输入: [2,7,9,3,1]
输出: 12
解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12

示例 3:

输入: [2,1,4,5,3,1,1,3]
输出: 12
解释: 选择 1 号预约、 3 号预约、 5 号预约和 8 号预约,总时长 = 2 + 4 + 3 + 3 = 12

1.3 解法(动态规划)

算法思路

  1. 状态表⽰:

    对于简单的线性 dp ,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:

    • i. 以某个位置为结尾,巴拉巴拉;
    • ii. 以某个位置为起点,巴拉巴拉。

    这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:

    dp[i] 表⽰:选择到 i 位置时,此时的最⻓预约时⻓。

    但是我们这个题在 i 位置的时候,会⾯临「选择」或者「不选择」两种抉择,所依赖的状态需要细分:

    • f[i] 表⽰:选择到 i 位置时, nums[i] 必选,此时的最⻓预约时⻓;
    • g[i] 表⽰:选择到 i 位置时, nums[i] 不选,此时的最⻓预约时⻓。
  2. 状态转移⽅程:

    因为状态表⽰定义了两个,因此我们的状态转移⽅程也要分析两个:

  • 对于 f[i] :

    • 如果 nums[i] 必选,那么我们仅需知道 i - 1 位置在不选的情况下的最⻓预约时⻓,然后加上 nums[i] 即可,因此 f[i] = g[i - 1] + nums[i] 。
  • 对于 g[i] :

    • 如果 nums[i] 不选,那么 i - 1 位置上选或者不选都可以。因此,我们需要知道 i - 1 位置上选或者不选两种情况下的最⻓时⻓,因此 g[i] = max(f[i - 1], g[i - 1]) 。
  1. 初始化:

    这道题的初始化⽐较简单,因此⽆需加辅助节点,仅需初始化 f[0] = nums[0], g[0] = 0 即可。

  2. 填表顺序

    根据「状态转移⽅程」得「从左往右,两个表⼀起填」。

  3. 返回值

    根据「状态表⽰」,应该返回 max(f[n - 1], g[n - 1]) 。

1.4 C++算法代码:

class Solution {
public:
    int massage(vector<int>& nums) {
        int n = nums.size();
        if(n == 0) return 0;

        vector<int> f(n);
        auto g = f;
        f[0] = nums[0];
        for(int i = 1; i < n; i++)
        {
            f[i] = g[i - 1] + nums[i];
            g[i] = max(f[i - 1], g[i - 1]);
        }
        return max(f[n - 1], g[n - 1]);
    }
};

2 打家劫舍 II

2.1 题目链接

213. 打家劫舍 II

2.2 题目描述

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

 

示例 1:

输入: nums = [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入: nums = [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4

示例 3:

输入: nums = [1,2,3]
输出: 3

 

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000

2.3 解法(动态规划)

算法思路

这⼀个问题是「打家劫舍I」问题的变形。

上⼀个问题是⼀个「单排」的模式,这⼀个问题是⼀个「环形」的模式,也就是⾸尾是相连的。但

是我们可以将「环形」问题转化为「两个单排」问题:

  • a. 偷第⼀个房屋时的最⼤⾦额 x ,此时不能偷最后⼀个房⼦,因此就是偷 [0, n - 2] 区间的房⼦;
  • b. 不偷第⼀个房屋时的最⼤⾦额 y ,此时可以偷最后⼀个房⼦,因此就是偷 [1, n - 1] 区间的房⼦;

两种情况下的「最⼤值」,就是最终的结果。

因此,问题就转化成求「两次单排结果的最⼤值」。

2.4 C++算法代码:

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        // 两种情况下的最大值
        return max(nums[0] + rob1(nums, 2, n - 2), rob1(nums, 1 , n - 1));
    }
    int rob1(vector<int>& nums, int left, int right)
    {
        if(left > right) return 0;

        int n = nums.size();
        vector<int> f(n);
        auto g = f;
        f[left] = nums[left];
        for(int i = left + 1; i <= right; i++)
        {
            f[i] = g[i - 1] + nums[i];
            g[i] = max(g[i - 1], f[i - 1]);
        }
        return max(f[right], g[right]);
    }
};

3 删除并获得点数

3.1 题目链接

740. 删除并获得点数

3.2 题目描述

给你一个整数数组 nums ,你可以对它进行一些操作。

每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。

开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。

 

示例 1:

输入: nums = [3,4,2]
输出: 6
解释:
删除 4 获得 4 个点数,因此 3 也被删除。
之后,删除 2 获得 2 个点数。总共获得 6 个点数。

示例 2:

输入: nums = [2,2,3,3,3,4]
输出: 9
解释:
删除 3 获得 3 个点数,接着要删除两个 2 和 4 。
之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。
总共获得 9 个点数。

 

提示:

  • 1 <= nums.length <= 2 * 104
  • 1 <= nums[i] <= 104

3.3 解法(动态规划)

算法思路

其实这道题依旧是「打家劫舍I」问题的变型。

我们注意到题⽬描述,选择 x 数字的时候, x - 1 与 x + 1 是不能被选择的。像不像「打家劫舍」问题中,选择 i 位置的⾦额之后,就不能选择 i - 1 位置以及 i + 1 位置的⾦额呢~

因此,我们可以创建⼀个⼤⼩为 10001 (根据题⽬的数据范围)的 hash 数组,将 nums 数组中每⼀个元素 x ,累加到 hash 数组下标为 x 的位置处,然后在 hash 数组上来⼀次「打家劫舍」即可。

3.4 C++算法代码:

class Solution {
public:
    int deleteAndEarn(vector<int>& nums) {
        const int N = 10001;
        // 1. 预处理
        int arr[N] = {0};
        for(auto e : nums) arr[e] += e;

        // 2. 在 arr 数组上,做一次 “打家劫舍” 问题
        // 创建 dp 表
        vector<int> f(N);
        auto g = f;
        // 填表
        for(int i = 1; i < N; i++)
        {
            f[i] = g [i - 1] + arr[i];
            g[i] = max(f[i - 1], g[i - 1]);
        }
        // 返回结果
        return max(f[N - 1], g[N - 1]);
    }
};

4 粉刷房子

4.1 题目链接

LCR 091. 粉刷房子

4.2 题目描述

假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。

当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3 **的正整数矩阵 costs 来表示的。

例如,costs[0][0] 表示第 0 号房子粉刷成红色的成本花费;costs[1][2] 表示第 1 号房子粉刷成绿色的花费,以此类推。

请计算出粉刷完所有房子最少的花费成本。

 

示例 1:

输入: costs = [[17,2,17],[16,16,5],[14,3,19]]
输出: 10
解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色 。
     最少花费: 2 + 5 + 3 = 10

示例 2:

输入: costs = [[7,6,2]]
输出: 2

 

提示:

  • costs.length == n
  • costs[i].length == 3
  • 1 <= n <= 100
  • 1 <= costs[i][j] <= 20

4.3 解法(动态规划)

算法思路

  1. 状态表⽰:

    对于线性 dp ,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:

    • i. 以某个位置为结尾,巴拉巴拉;
    • ii. 以某个位置为起点,巴拉巴拉。 这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:

    但是我们这个题在 i 位置的时候,会⾯临「红」「蓝」「绿」三种抉择,所依赖的状态需要细分:

    • dp[i][0] 表⽰:粉刷到 i 位置的时候,最后⼀个位置粉刷上「红⾊」,此时的最⼩花费;
    • dp[i][1] 表⽰:粉刷到 i 位置的时候,最后⼀个位置粉刷上「蓝⾊」,此时的最⼩花费;
    • dp[i][2] 表⽰:粉刷到 i 位置的时候,最后⼀个位置粉刷上「绿⾊」,此时的最⼩花费。
  2. 状态转移⽅程:

    因为状态表⽰定义了三个,因此我们的状态转移⽅程也要分析三个:

    • 对于 dp[i][0] :
      • 如果第 i 个位置粉刷上「红⾊」,那么 i - 1 位置上可以是「蓝⾊」或者「绿⾊」。因此我们需要知道粉刷到 i - 1 位置上的时候,粉刷上「蓝⾊」或者「绿⾊」的最⼩花费,然后加上 i位置的花费即可。于是状态转移⽅程为: dp[i][0] = min(dp[i - 1][1], dp[i - 1][2]) + costs[i - 1][0] ;
    • 同理,我们可以推导出另外两个状态转移⽅程为:
      • dp[i][1] = min(dp[i - 1][0], dp[i - 1][2]) + costs[i - 1][1] ;
      • dp[i][2] = min(dp[i - 1][0], dp[i - 1][1]) + costs[i - 1][2] 。
  3. 初始化:

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

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

    在本题中,添加⼀个节点,并且初始化为 0 即可。

  4. 填表顺序

    根据「状态转移⽅程」得「从左往右,三个表⼀起填」。

  5. 返回值

    根据「状态表⽰」,应该返回最后⼀个位置粉刷上三种颜⾊情况下的最⼩值,因此需要返回:

    min(dp[n][0], min(dp[n][1], dp[n][2])) 。

4.4 C++算法代码:

class Solution {
public:
    int minCost(vector<vector<int>>& costs) {
        int n = costs.size();
        vector<vector<int>> dp(n + 1, vector<int>(3));
        for(int i = 1;i <= n; i++)
        {
            dp[i][0] = min(dp[i - 1][1], dp[i - 1][2]) + costs[i - 1][0];
            dp[i][1] = min(dp[i - 1][0], dp[i - 1][2]) + costs[i - 1][1];
            dp[i][2] = min(dp[i - 1][0], dp[i - 1][1]) + costs[i - 1][2];
        }
        return min(min(dp[n][0],dp[n][1]),dp[n][2]);
    }
};

5 买卖股票的最佳时机含冷冻期

5.1 题目链接

309. 买卖股票的最佳时机含冷冻期

5.2 题目描述

给定一个整数数组prices,其中第 **prices[i] 表示第 i 天的股票价格 。​

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

 

示例 1:

输入: prices = [1,2,3,0,2]
输出: 3 
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

示例 2:

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

 

提示:

  • 1 <= prices.length <= 5000
  • 0 <= prices[i] <= 1000

5.3 解法(动态规划)

算法思路

  1. 状态表⽰:

    对于线性 dp ,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:

    • i. 以某个位置为结尾,巴拉巴拉;
    • ii. 以某个位置为起点,巴拉巴拉。

    这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:

    由于有「买⼊」「可交易」「冷冻期」三个状态,因此我们可以选择⽤三个数组,其中:

    • dp[i][0] 表⽰:第 i 天结束后,处于「买⼊」状态,此时的最⼤利润;
    • dp[i][1] 表⽰:第 i 天结束后,处于「可交易」状态,此时的最⼤利润;
    • dp[i][2] 表⽰:第 i 天结束后,处于「冷冻期」状态,此时的最⼤利润。
  2. 状态转移⽅程:

    我们要谨记规则:

    • i. 处于「买⼊」状态的时候,我们现在有股票,此时不能买股票,只能继续持有股票,或者卖出股票;
    • ii. 处于「卖出」状态的时候:
      • 如果「在冷冻期」,不能买⼊;
      • 如果「不在冷冻期」,才能买⼊。
  • 对于 dp[i][0] ,我们有「两种情况」能到达这个状态:

    • i. 在 i - 1 天持有股票,此时最⼤收益应该和 i - 1 天的保持⼀致: dp[i - 1]
    • ii. 在i 天买⼊股票,那我们应该选择 i - 1 天不在冷冻期的时候买⼊,由于买⼊需要花钱,所以此时最⼤收益为: dp[i - 1][1] - prices[i]
    • 两种情况应取最⼤值,因此: dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]) 。
  • 对于 dp[i][1] ,我们有「两种情况」能到达这个状态:

    • i. 在 i - 1 天的时候,已经处于冷冻期,然后啥也不⼲到第 i 天,此时对应的状态为:dp[i - 1][2] ;
    • ii. 在 i - 1 天的时候,⼿上没有股票,也不在冷冻期,但是依旧啥也不⼲到第 i 天,此时对应的状态为 dp[i - 1][1] ;

    两种情况应取最⼤值,因此: dp[i][1] = max(dp[i - 1][1], dp[i - 1][2]) 。

  • 对于 dp[1][i] ,我们只有「⼀种情况」能到达这个状态:

    • i. 在 i - 1 天的时候,卖出股票。
  • 因此对应的状态转移为: dp[i][2] = dp[i - 1][0] + prices[i] 。

  1. 初始化:

    三种状态都会⽤到前⼀个位置的值,因此需要初始化每⼀⾏的第⼀个位置:

    • dp[0][0] :此时要想处于「买⼊」状态,必须把第⼀天的股票买了,因此 dp[0][0] = - prices[0] ;
    • dp[0][1] :啥也不⽤⼲即可,因此 dp[0][1] = 0 ;
    • dp[0][2] :⼿上没有股票,买⼀下卖⼀下就处于冷冻期,此时收益为 0 ,因此 dp[0][2] = 0 。
  2. 填表顺序:

    根据「状态表⽰」,我们要三个表⼀起填,每⼀个表「从左往右」。

  3. 返回值:

    应该返回「卖出状态」下的最⼤值,因此应该返回 max(dp[n - 1][1], dp[n - 1][2]) 。

5.4 C++算法代码:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n, vector<int>(3));
        //dp[i][0] 买入状态  手里有一只股票
        //dp[i][1] 可交易状态 手里没有股票  可买入
        //dp[i][2] 冷冻状态 前一天刚卖出  不可买入
        dp[0][0] = -prices[0];
        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];
        }
        return max(max(dp[n - 1][0], dp[n - 1][1]), dp[n - 1][2]);
    }
};

6 买卖股票的最佳时机含手续费

6.1 题目链接

714. 买卖股票的最佳时机含手续费

6.2 题目描述

给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意: 这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

 

示例 1:

输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:  
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8

示例 2:

输入: prices = [1,3,7,5,10,3], fee = 3
输出: 6

 

提示:

  • 1 <= prices.length <= 5 * 104
  • 1 <= prices[i] < 5 * 104
  • 0 <= fee < 5 * 104

6.3 解法(动态规划)

算法思路

  1. 状态表⽰:
  • 对于线性 dp ,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:

    • i. 以某个位置为结尾,巴拉巴拉;
    • ii. 以某个位置为起点,巴拉巴拉。
  • 这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:

  • 由于有「买⼊」「可交易」两个状态,因此我们可以选择⽤两个数组,其中:

    • f[i] 表⽰:第 i 天结束后,处于「买⼊」状态,此时的最⼤利润;
    • g[i] 表⽰:第 i 天结束后,处于「卖出」状态,此时的最⼤利润。
  1. 状态转移⽅程:

我们选择在「卖出」的时候,⽀付这个⼿续费,那么在「买⼊」的时候,就不⽤再考虑⼿续费的问题。

  • 对于 f[i] ,我们有两种情况能到达这个状态:

    • i. 在 i - 1 天「持有」股票,第 i 天啥也不⼲。此时最⼤收益为 f[i - 1] ;
    • ii. 在 i - 1 天的时候「没有」股票,在第 i 天买⼊股票。此时最⼤收益为 g[i - 1] - prices[i]) ;
    • 两种情况下应该取最⼤值,因此 f[i] = max(f[i - 1], g[i - 1] - prices[i]) 。
  • 对于 g[i] ,我们也有两种情况能够到达这个状态:

    • i. 在 i - 1 天「持有」股票,但是在第 i 天将股票卖出。此时最⼤收益为: f[i - 1] + prices[i] - fee) ,记得⼿续费;
    • ii. 在 i - 1 天「没有」股票,然后第 i 天啥也不⼲。此时最⼤收益为: g[i - 1] ;
    • 两种情况下应该取最⼤值,因此 g[i] = max(g[i - 1], f[i - 1] + prices[i] - fee) 。
  1. 初始化:

    由于需要⽤到前⾯的状态,因此需要初始化第⼀个位置。

    • 对于 f[0] ,此时处于「买⼊」状态,因此 f[0] = -prices[0] ;
    • 对于 g[0] ,此时处于「没有股票」状态,啥也不⼲即可获得最⼤收益,因此 g[0] = 0 。
  2. 填表顺序:

    毫⽆疑问是「从左往右」,但是两个表需要⼀起填。

  3. 返回值:

    应该返回「卖出」状态下,最后⼀天的最⼤值收益: g[n - 1] 。

6.4 C++算法代码:

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        vector<int> f(n);
        auto g = f;
        f[0] = -prices[0];
        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);
        }
        return g[n - 1];
    }        
};

7 买卖股票的最佳时机 III

7.1 题目链接

123. 买卖股票的最佳时机 III

7.2 题目描述

给定一个数组,它的第 **i 个元素是一支给定的股票在第 i **天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

 

示例 1:

输入: prices = [3,3,5,0,0,3,1,4]
输出: 6
解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
     随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3

示例 2:

输入: prices = [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。   
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。   
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入: prices = [7,6,4,3,1] 
输出: 0 
解释: 在这个情况下, 没有交易完成, 所以最大利润为 0。

示例 4:

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

 

提示:

  • 1 <= prices.length <= 105
  • 0 <= prices[i] <= 105

7.3 解法(动态规划)

算法思路

  1. 状态表⽰:

    对于线性 dp ,我们可以⽤「经验 + 题⽬要求」来定义状态表⽰:

    • i. 以某个位置为结尾,巴拉巴拉;
    • ii. 以某个位置为起点,巴拉巴拉。

    这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:

  • 由于有「买⼊」「可交易」两个状态,因此我们可以选择⽤两个数组。但是这道题⾥⾯还有交易次数的限制,因此我们还需要再加上⼀维,⽤来表⽰交易次数。其中:
    • f[i][j] 表⽰:第 i 天结束后,完成了 j 次交易,处于「买⼊」状态,此时的最⼤利润;
    • g[i][j] 表⽰:第 i 天结束后,完成了 j 次交易,处于「卖出」状态,此时的最⼤利润。
  1. 状态转移⽅程:
  • 对于 f[i][j] ,我们有两种情况到这个状态:

    • i. 在 i - 1 天的时候,交易了 j 次,处于「买⼊」状态,第 i 天啥也不⼲即可。此时最⼤利润为: f[i - 1][j] ;
    • ii. 在 i - 1 天的时候,交易了 j 次,处于「卖出」状态,第 i 天的时候把股票买了。此时的最⼤利润为: g[i - 1][j] - prices[i] 。
  • 综上,我们要的是「最⼤利润」,因此是两者的最⼤值: f[i][j] = max(f[i - 1][j],g[i - 1][j] - prices[i]) 。

  • 对于 g[i][j] ,我们也有两种情况可以到达这个状态:

    • i. 在 i - 1 天的时候,交易了 j 次,处于「卖出」状态,第 i 天啥也不⼲即可。此时的最⼤利润为: g[i - 1][j] ;
    • ii. 在 i - 1 天的时候,交易了 j - 1 次,处于「买⼊」状态,第 i 天把股票卖了,然后就完成了 j ⽐交易。此时的最⼤利润为: f[i - 1][j - 1] + 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]);
  1. 初始化:
  • 由于需要⽤到 i = 0 时的状态,因此我们初始化第⼀⾏即可。
    • 当处于第 0 天的时候,只能处于「买⼊过⼀次」的状态,此时的收益为 -prices[0] ,因此 f[0][0] = - prices[0] 。
    • 为了取 max 的时候,⼀些不存在的状态「起不到⼲扰」的作⽤,我们统统将它们初始化为 - INF (⽤ INT_MIN 在计算过程中会有「溢出」的⻛险,这⾥ INF 折半取0x3f3f3f3f ,⾜够⼩即可)
  1. 填表顺序:

    从「上往下填」每⼀⾏,每⼀⾏「从左往右」,两个表「⼀起填」。

  2. 返回值:

    返回处于「卖出状态」的最⼤值,但是我们也「不知道是交易了⼏次」,因此返回 g 表最后⼀⾏的最⼤值。

7.4 C++算法代码:

class Solution {
    int INF = 0x3f3f3f3f;
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> f(n,vector<int>(3, -INF));
        auto g = f;
        f[0][0] = - prices[0], g[0][0] = 0;
        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  >= 0)  // 如果该状态存在
                    g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);
            }
        }

        // 找到最后一行的最大值
        int ret = 0;
        for(int i = 0; i < 3; i++)
            ret = max(ret, g[n - 1][i]);
        return ret;
    }
};

8 买卖股票的最佳时机 IV

8.1 题目链接

188. 买卖股票的最佳时机 IV

8.2 题目描述

给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i **天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

 

示例 1:

输入: k = 2, prices = [2,4,1]
输出: 2
解释: 在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2

示例 2:

输入: k = 2, prices = [3,2,6,5,0,3]
输出: 7
解释: 在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
     随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3

 

提示:

  • 1 <= k <= 100
  • 1 <= prices.length <= 1000
  • 0 <= prices[i] <= 1000

8.3 解法(动态规划)

算法思路

  1. 状态表⽰:

    • 为了更加清晰的区分「买⼊」和「卖出」,我们换成「有股票」和「⽆股票」两个状态。
    • f[i][j] 表⽰:第 i 天结束后,完成了 j 笔交易,此时处于「有股票」状态的最⼤收益;
    • g[i][j] 表⽰:第 i 天结束后,完成了 j 笔交易,此时处于「⽆股票」状态的最⼤收益。
  2. 状态转移⽅程:

  • 对于 f [i][j] ,我们也有两种情况能在第 i 天结束之后,完成 j 笔交易,此时⼿⾥「有股票」的状态:

    • i. 在 i - 1 天的时候,⼿⾥「有股票」,并且交易了 j 次。在第 i 天的时候,啥也不⼲。此时的收益为 f[i - 1][j] ;
    • ii. 在 i - 1 天的时候,⼿⾥「没有股票」,并且交易了 j 次。在第 i 天的时候,买了股票。那么 i 天结束之后,我们就有股票了。此时的收益为 g[i - 1][j] - prices[i] ;
  • 上述两种情况,我们需要的是「最⼤值」,因此 f 的状态转移⽅程为:

    • f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i])
  • 对于 g [i][j] ,我们有下⾯两种情况能在第 i 天结束之后,完成 j 笔交易,此时⼿⾥「没有股票」的状态:

    • i. 在 i - 1 天的时候,⼿⾥「没有股票」,并且交易了 j 次。在第 i 天的时候,啥也不⼲。此时的收益为 g[i - 1][j] ;
    • ii. 在 i - 1 天的时候,⼿⾥「有股票」,并且交易了 j - 1 次。在第 i 天的时候,把股票卖了。那么 i 天结束之后,我们就交易了 j 次。此时的收益为 f[i - 1][j - 1] + prices[i] ;
  • 上述两种情况,我们需要的是「最⼤值」,因此 g 的状态转移⽅程为:

    • g[i][j] = max(g[i - 1][j], f[i - 1][j - 1] + prices[i])

如果画⼀个图的话,它们之间交易关系如下:

image.png

  1. 初始化:
  • 由于需要⽤到 i = 0 时的状态,因此我们初始化第⼀⾏即可。
    • 当处于第 0 天的时候,只能处于「买⼊过⼀次」的状态,此时的收益为 -prices[0] ,因此 f[0][0] = - prices[0] 。
    • 为了取 max 的时候,⼀些不存在的状态「起不到⼲扰」的作⽤,我们统统将它们初始化为 - INF (⽤ INT_MIN 在计算过程中会有「溢出」的⻛险,这⾥ INF 折半取0x3f3f3f3f ,⾜够⼩即可)
  1. 填表顺序:

    从上往下填每⼀⾏,每⼀⾏从左往右,两个表⼀起填。

  2. 返回值:

    返回处于卖出状态的最⼤值,但是我们也不知道是交易了⼏次,因此返回 g 表最后⼀⾏的最⼤值。

优化点

  • 我们的交易次数是不会超过整个天数的⼀半的,因此我们可以先把 k 处理⼀下,优化⼀下问题的规模:
    • k = min(k, n / 2)

8.4 C++算法代码:

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        const int INF = 0x3f3f3f3f;
        // 处理一个细节问题
        int n = prices.size();
        k = min(k, n / 2);

        vector<vector<int>> f(n, vector<int>(k + 1, - INF));
        auto g = f;
        f[0][0] = -prices[0], g[0][0] = 0;
        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  >= 0)  // 如果该状态存在
                    g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);
            }
        }

        // 找到最后一行的最大值
        int ret = 0;
        for(int i = 0; i <= k; i++)
            ret = max(ret, g[n - 1][i]);
        return ret;
    }
};