动态规划系列 之 股票相关问题 (C语言刷leetcode)

  • 2020-10-15

    “知识的诅咒”:一旦我们知道某样东西,我们就会发现很难想象不知道它的时候会是什么样子。

一、概述

leetcode中的股票相关的题目如下:
在这里插入图片描述

打开看很容易就知道是求最大收益,由于是求最值,很容易就想到要使用动态规划。
其实动态规划就是使用了穷举,但是因为这类问题存在「重叠⼦问题」,可以使用DP table来优
化穷举过程,记录过计算的结果,避免不必要的计算。

动态规划三要素

  • 重叠⼦问题(如果暴力解决,存在大量运算,可以使用备忘录(DP table)来解决)
  • 最优⼦结构
    要符合「最优⼦结构」(子结构之间追求最优是独立的),⼦问题间必须互相独⽴。
  • 状态转移⽅程
    你把 f(n) 想做⼀个状态 n,这个状态 n 是由状态 n - 1 和状态 n - 2 相加转移⽽来,这就叫状态转移,仅此⽽已。

其实最难的是如何写出动态转移方程,这个转移方程要求能够穷举(中途可能经过判断忽略一些不可能的计算)且不重复。

二、股票相关问题模板

该模板来自大佬labuladong,其网站文章讲的更详细:https://labuladong.gitbook.io/algo/di-ling-zhang-bi-du-xi-lie/tuan-mie-gu-piao-wen-ti
本人只是用自己的理解复述一下。

状态转移方程的实现:

  • 先利⽤「状态」进⾏穷举,找到能表示股票交易过程中状态的方法。
  • 状态转移方程(利用买卖、持有的关系,建立状态机)。
  • 定义 base case(这些值可以代入初始时状态转移方程验证一下),即最简单的情况。

我们的结果是求最大收益,所以状态方程的结果肯定是收益,由这些题目可以得出有两个条件约束最后的收益。
这两个条件是时间(冻结时间等)、交易次数。

再由于状态转移方程中,第n次的最值,需要由前面的第n-1次(甚至前面的n-2次等)转移得到。前面n-1次的持有或者不持有的状态也会影响(一般在思考状态转移方程如何转换才会想到),所以现在有三个状态:时间、交易次数限制、是否持有。
所以可以说是三维DP问题。

  • 得到模板如下:
    // i表示日期(第几天)、k表示还能交易的次数、第三位表示该天是持有还是不持有股票
    //	dp[i][k][0/1] 表示到第i天持有的最大利润(利用一个三维数组来实现)
    
    //状态转移⽅程:
    dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
    //解释:今天我没有持有股票,有两种可能:
    //要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
    //要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。
    dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
    //解释:今天我持有着股票,有两种可能:
    //要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
    //要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。
    
    状态转移方程需要base case,也就是最开始的时候,需要一些初始值来让转移方程开始动作。(比如这里要得出的第一天的情况,你就需要构造出第0天的情况(虽然不存在,但是你得合理构造才能利用dp0推导出dp1))
    // 最简单情况(-1是由于状态转移方程的需要所以构造)
    base case:
    dp[-1][k][0] = dp[i][0][0] = 0
    dp[-1][k][1] = dp[i][0][1] = -infinity(负无穷)
        
    //对base case的解释
    dp[-1][k][0] = 0
    解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0。
    dp[-1][k][1] = -infinity
    解释:还没开始的时候,是不可能持有股票的,⽤负⽆穷表⽰这种不可能。
    dp[i][0][0] = 0
    解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0。
    dp[i][0][1] = -infinity
    解释:不允许交易的情况下,是不可能持有股票的,⽤负⽆穷表⽰这种不可能。
    
三、具体问题,具体分析
  • 121、买卖股票的最佳时机

    给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
    如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
    注意:你不能在买入股票前卖出股票。

    这道题限制了只能交易一次,相当于上文中的k不用考虑,所以状态变成了两个,简单很多。

    int maxProfit(int* prices, int pricesSize){
        if(pricesSize == 0)
            return 0;
        // base case
        int dp_i_0 = 0;
        int dp_i_1 = -prices[0];
    
    	//状态转移方程从第1天开始转移,直到最后一天得到结果
        for(int i=0; i<pricesSize; ++i)
        {
            dp_i_0 = (dp_i_0 > (dp_i_1+prices[i]))? dp_i_0 : (dp_i_1+prices[i]);
            dp_i_1 = (dp_i_1 > (-prices[i])) ? dp_i_1 : (-prices[i]); //因为只能交易一次,dp_i_1只会记录买入的花费
        }
        
        return dp_i_0;
    }
    
  • 122.、买卖股票的最佳时机 II

    给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
    设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
    注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

    int maxProfit(int* prices, int pricesSize){
        if(pricesSize == 0)
            return 0;
        // base case:初始化的值为第一天的收益
        int dp_i_0 = 0;
        int dp_1_1 = -prices[0];
    
        for(int i=0; i<pricesSize; ++i)
        {
            dp_i_0 = (dp_i_0 > (dp_1_1+prices[i]))? dp_i_0 : (dp_1_1+prices[i]);
            dp_1_1 = (dp_1_1 > (dp_i_0-prices[i])) ? dp_1_1 : (dp_i_0-prices[i]);
        }
    
        return dp_i_0;
    }
    
  • 309.、最佳买卖股票时机含冷冻期

    给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​
    设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
    你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
    卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

    由于存在冷冻期,所以得多记录前两天未持有股票的情况,状态转移方程也要适当变换。

    int maxProfit(int* prices, int pricesSize){
    
        int dp_i_0 = 0;
        int dp_i_1 = -0x7FFFFFFF;
        int dp_i_2_0 = 0;
    
        for(int i=0; i<pricesSize; ++i)
        {
            int temp = dp_i_0;
            dp_i_0 = (dp_i_0 > (dp_i_1+prices[i]))?  dp_i_0 : (dp_i_1+prices[i]);
            dp_i_1 = (dp_i_1 > (dp_i_2_0-prices[i]))? dp_i_1 : (dp_i_2_0-prices[i]);
            dp_i_2_0 = temp;
        }
    
        return dp_i_0;
    }
    
  • 714.、买卖股票的最佳时机含手续费

    给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。
    你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
    返回获得利润的最大值。
    注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

    这里只是需要把花费加上而已,和122题相差不大。

    int maxProfit(int* prices, int pricesSize, int fee){
        int dp_i_0 = 0;
        int dp_i_1 = -0x7FFFFFFF; //用一个最小的数表示不可能的结果,让状态转移方程避开这种情况
    
        for(int i=0; i<pricesSize; ++i)
        {
            dp_i_0 = (dp_i_0 > (dp_i_1+prices[i]))? dp_i_0 : (dp_i_1+prices[i]);
            dp_i_1 = (dp_i_1 > (dp_i_0-prices[i]-fee)) ? dp_i_1 : (dp_i_0-prices[i]-fee);
        }
    
        return dp_i_0;
    }
    
  • 123、 买卖股票的最佳时机 III

    给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
    设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
    注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

    既然最多只能交易两次,那就把两次的分开就行了。(这里只用四个变量保存其中的结果,因为一些值已经不会对结果产生影响)。

    int maxProfit(int* prices, int pricesSize){
        //base case
        int dp_i_k_0 = 0; //dp[i][k][0]
        int dp_i_k_1 = -0x7FFFFFFF; //dp[i][k][1]用一个最小的数表示不可能的结果,让状态转移方程避开这种情况
        int dp_i_k1_0 = 0;//dp[i][k-1][0]
        int dp_i_k1_1 = dp_i_k_1;//dp[i][k-1][1]
    
        for(int i=0; i<pricesSize; ++i)
        {
            // k=1
            dp_i_k1_0 = (dp_i_k1_0 > (dp_i_k1_1+prices[i]))? dp_i_k1_0 : (dp_i_k1_1+prices[i]);
            dp_i_k1_1 = (dp_i_k1_1 > (-prices[i])) ? dp_i_k1_1 : (-prices[i]); 
    
            //k=2
            dp_i_k_0 = (dp_i_k_0 > (dp_i_k_1+prices[i]))? dp_i_k_0 : (dp_i_k_1+prices[i]);
            dp_i_k_1 = (dp_i_k_1 > (dp_i_k1_0-prices[i])) ? dp_i_k_1 : (dp_i_k1_0-prices[i]);
        }
    
        return dp_i_k_0; //最多交易k次的最大收益
    }
    
  • 188、买卖股票的最佳时机 IV

    给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
    设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
    注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)

    这道题是真难呀,看一下大神的解释:四种解法+图解 188.买卖股票的最佳时机 IV

四、参考资料
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值