0813算法 - 动态规划09

188. 买卖股票的最佳时机 IV | 309. 买卖股票的最佳时机含冷冻期 | 714. 买卖股票的最佳时机含手续费

Posted by ZA on August 13, 2024

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

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

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

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

示例 1:

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

思路

这就是昨天买卖股票III的升级版,从限定最多2次到最多k次,难点就在于,k作为任意一个数值,如何像k=2一样划分出确定的5种状态?

这就需要分析状态编号的排布,找出状态编号的内部规律:

  • 0 表示不操作
  • 1 第一次买入
  • 2 第一次卖出
  • 3 第二次买入
  • 4 第二次卖出
  • ……

    可以看出奇数为买入,偶数为卖出,且j的范围就是2*k+1

  1. dp数组及其下标含义:第i天的状态为j,其最大资金为dp[i][j]
  2. 递推公式:同理,无论是买入卖出,每一天的状态都可以分为 “顺沿自前一天” 和 “当天操作” 两类,i(当天),i-1(昨天),j(当前状态),j-1(上一状态)
    1. 昨日顺沿:时间变化,状态不变,dp[i-1][j]
    2. 当天操作:时间变化,状态变化,dp[i-1][j-1]
  3. 数组初始化:由于规定只能出售掉才能购买,在第0天的任意一次买入前都有n=[0,k-1]次完整的买卖操作,因此dp[0][j] = -prices[0]j为任意奇数)dp[0][j] = 0j为任意偶数)
  4. 遍历顺序:同理,dp[i]依靠dp[i-1]的数值,从前向后遍历
  5. 举例推导:以输入[1,2,3,4,5],k=2为例。

代码实现

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        vector<vector<int>>dp(prices.size(), vector<int>(2*k+1, 0));
        for(int j=1; j<2*k; j+=2){
            dp[0][j] = -prices[0];
        }

        for(int i=1; i<prices.size(); i++){
            for(int j=0; j<2*k-1; j+=2){
                dp[i][j+1] = max(dp[i-1][j+1], dp[i-1][j] - prices[i]);
                dp[i][j+2] = max(dp[i-1][j+2], dp[i-1][j+1] + prices[i]);
            }
        }

        return dp[prices.size()-1][2*k];
    }
};

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

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

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

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

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

示例 1:

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

思路

同样从股票买卖在每一天所有可能状态的角度分析 难点在于加入冷静期,卖出状态后的一段时间,可选的状态变为了两种:“主动保持卖出状态不持有” 和 “冷静期无法操作”,其中冷冻期状态不可持续,仅存在一天! 状态转换图如上,具体可以区分出如下四个状态:

  • 状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)
  • 不持有股票状态,这里就有两种卖出股票状态
    • 状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
    • 状态三:今天卖出股票
  • 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
  1. dp数组及其下标含义:同理,第i天的状态为j,其最大资金为dp[i][j]
  2. 递推公式:根据状态转换图分析:
    1. 状态一:持有状态,分为 “顺沿前一天持有” 和 “今天买入”,由于冷静期,“今天买入”的前一天状态又可分为“前一天为冷冻期” 和 “前一天主动未持有”
    2. 状态二:未持有状态,分为 “顺沿前一天未持有” 和 “前一天为冷静期”
    3. 状态三:卖出状态,只有一种可能,前一天为状态一持有状态
    4. 状态四:冷冻期,只有一种可能,前一天为状态三卖出状态
dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];

除了之前出现过的持有和未持有状态 + 题目要求的冷静期,还新引入了一个卖出状态,它是冷冻期的特定上一状态,因未持有状态出现分支而定义

  1. 数组初始化:同dp[0][0] = -prices[i]; dp[0][1] = 0;
  2. 遍历顺序:同为从前向后遍历
  3. 举例推导:以 [1,2,3,0,2] 为例,dp数组如下:

最后只要是非持有股票的状态,都有可能是最大值,因此最终输出要从最后一天的状态二三四中取最大值。

代码实现

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        vector<vector<int>>dp(prices.size(), vector<int>(4,0));
        dp[0][0] = -prices[0];

        for(int i=1; i<prices.size(); i++){
            dp[i][0] = max(dp[i-1][0], max(dp[i-1][3]-prices[i], dp[i-1][1]-prices[i]));
            dp[i][1] = max(dp[i-1][1], dp[i-1][3]);
            dp[i][2] = dp[i-1][0] + prices[i];
            dp[i][3] = dp[i-1][2];
        }

        return max(dp[prices.size()-1][1], max(dp[prices.size()-1][2], dp[prices.size()-1][3]));
    }
};

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

给定一个整数数组 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

思路

本题支持任意次数买卖,跟 122.买卖股票的最佳时机 II 的唯一别就是加入了手续费,其实只要在递推公式位置比较max时,额外减去一个手续费即可

  1. dp数组及其下标含义:同理,
  2. 递推公式
  3. 数组初始化:取决于第0天的五种状态
    1. 无操作:易得 dp[0][0] = 0
    2. 第一轮持有:开局就买入dp[0][1] = -price[0]
    3. 第一轮未持有,第二轮持有/未持有:只不过是左右倒手,反复买卖: dp[0][2] = dp[0][4] = 0; dp[0][3] = dp[0][1] = -price[0]
  4. 遍历顺序:同理为从前向后
  5. 举例推导:以一个递增数列[1,2,3,4,5]为例:

代码实现

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();

        vector<vector<int>>dp(n, vector<int>(2,0));
        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][0]+prices[i]-fee);
        }

        return dp[n-1][1];
    }
};

后记

股票买卖问题的核心还是在于状态的分解,本组题目限定可选且仅能持有一支股票,因此每个时刻的可选状态都能够划分为一个有限集合(是否持有?顺沿持有还是当天购入持有?),此时只要确定好初始状态,自i-1i逐步推算即可

今天本组题目对买卖股票问题进行了复杂化,主要从三个方向:

  • 买卖次数为任意数——改为构建具有编号规则的j种状态,而非固定5
  • 加入冷静期——引入新的状态转移,影响到了未持有状态中的顺沿和买入,虽固定个数但需要重新设计
  • 加入手续费——最简单,只需要为当天卖出的式子后面减去一个手续费即可

参考