一个方法解决6道买卖股票题:来自LeetCode题解

一、穷举框架

利用「状态」进行穷举。我们具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。

for 状态1 in 状态1的所有取值:for 状态2 in 状态2的所有取值:for ...dp[状态1][状态2][...] = 择优(选择1,选择2...)

每天都有三种「选择」:买入、卖出、无操作。这个问题的「状态」有三个,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的 rest 的状态,我们不妨用 1 表示持有,0 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:

dp[i][k][0 or 1]
0 <= i <= n-1, 1 <= k <= K
n 为天数,大 K 为最多交易数
此问题共 n × K × 2 种状态,全部穷举就能搞定。for 0 <= i < n:for 1 <= k <= K:for s in {0, 1}:dp[i][k][s] = max(buy, sell, rest)

用自然语言描述出每一个状态的含义,dp[3][2][1]的含义就是:今天是第3天,我现在手上持有着股票,至今最多进行 2 次交易。我们想求的最终答案是 dp[n - 1][K][0],即最后一天,最多允许 K 次交易,最多获得多少利润。因为 [1] 代表手上还持有股票,[0] 表示手上的股票已经卖出去了,很显然后者得到的利润一定大于前者。
记住如何解释「状态」,一旦你觉得哪里不好理解,把它翻译成自然语言就容易理解了。

二、状态转移框架

状态转移图:

根据图写出状态转移方程:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])max(   选择 rest  ,           选择 sell      )解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])max(   选择 rest  ,           选择 buy         )解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。

如果 buy,就要从利润中减去 prices[i],如果 sell,就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 k 的限制,我们在选择 buy 的时候,把 k 减小了 1(也可以在 sell 的时候减 1)。
定义 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
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。

把上面的状态转移方程总结一下:

base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

下面开始具体化。

三、解决题目

第一题,k = 1

直接套状态转移方程,根据 base case,可以做一些化简:

dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) = max(dp[i-1][1][1], -prices[i])
解释:k = 0 的 base case,所以 dp[i-1][0][0] = 0。现在发现 k 都是 1,不会改变,即 k 对状态转移已经没有影响了。
可以进行进一步化简去掉所有 k:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])

直接写出代码:

if(prices.size()<2) return 0;
int n = prices.size();
int dp[n][2];
for (int i = 0; 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], -prices[i]);
}
return dp[n - 1][0];

显然 i = 0 时 dp[i-1] 是不合法的。这是因为我们没有对 i 的 base case 进行处理。可以这样处理:

for (int i = 0; i < n; i++) {if (i - 1 == -1) {dp[i][0] = 0;// 解释://   dp[i][0] // = max(dp[-1][0], dp[-1][1] + prices[i])// = max(0, -infinity + prices[i]) = 0dp[i][1] = -prices[i];//解释://   dp[i][1] // = max(dp[-1][1], dp[-1][0] - prices[i])// = max(-infinity, 0 - prices[i]) // = -prices[i]continue;}dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]);dp[i][1] = max(dp[i-1][1], -prices[i]);
}
return dp[n - 1][0];

第一题就解决了,但是这样处理 base case 很麻烦,而且注意一下状态转移方程,新状态只和相邻的一个状态有关,其实不用整个 dp 数组,只需要一个变量储存相邻的那个状态就足够了,这样可以把空间复杂度降到 O(1):

// k == 1
int maxProfit_k_1(vector<int>& prices) {int n = prices.size();// base case: dp[-1][0] = 0, dp[-1][1] = -infinityint dp_i_0 = 0, dp_i_1 = INT_MIN;for (int i = 0; i < n; i++) {// dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])dp_i_0 = max(dp_i_0, dp_i_1 + prices[i]);// dp[i][1] = max(dp[i-1][1], -prices[i])dp_i_1 = max(dp_i_1, -prices[i]);}return dp_i_0;
}

两种方式都是一样的,不过这种编程方法简洁很多。但是如果没有前面状态转移方程的引导,是肯定看不懂的。后续的题目,主要写这种空间复杂度 O(1) 的解法。

第二题,k = +infinity

如果 k 为正无穷,那么就可以认为 k 和 k - 1 是一样的。可以这样改写框架:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])= max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])我们发现数组中的 k 已经不会改变了,也就是说不需要记录 k 这个状态了:
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])

直接翻译成代码:

int maxProfit_k_inf(vector<int>& prices) {int n = prices.size();int dp_i_0 = 0, dp_i_1 = INT_MIN;for (int i = 0; i < n; i++) {int tmp = dp_i_0;dp_i_0 = max(dp_i_0, dp_i_1 + prices[i]);dp_i_1 = max(dp_i_1, tmp - prices[i]);}return dp_i_0;
}

第三题,k = +infinity with cooldown

每次 sell 之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可:

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。

翻译成代码:

int maxProfit_with_cool(vector<int>& prices) {int n = prices.size();int dp_i_0 = 0, dp_i_1 = INT_MIN;int dp_pre_0 = 0; // 代表 dp[i-2][0]for (int i = 0; i < n; i++) {int tmp = dp_i_0;dp_i_0 = max(dp_i_0, dp_i_1 + prices[i]);dp_i_1 = max(dp_i_1, dp_pre_0 - prices[i]);dp_pre_0 = tmp;}return dp_i_0;
}

第四题,k = +infinity with fee

每次交易要支付手续费,只要把手续费从利润中减去即可。改写方程:

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)
解释:相当于买入股票的价格升高了。
在第一个式子里减也是一样的,相当于卖出股票的价格减小了。

直接翻译成代码:

int maxProfit_with_fee(vector<int>& prices, int fee) {int n = prices.size();int dp_i_0 = 0, dp_i_1 = INT_MIN;for (int i = 0; i < n; i++) {int tmp = dp_i_0;dp_i_0 = max(dp_i_0, dp_i_1 + prices[i]);dp_i_1 = max(dp_i_1, tmp - prices[i] - fee);}return dp_i_0;
}

第五题,k = 2

k = 2 和前面题目的情况稍微不同,因为上面的情况都和 k 的关系不太大。要么 k 是正无穷,状态转移和 k 没关系了;要么 k = 1,跟 k = 0 这个 base case 挨得近,最后也没有存在感。

这道题 k = 2 和后面要讲的 k 是任意正整数的情况中,对 k 的处理就凸显出来了。我们直接写代码,边写边分析原因。

原始的动态转移方程,没有可化简的地方
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

按照之前的代码,我们可能想当然这样写代码(错误的):

int k = 2;
int dp[n][k + 1][2];
for (int i = 0; i < n; i++)if (i - 1 == -1) { /* 处理一下 base case*/ }dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
return dp[n - 1][k][0];

我们必须穷举所有状态。其实我们之前的解法,都在穷举所有状态,只是之前的题目中 k 都被化简掉了。这道题由于没有消掉 k 的影响,所以必须要对 k 进行穷举:

int max_k = 2;
int dp[n][max_k + 1][2];
for (int i = 0; i < n; i++) {for (int k = max_k; k >= 1; k--) {if (i - 1 == -1) { /*处理 base case */ }dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);}
}
// 穷举了 n × max_k × 2 个状态,正确。
return dp[n - 1][max_k][0];

如果不理解,可以返回第一点「穷举框架」重新阅读体会一下。

这里 k 取值范围比较小,所以可以不用 for 循环,直接把 k = 1 和 2 的情况手动列举出来也可以:

dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i])
dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i])
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], -prices[i])int maxProfit_k_2(vector<int>& prices) {int dp_i10 = 0, dp_i11 = INT_MIN;int dp_i20 = 0, dp_i21 = INT_MIN;for (int price : prices) {dp_i20 = max(dp_i20, dp_i21 + price);dp_i21 = max(dp_i21, dp_i10 - price);dp_i10 = max(dp_i10, dp_i11 + price);dp_i11 = max(dp_i11, -price);}return dp_i20;
}

有状态转移方程和含义明确的变量名指导,很容易看懂。其实可以把上述四个变量换成 a, b, c, d来装b。

第六题,k = any integer

有了上一题 k = 2 的铺垫,这题应该和上一题的第一个解法没啥区别。但是出现了一个超内存的错误,原来是传入的 k 值会非常大,dp 数组太大了。

一次交易由买入和卖出构成,至少需要两天。所以说有效的限制 k 应该不超过 n/2,如果超过,就没有约束作用了,相当于 k = +infinity。这种情况是之前解决过的。

直接把之前的代码重用:

int maxProfit_k_any(int max_k, vector<int>& prices) {int n = prices.size();if (max_k > n / 2) return maxProfit_k_inf(prices);int dp[n][max_k + 1][2];for (int i = 0; i < n; i++) dp[i][0][0]=0; dp[i][0][1]=INT_MIN;for (int k = max_k; k >= 1; k--) {if (i - 1 == -1) {dp[i][j][0]=0; dp[i][j][1]=-prices[i];continue;}dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);     }return dp[n - 1][max_k][0];
}

四、总结

关键就在于列举出所有可能的「状态」,然后想想怎么穷举更新这些「状态」。具体到股票买卖问题,我们发现了三个状态,使用了一个三维数组,无非还是穷举 + 更新。
不要被各种高大上的名词吓到,再多的困难问题,奇技淫巧,也不过是基本套路的不断升级组合产生的。只要把住算法的底层原理,即可举一反三,逐个击破。

参考: LeetCode英文题解

LeetCode 买卖股票的最佳时机 6道题1个解法总结相关推荐

  1. LeetCode买卖股票的最佳时机系列总结

    LeetCode买卖股票的最佳时机系列总结 此类动态规划从二维动规理解后优化到一维动规,部分题目还可以用到贪心. 目录: 121 买卖股票的最佳时机1 122 买卖股票的最佳时机2 123 买卖股票的 ...

  2. LeetCode 买卖股票的最佳时机 - 超详细讲解系列题

    1.分析 使用通用方法,也即动态规划DP (1)LeetCode 121. 买卖股票的最佳时机 class Solution {public int maxProfit(int[] prices) { ...

  3. Leetcode 买卖股票的最佳时机

    买卖股票的最佳时机 题目描述: 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格. 你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖 ...

  4. leetcode 买卖股票的最佳时机 II

    201 / 201 个通过测试用例 状态:通过 执行用时:2 ms 内存消耗:37.4 MB 提交时间:6 月,3 周之前 class Solution {public int maxProfit(i ...

  5. 算法训练Day49 | Leetcode121. 买卖股票的最佳时机(只能买卖一次);LeetCode122. 买卖股票的最佳时机II(可以买卖多次)

    目录 Leetcode121. 买卖股票的最佳时机 方法一:暴力解法 1. 思路 2. 代码实现 3. 复杂度分析 4. 思考与收获 方法二:贪心算法 1. 思路 2. 代码实现 3. 复杂度分析 方 ...

  6. LeetCode 188. 买卖股票的最佳时机 IV(动态规划)

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

  7. 【LeetCode】买卖股票的最佳时机含手续费 [M](动态规划)

    714. 买卖股票的最佳时机含手续费 - 力扣(LeetCode) 一.题目 给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 :整数 fee 代表了交易股票的手续费用 ...

  8. (C++)Leetcode狂刷200题——标签“数组篇--简单难度10道 #121. 买卖股票的最佳时机

    第五道题 #121. 买卖股票的最佳时机 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格. 如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大 ...

  9. 《LeetCode力扣练习》第121题 买卖股票的最佳时机 Java

    <LeetCode力扣练习>第121题 买卖股票的最佳时机 Java 一.资源 题目: 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价 ...

最新文章

  1. redis报protocol error的真正原凶
  2. qt mysql显示文件名字_【实例】Qt获取文件属性
  3. 开始学习RED HAT LINUX了
  4. C高级第一次PTA作业(2)
  5. (19)脚手架工具Yeoman
  6. 初学网站建设,要学习些什么?
  7. 嘘!你与谷歌语音助手的对话,可能已经泄露……
  8. 22男人应该明白的道理
  9. autogen.sh 的使用
  10. 肝毒净-道格拉斯实验室
  11. [转]vs2003,安装程序检测到另一个程序要求计算机重新启动
  12. project甘特图导出图片_Project将任务及甘特图导出Excel的方法
  13. armbian 斐讯n1_斐讯N1刷Armbian Linux做服务器
  14. 为什么说程序员的前三年不要太看重工资水平?
  15. 基金购买和赎回线下业务
  16. 弱电系统的布线施工应注意什么?
  17. ixgbe 驱动安装
  18. Android屏幕监视方法,Android 关于手机屏幕的那些事
  19. 极路由——彻底凉了,云插件服务都关闭了,还没root的抓紧了
  20. 轻松掌握CAP定理精髓

热门文章

  1. android 笔记管理app,安卓有什么记笔记的软件?安卓平台好用的笔记app
  2. php手机短信接口发送验证码,PHP手机短信接口发送验证码
  3. python打开闪一下就关闭_如何使得 python 脚本 不一闪而过
  4. 什么是云计算?云计算学习方向有哪些?
  5. java 导出word 带复选框和图片
  6. 深度学习--- Xception迁移学习及实例
  7. [原创] Win7全自动精简批处理_绝对原创,绝对给力_感谢无忧给了我一年的潜水...
  8. (软件工程)——需求分析
  9. linux7.0可以装什么宝塔版本,宝塔 7.0.3 专业开心版一键安装可使用
  10. Figma如何导出jpg格式?