- 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问题。
- 得到模板如下:
状态转移方程需要base case,也就是最开始的时候,需要一些初始值来让转移方程开始动作。(比如这里要得出的第一天的情况,你就需要构造出第0天的情况(虽然不存在,但是你得合理构造才能利用dp0推导出dp1))// 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,所以今天我就持有股票了。
// 最简单情况(-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