「股票买卖问题」大概是每个刷 LeetCode 的同学都会遇到的一大拦路虎,特别是其中的第三道题。你是否也曾因为这道题而懵逼呢?

股票买卖系列问题

LeetCode 上的股票买卖系列问题一共六道,形成一个巨大的系列,蔚为壮观。系列的前两道题并不难,可以通过思维转换得到解法。然而就在你以为整个系列都可以循序渐进地做出来时,第三道题的难度陡然上升,让人猝不及防。

更令人沮丧的是,这样一道难题,打开讨论区,看到的却是一份异常装逼的题解代码:

public int maxProfit(int[] prices) {if (prices.length == 0) return 0;int s1 = Integer.MIN_VALUE, s2 = 0, s3 = Integer.MIN_VALUE, s4 = 0;for (int p : prices) {s1 = Math.max(s1, -p);s2 = Math.max(s2, s1 + p);s3 = Math.max(s3, s2 - p);s4 = Math.max(s4, s3 + p);}return Math.max(0, s4);
}

WTF?? 这谜一样的四个变量 s1/s2/s3/s4 是怎么回事?这种计算方式怎么能体现题目中「最多买卖两次」的限制?

不要慌张。其实这类问题是有套路的,只要掌握了套路,你也一样能写出这样装逼的解法。这个套路也非常实用,几道股票买卖问题都可以用这个套路解出来。

这样实用而装逼的解法,今天就让我为你细细讲述。本文会介绍股票买卖问题的这个解法中涉及到的几个技巧:

  • 动态规划子问题的状态拆解与状态机定义

  • DP 数组的特殊值定义

  • 动态规划的空间优化技巧

问题解法

我们来求解最典型的股票问题(三),它是其他几道题目解法的基础:

LeetCode 123. Best Time to Buy and Sell Stock III(Hard)

给定一个数组,它的第




个元素是一支给定的股票在第




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

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

示例:

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

很多同学可能已经隐约想到这道题是用动态规划来解,但是一直想不出来合适的具体思路。

这道题最大的难点就在于其限制条件「最多完成两笔交易」。如何在动态规划中描述这个限制条件?如何记录股票买卖过程中的「不同状态」?其实,全部的答案就在我们上一篇文章中讨论的技巧:拆分动态规划的子问题

不记得上一篇文章内容的同学可以点这里回顾:

LeetCode 例题精讲 | 17 动态规划如何拆分子问题,简化思路

当然,如果你只想知道股票买卖问题的解法,可以直接往下看。

状态机定义

对于题目中「最多完成两笔交易」这个限制条件,我们可以理解成:股票买卖的过程,经历了不同的阶段

  • 在一开始,限制是「最多完成两笔交易」;

  • 做完一笔交易之后,限制变成「只做一笔交易」;

  • 做完两笔交易之后,限制变成「不能再做交易」。

有的解法选择定义一个参数




来表示可以进行交易的数量。不过




的取值只有 2、1、0,却要给动态规划增加一个维度,不太划算。我们可以直接定义多个子问题来描述这些不同的阶段。

另外,题目中还有一个条件是「再次购买前必须卖掉之前的股票」,这实际上又给股票买卖过程划分了阶段。我们有「持有股票」和「不持有股票」两种状态。在持有股票的时候,只能卖出,不能买入。不持有股票的时候则反之。

总体来看,做两笔交易,则股票买卖的过程可以分为五个阶段

阶段 可交易次数 持股状态 可买入/卖出
0 2 不持有 可买入
1 1 持有 可卖出
2 1 不持有 可买入
3 0 持有 可卖出
4 0 不持有 可买入

股票买卖过程的五个阶段

对应这五个阶段,我们可以定义五个子问题,分别用







































来表示。字母 s 代表状态,股票买卖阶段的变化,其实就是状态的转移。

例如在阶段 1,我们持有股票,此时如果卖出股票,则变成不持有股票的状态,进入阶段 2。









~







之间的状态转移可以用下面这张图来表示:

子问题间的状态转移关系(状态机)

在每一天,我们既可以选择买入/卖出,又可以选择不进行买卖。选择买入或者卖出的话,就会进入下一个阶段,对应状态转移图中向右的边;选择不买卖的话,会继续待在当前阶段,对应状态转移图中的环路。

这就是所谓的「状态机 DP」。定义多个子问题,从另一个角度来看,就是子问题在不同的「状态」间跳来跳去。

在理解了子问题之间的关系之后,我们正式地定义一下子问题和递推关系:子问题




























分别表示「前




天结束后,处于阶段 0/1/2/3/4 时,当前的最大利润」。那么我们有:














  • 。因为阶段 0 时没有任何买入卖出。

  • 。第 k 天处于阶段 1,可能是前一天处于阶段 1,或者是前一天处于阶段 0,然后买入了第 k 天的股票(利润减去







    )。

  • 。第 k 天处于阶段 2,可能是前一天处于阶段 2,或者是前一天处于阶段 1,然后卖出了第 k 天的股票(利润增加







    )。

  • 。分析同










  • 。分析同










理解 DP 数组

在定义了子问题及其递推关系后,我们还需要搞清楚 DP 数组的计算顺序。

首先,由于







始终为 0,我们其实不需要计算,直接作为常数 0 代入即可。这样就只剩







/







/







/







四个子问题了。

四个子问题中,




的取值范围都是








。这样我们的 DP 数组是四个长度为






的一维数组,如下图所示。

DP 数组

接着是 DP 数组中的依赖关系:

DP 数组的依赖关系

可以看出,DP 数组的计算顺序是从左到右、从上到下。我们可以根据这个写出初步的题解代码:

// 注意:这段代码并不完整
public int maxProfit(int[] prices) {if (prices.length == 0) {return 0;}int n = prices.length;int[] s1 = new int[n+1];int[] s2 = new int[n+1];int[] s3 = new int[n+1];int[] s4 = new int[n+1];// 注意:这里还缺少 base case 的赋值for (int k = 1; k <= n; k++) {s1[k] = Math.max(s1[k-1], -p[k-1]);s2[k] = Math.max(s2[k-1], s1[k-1] + p[k-1]);s3[k] = Math.max(s3[k-1], s2[k-1] - p[k-1]);s4[k] = Math.max(s4[k-1], s3[k-1] + p[k-1]);}return Math.max(0, Math.max(s2[n], s4[n]));
}

处理 base case

上面的代码还不是很完整,我们还需要填上子问题的 base case 的取值。

对于这道题来说,确定 base case 的取值并不容易。难点在于 DP 数组中的部分元素是无效的。









为例,










的含义应该是:在第 0 天(即一开始),经过一次买入、一次卖出后,所获的最大利润。然而我们在第 0 天显然还不能进行任何买卖,那么










就是无效元素。我们可以推出,














时才有效。

同样的道理,我们可以计算出































的 base case 分别是











































,如下图所示(这里放上







以方便理解)。

DP 数组中的无效元素

如果用条件判断来处理这些无效元素,代码会变得非常复杂。有没有什么更好的方法呢?

答案是给











































赋特殊值。虽然这些值没有什么实际意义,但不会影响后面有效值的计算,也不会影响最终结果。

既然最终结果要求的是最大利润(max),我们可以给这些无效元素赋一个比任何可能结果都小的值

  • 对于















    ,这个值是




    。因为这两个状态不持有股票,有效值显然不会低于 0(可以不买也不卖,利润就是 0)。

  • 对于















    ,这个值是





    。因为这两个状态要持有股票,买入后会出现暂时的负利润。

加入这些 base case 之后,我们得到完整的代码:

public int maxProfit(int[] prices) {if (prices.length == 0) {return 0;}int n = prices.length;int[] s1 = new int[n+1];int[] s2 = new int[n+1];int[] s3 = new int[n+1];int[] s4 = new int[n+1];s1[0] = Integer.MIN_VALUE;s2[0] = 0;s3[0] = Integer.MIN_VALUE;s4[0] = 0;for (int k = 1; k <= n; k++) {s1[k] = Math.max(s1[k-1], -prices[k-1]);s2[k] = Math.max(s2[k-1], s1[k-1] + prices[k-1]);s3[k] = Math.max(s3[k-1], s2[k-1] - prices[k-1]);s4[k] = Math.max(s4[k-1], s3[k-1] + prices[k-1]);}return Math.max(0, Math.max(s2[n], s4[n]));
}

空间优化

上面的代码已经比较简洁了,不过它和我们一开始展示的装逼型代码还有一点差距。接下来,我们使用一点空间优化的技巧,让代码更加简洁。

回顾一下上面的 DP 数组依赖图:

DP 数组的依赖关系

我们发现,每一列的值都只依赖于上一列的值。这样,我们只需要保存当前一列的值,然后在每一轮迭代中计算下一列的值。

空间优化方案,迭代计算每一列

这样,s1s2s3s4 就从一维数组变成了单个变量。

int s1 = Integer.MIN_VALUE;
int s2 = 0;
int s3 = Integer.MIN_VALUE;
int s4 = 0;
for (int k = 1; k <= n; k++) {s4 = Math.max(s4, s3 + prices[k-1]);s3 = Math.max(s3, s2 - prices[k-1]);s2 = Math.max(s2, s1 + prices[k-1]);s1 = Math.max(s1, -prices[k-1]);
}

上面的代码中大量出现 prices[k-1]。我们把 k-1 替换成 k

int s1 = Integer.MIN_VALUE;
int s2 = 0;
int s3 = Integer.MIN_VALUE;
int s4 = 0;
for (int k = 0; k < n; k++) {s4 = Math.max(s4, s3 + prices[k]);s3 = Math.max(s3, s2 - prices[k]);s2 = Math.max(s2, s1 + prices[k]);s1 = Math.max(s1, -prices[k]);
}

然后把 for 循环改成 for-each 循环:

int s1 = Integer.MIN_VALUE;
int s2 = 0;
int s3 = Integer.MIN_VALUE;
int s4 = 0;
for (int p : prices) {s4 = Math.max(s4, s3 + p);s3 = Math.max(s3, s2 - p);s2 = Math.max(s2, s1 + p);s1 = Math.max(s1, -p);
}

这样,我们就得到了最终的简化版代码:

public int maxProfit(int[] prices) {if (prices.length == 0) {return 0;}int s1 = Integer.MIN_VALUE;int s2 = 0;int s3 = Integer.MIN_VALUE;int s4 = 0;for (int p : prices) {s4 = Math.max(s4, s3 + p);s3 = Math.max(s3, s2 - p);s2 = Math.max(s2, s1 + p);s1 = Math.max(s1, -p);}return Math.max(0, Math.max(s2, s4));
}

这样一步步看下来,是不是感觉开头那个装逼的代码也没有那么难懂了?

总结

本文一步步剖析了股票买卖问题的解题技巧。如果你直接看最终的代码,会觉得「装逼」而放弃这道题。但跟着本文的思路一步步走,会发现这样的代码其实是经过一步步的简化,逐渐变成这个样子的。

LeetCode 讨论区的很多答案喜欢炫耀代码的简洁,比拼行数。但是追求代码的极简并不利于我们掌握问题思路。对于本题而言,其实最值得掌握的是没有经过空间优化的、定义四个一维数组的代码。特别是在面试中,如果你一上来就写出了经过空间优化后的极简代码,面试官可能觉得你是在「背题」,反而对你印象不好。

本文讲述的股票问题的解法有人称之为「状态机 DP」。解法的关键就在于定义多个子问题,然后描述子问题之间的状态转移关系。读完本文的同学,强烈建议把股票买卖问题跟上一篇文章中的例题联系起来看,会让你对这一类问题有更深的理解。

股票买卖系列的其他问题同样可以用这个解题技巧做出来。后面的文章我会给大家展示如何把「最多完成两笔交易」扩充到「最多完成 k 笔交易」的通用版题目,以及带有交易手续费和冷却期的变种题目的求解方法,敬请期待。

往期文章

  • LeetCode 例题精讲 | 17 动态规划如何拆分子问题,简化思路

  • LeetCode 例题精讲 | 16 最大子数组和:子数组类问题的动态规划技巧

  • LeetCode 例题精讲 | 14 打家劫舍问题:动态规划的解题四步骤

我是 nettee,致力于分享面试算法的解题套路,让你真正掌握解题技巧,做到举一反三。我的《LeetCode 例题精讲》系列文章正在写作中,关注我的公众号,获取最新文章。

原创不易,点个「在看」吧 ↘︎

一文教你股票买卖问题实用而装逼的解法相关推荐

  1. 分享几个实用,装逼的cmd命令。

    1,端口占用查询解决:netstat -ano|findstr "80", 红框内的对应进程里面的pid,如果端口占用,打开任务管理器,找到对应的pid,结束进程就行. 2,查询m ...

  2. IDEA2020的中文插件_IDEA2020个性化设置(装逼且实用)

    作者:暗中观察i 一.个性化主题设置 第一步:获得主题文件,推荐主题下载网址 http://www.soft-hub.cn或http://www.riaway.com/ 第二步:在你喜欢的磁盘下随意新 ...

  3. cmd 实用命令以及如何装逼

    在cmd环境下打开文件和文件夹. 用start命令 例如start 文件夹 或者直接start 路径 例如start g:\tmp          <-- 打开文件夹        start ...

  4. IDEA2020个性化设置(装逼且实用)

    IDEA2020个性化设置 一.个性化主题设置 第一步:获得主题文件,推荐主题下载网址 http://www.soft-hub.cn或http://www.riaway.com/ 第二步:在你喜欢的磁 ...

  5. notepad正则表达式替换_正则表达式装逼(实用)指南

    正则表达式(Regular Expression,或者Regex),能干嘛?听说很强悍,很多人用来查找字符串,或者替换某些字符串. 实际上,正则表达式有四个功能: 匹配,即查找,例如,从杂乱的一堆文本 ...

  6. 装逼一步到位!GauGAN代码解读来了

    ↑↑↑关注后"星标"Datawhale 每日干货 & 每月组队学习,不错过 Datawhale干货 作者:游璐颖,福州大学,Datawhale成员 AI神笔马良 如何装逼一 ...

  7. JavaScript装逼指南

    如何写JavaScript才能逼格更高呢?怎样才能组织JavaScript才能让别人一眼看出你不简单呢?是否很期待别人在看完你的代码之后感叹一句"原来还可以这样写"呢?下面列出一些 ...

  8. 这些API接口,随便拿出来一个就能装逼、赚钱

    首发链接:这些API接口,随便拿出来一个就能装逼.赚钱 "想写个 App 练手,有什么有趣的 API 接口推荐吗?" 这是知乎上的一个很好的问题.我们为你整理了这些答案,下面的几乎 ...

  9. 计算机病毒装逼桌面,3个Win10神秘装逼小技巧

    电脑很多人每天都在用,不少人也觉得自己对于电脑非常的熟悉.但电脑中还是隐藏着一些非常实用的小技巧,这些鲜为人知的功能不但能提高你的工作效率,还能让你在好友面前小小的装一把哦.今天小编针对Win10电脑 ...

最新文章

  1. 二叉树的先序遍历(非递归)
  2. 把libreoffice集成到网页中_Python3.7.3安装教程并集成Sublime Text3
  3. 常见DDoS技术方法和对应防御措施
  4. 牛客网--字符串合并处理(Java)
  5. mysql和mongodb存储时间_MongoDB存储时间
  6. Linux E: 无法定位软件包
  7. python之numpy之方差numpy.var
  8. Oracle VM VirtualBox固定ip
  9. 【读书】少有人走的路---自律(斯科特 派克)
  10. 软件测试类型——集成测试
  11. linux 截取某一段时间的日志,存储到另一个文件中
  12. 取小数点前两位,并四舍五入
  13. idea通过添加补丁来破解
  14. opencv实现色彩还原(白平衡)
  15. 计算机表格怎么取消分页,Excel表格自动分页、取消分页等技巧 专家详解
  16. k8s中将flannel网络切换calico网络
  17. word2013中插入参考文献
  18. Unreal Engin_画廊制作笔记 _007Fog处理,雾的设置
  19. Computer Architectrure: Quantitative Approch 第三章第十三节
  20. echarts中渐变色的使用

热门文章

  1. 2022-2028年中国植物蛋白饮品行业市场全景调研及战略咨询研究报告
  2. mybatis-plus的使用 ------ 进阶
  3. 湖南计算机本科,湖南搞计算机科学与技术的本科有哪些?
  4. 《现代密码学》第二章——完善保密加密
  5. oracle系统表空间和自定义表空间
  6. Android如何实现简单的手机桌面GridView
  7. Pythont tip
  8. 《Alogrithms》算法学习笔记——第一章:导语与数论
  9. 硬件基础知识----(1)基本概念
  10. matlab carcasonne,米迪运河.ppt