[leetcode] 股票问题
参考文章:
其实文章 [1] 是文章 [2] 的「二次创作」,建议先阅读 [2] 后再阅读 [1] 。文章 [2] 最大的亮点是使用了状态机图对股票问题进行建模和描述,我觉得是写得很好的文章(因为动态规划最原始的数学模型就是状态机)。
本文通过的题目有:
- 题目[121]:买卖股票的最佳时机
- 题目[122]:买卖股票的最佳时机 II
- 题目[123]:买卖股票的最佳时机 III
- 题目[188]:买卖股票的最佳时机 IV
- 题目[309]:最佳买卖股票时机含冷冻期
- 题目[714]:买卖股票的最佳时机含手续费
预备知识
股票买卖问题的本质是状态穷举。或者说,其实大部分动态规划问题都是状态穷举,只不过是某个状态的计算不是从初始条件开始计算,而是依赖于已经计算过的若干个状态。
股票问题面临的因素有三个:天数 \(N\) 、最大交易次数 \(K\) 、在某天股票的持有状态 \(S(S\in\{0,1\})\) 。
- 状态定义
dp[i][k][s] 表示在第 i 天,最大交易次数为 k ,当前股票持状态为 s 的情况下的最大利润。其中,\(0 \le i \le n-1, 1 \le k \le K, 0 \le s \le 1\) .
显然,股票问题所需的结果是 dp[n-1][K][0] 。为什么不是 dp[n-1][K][1] 呢?因为该状态表示持有股票,最后需要的结果当然是不持有股票的,卖出才具有最大利润。
- 转移方程
假设在第 i 天,最大交易次数为 k ,进行操作后没有持有股票,该状态依赖于:
- 第
i-1天持有股票,但是第i天卖出,即dp[i-1][k][1] + price[i]。 - 第
i-1天就不持有股票,即dp[i-1][k][0]。
假设在第 i 天,最大交易次数为 k ,进行操作后持有股票,该状态依赖于:
- 第
i-1天就持有股票,第i天什么都不做,即dp[i-1][k][1]。 - 第
i-1天不持有股票,第i天购入股票,即dp[i-1][k-1][0] - price[i]。因为第i天需要进行一次交易操作,所以要求前一天的交易次数减一。
所以有:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + price[i]) if i>=1 and k>=1
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - price[i]) if i>=1 and k>=1
dp[0][k][0] = 0 if i==0 and k>=1
dp[0][k][1] = -price[0] if i==0 and k>=1
第三个下标只有 0 和 1 ,所以我个人更偏向于将这个三维数组拆分为 2 个二维数组:
dp0[i][k] = max(dp0[i-1][k], dp1[i-1][k] + price[i]) if i>=1 and k>=1
dp1[i][k] = max(dp1[i-1][k], dp0[i-1][k-1] - price[i]) if i>=1 and k>=1
本文就采用 2 个二维数组的形式去解题。
- 边界条件
边界的发生主要发生在变量 i 和 k 上,具体条件是 i == -1 和 k == 0 。
dp[-1][k][0] = 0, dp[-1][k][1] = -INF
dp[i][0][0] = 0, dp[i][0][1] = -INF
dp[-1][k][0] 表示允许交易(即 \(k \ge 1\)),但时间未开始(一个形象比喻:股票交易市场未开市),手上未持有股票,利润固然为 0 .
dp[i][0][0] 表示不允许交易,股票市场开市,所以利润为 0 .
dp[-1][k][1] 表示允许交易,股票市场未开市,但手中已持有股票,该状态是不可能的。
dp[i][0][1] 表示不允许交易,股票市场开市,但手中已持有股票,该状态也是不可能的。
因为求解过程中需要取 max ,所以不可能状态以最小值 -INF 表示。
买卖股票的最佳时机
题目[121]:链接 。
这里 \(K = 1\) ,代入状态转移方程可得:
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + price[i]) if i>=1
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - price[i]) if i>=1
由于 dp[i-1][0][0] 表示不允许交易,且未持有股票,所以为 0 . 因此:
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + price[i]) if i>=1
dp[i][1][1] = max(dp[i-1][1][1], -price[i]) if i>=1
(请注意此处的处理与下面 “买卖股票的最佳时机 Ⅱ” 的区别!)
可以发现,该方程与 K 无关,因此可以进一步简化:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + price[i]) if i>=1
dp[i][1] = max(dp[i-1][1], -price[i]) if i>=1
dp[i] 只依赖于上一个状态,因此可进行空间优化:
dp0 = max(dp0, dp1 + price[i]) if i>=1
dp1 = max(dp1, -price[i]) if i>=1
初始状态,第 0 天,dp0 = 0 表示在第 0 天未持有股票;dp1 = -price[0] 表示在第 0 天购入股票。
代码如下:
int maxProfit(vector<int> &prices)
{
if (prices.size() == 0) return 0;
int dp0 = 0, dp1 = -prices[0];
for (int x : prices)
{
dp0 = max(dp0, dp1 + x);
dp1 = max(dp1, -x);
}
return dp0;
}
在这篇文章中,还有一个适合新手理解的方法,现在发现二者是一致的,dp1 实际上就是 minval 。
int maxProfit(vector<int> &prices)
{
int minval = 0x3f3f3f3f;
int maxval = 0;
for (auto x : prices)
{
minval = min(x, minval);
maxval = max(x - minval, maxval);
}
return maxval;
}
买卖股票的最佳时机 II
题目[122]:买卖股票的最佳时机 II 。
这里允许无限次交易,即 \(K = + \infty\) .
转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + price[i]) if i>=1 and k>=1
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - price[i]) if i>=1 and k>=1
由于 k 是无穷大,因此 k-1 也是无穷大。所以,方程与 k 无关。
dp0[i] = max(dp0[i-1], dp1[i-1] + price[i]) if i>=1
dp1[i] = max(dp1[i-1], dp0[i-1] - price[i]) if i>=1
空间优化:
dp0 = max(dp0, dp1 + price[i]) if i>=1
dp1 = max(dp1, dp0 - price[i]) if i>=1
初始状态:dp0 = 0, dp1 = -price[0] .
代码:
int maxProfit(vector<int> &prices)
{
if (prices.size() == 0) return 0;
int dp0 = 0, dp1 = -prices[0], t;
for (int x : prices)
t = dp0, dp0 = max(dp0, dp1 + x), dp1 = max(dp1, t - x);
return dp0;
}
买卖股票的最佳时机 III
题目[123]:买卖股票的最佳时机 III 。
这里 \(K=2\) ,转移方程:
dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + price[i]) if i>=1
dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - price[i]) if i>=1
对第三个下标降维,分解为 2 个 DP 数组:
dp0[i][2] = max(dp0[i-1][2], dp1[i-1][2] + price[i]) if i>=1
dp1[i][2] = max(dp1[i-1][2], dp0[i-1][1] - price[i]) if i>=1
我的解法
到这一步,要考虑的是怎么求出 dp0[i-1][1] ?它的含义是只允许一次交易,在第 i 天不持有股票的最大利润。显然这就是第一题 “买卖股票的最佳时机” 所求的。
所以,我们先求出 dp0[n][1] 这个数组,用 vector 记录下来。那么状态方程就变为:
dp0[i][2] = max(dp0[i-1][2], dp1[i-1][2] + price[i]) if i>=1
dp1[i][2] = max(dp1[i-1][2], v[i-1] - price[i]) if i>=1
可以发现,这时候与 k=2 无关(即与第二维下标无关):
dp0[i] = max(dp0[i-1], dp1[i-1] + price[i]) if i>=1
dp1[i] = max(dp1[i-1], v[i-1] - price[i]) if i>=1
空间优化:
dp0 = max(dp0, dp1 + price[i]) if i>=1
dp1 = max(dp1, v[i-1] - price[i]) if i>=1
代码:
int maxProfit3(vector<int> &prices)
{
if (prices.size() == 0) return 0;
vector<int> v(prices.size(), 0); // which is dp0 at above
int t = -prices[0]; // which is dp1 at above
int n = prices.size();
for (int i = 1; i < n; i++)
{
v[i] = max(v[i - 1], t + prices[i]);
t = max(t, -prices[i]);
}
int dp0 = 0, dp1 = -prices[0];
for (int i = 1; i < n; i++)
{
dp0 = max(dp0, dp1 + prices[i]);
dp1 = max(dp1, v[i - 1] - prices[i]);
}
return dp0;
}
原作者的解法
\(K=2\) 时的转移方程:
dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + price[i]) if i>=1
dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - price[i]) if i>=1
进一步对 dp[.][1][.] 进一步展开(实际上就是第一题 “买卖股票的最佳时机” 的转移方程):
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + price[i]) if i>=1
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - price[i]) if i>=1
综合一下:
dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + price[i]) if i>=1
dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - price[i]) if i>=1
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + price[i]) if i>=1
dp[i][1][1] = max(dp[i-1][1][1], -price[i]) if i>=1
对第二、第三维的下标进行降维:
dp20[i] = max(dp20[i-1], dp21[i-1] + price[i]) if i>=1
dp21[i] = max(dp21[i-1], dp10[i-1] - price[i]) if i>=1
dp10[i] = max(dp10[i-1], dp11[i-1] + price[i]) if i>=1
dp11[i] = max(dp11[i-1], -price[i]) if i>=1
空间优化:
dp20 = max(dp20, dp21 + price[i]) if i>=1
dp21 = max(dp21, dp10 - price[i]) if i>=1
dp10 = max(dp10, dp11 + price[i]) if i>=1
dp11 = max(dp11, dp00 - price[i]) if i>=1
初始状态:dp20=0, dp10=0, dp21=-price[0], dp11=-price[0] .
代码(Ps:把变量名改为 a,b,c,d 马上 bigger 就高了
[leetcode] 股票问题的更多相关文章
- leetcode股票问题方法收集 转载自微信公众号labuladong
一.穷举框架首先,还是一样的思路:如何穷举?这里的穷举思路和上篇文章递归的思想不太一样. 递归其实是符合我们思考的逻辑的,一步步推进,遇到无法解决的就丢给递归,一不小心就做出来了,可读性还很好.缺点就 ...
- leetcode 股票系列
五道股票题总结: 121 买卖股票的最佳时机 122 买卖股票的最佳时机 124 买卖股票的最佳时机4 309 最佳股票买卖含冷冻期 714 买卖股票的最佳时机含有手续费 121 买卖股票的最佳时机 ...
- [leetcode]股票题型123
122. Best Time to Buy and Sell Stock II Say you have an array for which the ith element is the price ...
- leetcode题解-122买卖股票的最佳时期
题目 leetcode题解-122.买卖股票的最佳时机:https://www.yanbinghu.com/2019/03/14/30893.html 题目详情 给定一个数组,它的第 i 个元素是一支 ...
- 【Leetcode】【简单】【122. 买卖股票的最佳时机 II】【JavaScript】
题目描述 122. 买卖股票的最佳时机 II 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格. 设计一个算法来计算你所能获取的最大利润.你可以尽可能地完成更多的交易(多次买卖一支股票) ...
- [LeetCode] 901. Online Stock Span 线上股票跨度
Write a class StockSpanner which collects daily price quotes for some stock, and returns the span of ...
- [LeetCode] 121. Best Time to Buy and Sell Stock 买卖股票的最佳时间
Say you have an array for which the ith element is the price of a given stock on day i. If you were ...
- [LeetCode] 122. Best Time to Buy and Sell Stock II 买卖股票的最佳时间 II
Say you have an array for which the ith element is the price of a given stock on day i. Design an al ...
- [LeetCode] 123. Best Time to Buy and Sell Stock III 买卖股票的最佳时间 III
Say you have an array for which the ith element is the price of a given stock on day i. Design an al ...
随机推荐
- 非系统盘根目录出现msdia80.dll文件,能否删除?
出现此问题的原因:计算机上安装了 Microsoft Visual C++ 2005 可再发行组件时,Msdia80.dll文件被错误安装在其他驱动器的根文件夹中. 它的正确路径应该是"C: ...
- 201771010128王玉兰《面向对象程序设计(Java)》第十六周学习总结
第一部分:理论基础 1.线程的概念 进程:进程是程序的一次动态执行,它对应了从代码加 载.执行至执行完毕的一个完整过程. 多线程:多线程是进程执行过程中产生的多条执行线索. 线程:线程是比进程执行 ...
- Windows系统下curl的下载和配置
curl的下载和配置 简介:用URL规则在命令行下工作的文件传输工具. 下载:下载地址为 https://curl.haxx.se/download.html,在最底部找到Windows的版本,我下载 ...
- MyBatis——com.mysql.cj.exceptions.InvalidConnectionAttributeException
报错信息 Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.c ...
- Istio的流量管理(实操二)(istio 系列四)
Istio的流量管理(实操二)(istio 系列四) 涵盖官方文档Traffic Management章节中的inrgess部分. 目录 Istio的流量管理(实操二)(istio 系列四) Ingr ...
- Python面向对象的特征跟私有属性
面向对象的三大特征 继成.封装.多态: 继承:一个类可以继承一个或多个类,被继承的类叫做父类,继承的类叫做子类 封装:将数据和方法放在了一个类中就构成了封装 多态:指的一类事物有多重形态,一个抽象类 ...
- 0507 构造代码块和static案例,接口interface
0507构造代码块和static案例,接口interface [重点] 1.局部变量,成员变量,静态变量的特点 2.接口 接口语法:interface A {} 接口内的成员变量[缺省属性]publi ...
- JavaSE案例-Bank
项目介绍 完成基本的银行业务功能 存款 取款 转账 查询余额 修改密码 修改预留手机号 注销账号 退出 任务分解: 定义三个基本类: BankTest():银行业务主程序 Bank(): 银行类,包含 ...
- linux高级应用第九章-正则表达式
笔记部分 基础正则表达式: ^ 第1个符号 ,以什么什么开头 ^m $ 第2个符号,以什么什么结尾 m$ ,还表示空行,或空格,可以用cat -An 试一下 ^$ 第3个符号,空行 ...
- PowerDesigner使用教程(二)
在PowerDesigner使用教程(一)中已经介绍了基本用法,下面介绍一些使用技巧. 1. 生成sql脚本 Database→Generate Database 选择要输出的文件路径,即文件存储路 ...